12 Commits

7 changed files with 305 additions and 93 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
config.ini config.ini
tasmotonov-gui-config.json
__pycache__/ __pycache__/

View File

@@ -10,6 +10,10 @@ Maybe it's straightforward or obvious, but just for completeness: the name comes
1. The direct bond to tasmota (written for its "API" if one can call the HTTP endpoints an API) 1. The direct bond to tasmota (written for its "API" if one can call the HTTP endpoints an API)
2. The ability to turn on and off tasmota devices ("on" and "off" pronounced directly one after the other sounds (a bit) like "onov") 2. The ability to turn on and off tasmota devices ("on" and "off" pronounced directly one after the other sounds (a bit) like "onov")
## Screenshots
![Tab 1 of the GUI](assets/gui_tab1.png)
![Tab 2 of the GUI](assets/gui_tab1.png)
## CLI Usage ## CLI Usage

BIN
assets/gui_tab1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
assets/gui_tab2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,40 +1,154 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""
Tasmotonov GUI - A simple Qt wrapper around the tasmotonov.py script
Copyright (C) 2025 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import sys import sys
import subprocess import tasmotonov
import json
from PySide6.QtUiTools import QUiLoader from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication,QFileDialog from PySide6.QtWidgets import QApplication,QFileDialog
from PySide6.QtCore import QFile, QUrl, QIODevice from PySide6.QtCore import QFile, QUrl, QIODevice
# set some standard values
filename = "" filename = ""
action = "toggle" action = "toggle"
def tab1_load_file(): def load_config(file="tasmotonov-gui-config.json"): # load config from configparser.ConfigParser object
try:
with open(file, "r") as f:
to_load = json.loads(f.read())
global filename, action
if "file" in to_load:
filename = to_load['file']
if filename != "":
try:
tab1_load_file()
except FileNotFoundError:
tab1_clear()
if "inline" in to_load:
window.tab2_plainTextEdit.setPlainText(to_load['inline'])
if "action" in to_load:
a_new = to_load['action']
if a_new != "":
action = a_new
if "tab_index" in to_load:
tab_index = to_load["tab_index"]
if type(tab_index) == int and (0 <= tab_index < window.tabWidget.count()):
window.tabWidget.setCurrentIndex(to_load["tab_index"])
except FileNotFoundError:
pass # no config there yet
def save_config(file="tasmotonov-gui-config.json"):
window.tab3_textBrowser.append("Saving configuration!")
global filename, action
to_save = {}
to_save['file'] = str(filename)
to_save['inline'] = str(window.tab2_plainTextEdit.toPlainText())
to_save['action'] = str(action)
to_save['tab_index'] = window.tabWidget.currentIndex()
with open(file, 'w') as f:
f.write(json.dumps(to_save))
# tab1 slots
def tab1_load_file_pressed():
global filename global filename
dialog = QFileDialog() dialog = QFileDialog()
dialog.setFileMode(QFileDialog.AnyFile) dialog.setFileMode(QFileDialog.AnyFile)
#fileNames = QStringList()
if dialog.exec(): if dialog.exec():
filename = dialog.selectedFiles()[0] filename = dialog.selectedFiles()[0]
window.tab1_label.setText(f"File loaded: {filename}") tab1_load_file()
window.tab1_label.show() def tab1_load_file():
window.tab1_filecontent_textBrowser.setSource(QUrl(filename)) global filename
window.tab1_label.setText(f"File loaded: {filename}")
window.tab3_textBrowser.append(f"File loaded: {filename}")
window.tab1_label.show()
window.tab1_filecontent_textBrowser.setSource(QUrl(filename))
window.tab1_label_help.show()
# clear listWidget and add the items
window.tab1_listWidget.clear()
addresses = tasmotonov.TasmotonovRunner("file", filename, action, False, False).get_addresses()
items_to_add = []
for address, comment in addresses.items():
if comment == "":
items_to_add.append(address)
else:
items_to_add.append(f"{comment} [{address}]")
window.tab1_listWidget.addItems(items_to_add)
def tab1_clear():
global filename
filename = ""
window.tab1_label.setText(f"File loaded: ")
window.tab3_textBrowser.append(f"File unloaded")
window.tab1_label.hide()
window.tab1_label_help.hide()
window.tab1_filecontent_textBrowser.clear()
window.tab1_listWidget.clear()
def tab1_action(): def tab1_action():
if filename != "": if filename != "":
p = subprocess.Popen(["python3", "tasmotonov.py", "file", filename, action], stdout=subprocess.PIPE) tasmotonov_runner = tasmotonov.TasmotonovRunner("file", filename, action, False, False)
out, err = p.communicate() tasmotonov_runner.run()
window.tab3_textBrowser.append("==== RUNNING ====\n" + str(out) + "\n=================\n") window.tab3_textBrowser.append("\n==== RUNNING ====\n\n" + str(tasmotonov_runner.logger.log_string) + "\n=================\n")
tasmotonov_runner.logger.log_string = ""
else: else:
window.tab3_textBrowser.append("Will not run, no file selected!") window.tab3_textBrowser.append("Will not run, no file selected!")
def tab1_single_action():
if filename != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("file", filename, action, False, False)
tasmotonov_runner.run_single(window.tab1_listWidget.currentRow())
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
# tab2 slots
def tab2_plainTextEdit_change():
# clear listWidget
window.tab2_listWidget.clear()
content = window.tab2_plainTextEdit.toPlainText()
if content != "":
tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False)
addresses = tasmotonov_runner.get_addresses()
items_to_add = []
for address, comment in addresses.items():
if comment == "":
items_to_add.append(address)
else:
items_to_add.append(f"{comment} [{address}]")
window.tab2_listWidget.addItems(items_to_add)
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
def tab2_action(): def tab2_action():
content = window.tab2_plainTextEdit.toPlainText() content = window.tab2_plainTextEdit.toPlainText()
if content != "": if content != "":
p = subprocess.Popen(["python3", "tasmotonov.py", "inline", content, action], stdout=subprocess.PIPE) tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False)
out, err = p.communicate() tasmotonov_runner.run()
window.tab3_textBrowser.append("==== RUNNING ====\n" + str(out) + "\n=================\n") window.tab3_textBrowser.append("\n==== RUNNING ====\n\n" + str(tasmotonov_runner.logger.log_string) + "\n=================\n")
print(out)
else: else:
window.tab3_textBrowser.append("Will not run, no input given!") window.tab3_textBrowser.append("Will not run, no input given!")
def tab2_single_action():
content = window.tab2_plainTextEdit.toPlainText()
tasmotonov_runner = tasmotonov.TasmotonovRunner("inline", content, action, False, False)
if len(tasmotonov_runner.get_addresses()) != 0:
tasmotonov_runner.run_single(window.tab2_listWidget.currentRow())
window.tab3_textBrowser.append(str(tasmotonov_runner.logger.log_string))
# tab3 slots
def tab3_clear():
window.tab3_textBrowser.clear()
# other slots
def select_on(): def select_on():
global action global action
action = "on" action = "on"
@@ -51,7 +165,7 @@ def select_toggle():
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
ui_file_name = "tasmotonov.ui" ui_file_name = "tasmotonov-gui.ui"
ui_file = QFile(ui_file_name) ui_file = QFile(ui_file_name)
if not ui_file.open(QIODevice.ReadOnly): if not ui_file.open(QIODevice.ReadOnly):
print(f"Cannot open {ui_file_name}: {ui_file.errorString()}") print(f"Cannot open {ui_file_name}: {ui_file.errorString()}")
@@ -64,14 +178,29 @@ if __name__ == "__main__":
sys.exit(-1) sys.exit(-1)
window.tab1_label.hide() window.tab1_label.hide()
window.tab1_load_file_pushButton.clicked.connect(tab1_load_file) window.tab1_label_help.hide()
window.tab1_load_file_pushButton.clicked.connect(tab1_load_file_pressed)
window.tab1_clear_pushButton.clicked.connect(tab1_clear)
window.tab1_action_pushButton.clicked.connect(tab1_action) window.tab1_action_pushButton.clicked.connect(tab1_action)
window.tab1_single_action_pushButton.clicked.connect(tab1_single_action)
window.tab2_plainTextEdit.textChanged.connect(tab2_plainTextEdit_change)
window.tab2_action_pushButton.clicked.connect(tab2_action) window.tab2_action_pushButton.clicked.connect(tab2_action)
window.tab2_single_action_pushButton.clicked.connect(tab2_single_action)
window.radioButton_on.clicked.connect(select_on) window.radioButton_on.clicked.connect(select_on)
window.radioButton_off.clicked.connect(select_off) window.radioButton_off.clicked.connect(select_off)
window.radioButton_toggle.clicked.connect(select_toggle) window.radioButton_toggle.clicked.connect(select_toggle)
window.tab3_clear_button.clicked.connect(tab3_clear)
load_config()
window.show() window.show()
sys.exit(app.exec()) # execute app loop and define exit strategy
code = app.exec()
save_config()
sys.exit(code)

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>630</width> <width>1166</width>
<height>404</height> <height>546</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -20,8 +20,8 @@
<enum>QTabWidget::TabShape::Rounded</enum> <enum>QTabWidget::TabShape::Rounded</enum>
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item row="0" column="1"> <item>
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="tabPosition"> <property name="tabPosition">
<enum>QTabWidget::TabPosition::East</enum> <enum>QTabWidget::TabPosition::East</enum>
@@ -30,7 +30,7 @@
<enum>QTabWidget::TabShape::Rounded</enum> <enum>QTabWidget::TabShape::Rounded</enum>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>1</number> <number>0</number>
</property> </property>
<property name="usesScrollButtons"> <property name="usesScrollButtons">
<bool>true</bool> <bool>true</bool>
@@ -43,17 +43,34 @@
<string>From File</string> <string>From File</string>
</attribute> </attribute>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="3" column="0"> <item row="8" column="0">
<widget class="QTextBrowser" name="tab1_filecontent_textBrowser"> <widget class="QPushButton" name="tab1_action_pushButton">
<property name="frameShadow"> <property name="minimumSize">
<enum>QFrame::Shadow::Sunken</enum> <size>
<width>414</width>
<height>0</height>
</size>
</property> </property>
<property name="openExternalLinks"> <property name="text">
<bool>true</bool> <string>Run bulk action (specified below)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="8" column="1">
<widget class="QPushButton" name="tab1_single_action_pushButton">
<property name="text">
<string>Run single action (specified below)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="tab1_label_help">
<property name="text">
<string>Select an item and run an action:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="tab1_label"> <widget class="QLabel" name="tab1_label">
<property name="enabled"> <property name="enabled">
<bool>true</bool> <bool>true</bool>
@@ -69,28 +86,77 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0"> <item row="6" column="0">
<widget class="QPushButton" name="tab1_action_pushButton"> <widget class="QTextBrowser" name="tab1_filecontent_textBrowser">
<property name="text"> <property name="sizePolicy">
<string>Run!</string> <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>414</width>
<height>0</height>
</size>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Sunken</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="6" column="1">
<widget class="QListWidget" name="tab1_listWidget"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QPushButton" name="tab1_load_file_pushButton"> <widget class="QPushButton" name="tab1_load_file_pushButton">
<property name="text"> <property name="text">
<string>Load file</string> <string>Load file</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="tab1_clear_pushButton">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab2_from_inline"> <widget class="QWidget" name="tab2_from_inline">
<attribute name="title"> <attribute name="title">
<string>From Inline</string> <string>From Inline</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QGridLayout" name="gridLayout">
<item> <item row="1" column="1">
<widget class="QListWidget" name="tab2_listWidget"/>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="tab2_single_action_pushButton">
<property name="text">
<string>Run single action (specified below)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="tab2_action_pushButton">
<property name="text">
<string>Run bulk action (specified below)!</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="tab2_label_help_single">
<property name="text">
<string>Single actions: select the address and click the button below</string>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QPlainTextEdit" name="tab2_plainTextEdit"> <widget class="QPlainTextEdit" name="tab2_plainTextEdit">
<property name="lineWrapMode"> <property name="lineWrapMode">
<enum>QPlainTextEdit::LineWrapMode::WidgetWidth</enum> <enum>QPlainTextEdit::LineWrapMode::WidgetWidth</enum>
@@ -106,13 +172,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="tab2_action_pushButton">
<property name="text">
<string>Run!</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab3_output"> <widget class="QWidget" name="tab3_output">
@@ -128,13 +187,27 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QTextBrowser" name="tab3_textBrowser"/> <widget class="QTextBrowser" name="tab3_textBrowser">
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="tab3_clear_button">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QRadioButton" name="radioButton_on"> <widget class="QRadioButton" name="radioButton_on">

View File

@@ -20,10 +20,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import argparse # to parse the cli args! import argparse # to parse the cli args!
import requests # needed to access the http endpoints import requests # needed to access the http endpoints
import threading # for running the requests simultaneously
import ipaddress # to validate IP addresses import ipaddress # to validate IP addresses
from fqdn import FQDN # validate FQDNs from fqdn import FQDN # validate FQDNs
version = "v1.1.0" version = "v1.2.0"
# some helpers / utils # some helpers / utils
@@ -99,99 +100,103 @@ class TasmotonovRunner:
raise ValueError(error_string) raise ValueError(error_string)
self.logger.log(f'Initialized runner: source={source}, data={data}, action={action}, verbose={verbose}') self.logger.log(f'Initialized runner: source={source}, data={data}, action={action}, verbose={verbose}')
self.tasmota_addresses = self.remove_invalid_addresses(self.clean_addresses(self.parse_addresses())) # returns a list self.set_valid_addresses_and_comments(self.parse_addresses())
self.logger.log(f'Validated addresses: distilled the following data. Devices added: {self.tasmota_addresses}')
def parse_addresses(self): def parse_addresses(self):
# convert the given adresses to a list # convert the given adresses to a list
tasmota_addresses = [] tasmota_addresses_raw = []
if self.source == 'file': if self.source == 'file':
try: try:
self.logger.log('Now trying to open the given file...', end='') self.logger.log('Now trying to open the given file...', end='')
with open(self.data, 'r') as file: with open(self.data, 'r') as file:
contents = file.read() contents = file.read()
if ';' in contents: if ',' in contents:
tasmota_addresses = contents.split(';') tasmota_addresses_raw = contents.split(',')
elif ',' in contents: elif ';' in contents:
tasmota_addresses = contents.split(',') tasmota_addresses_raw = contents.split(';')
elif '\n' in contents: elif '\n' in contents:
tasmota_addresses = contents.split('\n') tasmota_addresses_raw = contents.split('\n')
elif ' ' in contents:
tasmota_addresses = contents.split(' ')
else: else:
tasmota_addresses = [contents] tasmota_addresses_raw = [contents]
self.logger.log('Done.') self.logger.log('Done.')
self.logger.log(f'Collected addresses: {tasmota_addresses}') self.logger.log(f'Collected addresses: {tasmota_addresses_raw}')
except FileNotFoundError as e: except FileNotFoundError as e:
self.logger.log_error(f'Failed reading addresses. File with the path "{self.data} can\'t be opened because it doesn\'t exist. Exiting.') self.logger.log_error(f'Failed reading addresses. File with the path "{self.data} can\'t be opened because it doesn\'t exist. Exiting.')
raise e raise e
elif self.source == 'inline': elif self.source == 'inline':
self.logger.log('Now reading from inline... ', end='') self.logger.log('Now reading from inline... ', end='')
if ';' in self.data: if ',' in self.data:
tasmota_addresses = self.data.split(';') tasmota_addresses_raw = self.data.split(',')
elif ',' in self.data: elif ';' in self.data:
tasmota_addresses = self.data.split(',') tasmota_addresses_raw = self.data.split(';')
elif '\n' in self.data: elif '\n' in self.data:
tasmota_addresses = self.data.split('\n') tasmota_addresses_raw = self.data.split('\n')
elif ' ' in self.data:
tasmota_addresses = self.data.split(' ')
else: else:
tasmota_addresses = [self.data] tasmota_addresses_raw = [self.data]
self.logger.log('Done.') self.logger.log('Done.')
self.logger.log(f'Collected addresses: {tasmota_addresses}') self.logger.log(f'Collected addresses: {tasmota_addresses_raw}')
return tasmota_addresses_raw
return tasmota_addresses
def clean_addresses(self, addresses_raw):
# clean them up (e.g. remove newlines if the file has a random newline somewhere
tasmota_addresses_cleaned = []
for address in addresses_raw:
to_add = address.replace('\n', '').replace(';', '').replace(',', '').replace(' ', '')
if to_add != '':
tasmota_addresses_cleaned.append(to_add)
return tasmota_addresses_cleaned
def remove_invalid_addresses(self, addressse_raw): def set_valid_addresses_and_comments(self, tasmota_addresses_raw):
# now check data for consistency and integrity (is it really an ip address or a domain name?) # now check data for consistency and integrity (is it really an ip address or a domain name?)
# remove the ones that are not valid and log a warning # remove the ones that are not valid and log a warning
tasmota_addresses_validated = [] tasmota_addresses_validated = {}
self.logger.log('Now validating given addresses...') self.logger.log('Now validating given addresses...')
for address in addressse_raw: for address in tasmota_addresses_raw:
if is_ip(address) or is_fqdn(address): address_split = address.split("#") # split to separate existing comments
tasmota_addresses_validated.append(address) address_cleaned = address_split[0].replace('\n', '').replace(';', '').replace(',', '').replace(' ', '')
if len(address_split) > 1:
comment = address.split("#")[-1].lstrip()
else: else:
self.logger.log_warning(f'The address "{address}" is neither a valid IP address nor a FQDN / domain name; SKIPPING THIS ONE.') comment = ""
self.logger.log('Validated given adddresses.') if address_cleaned != "":
if is_ip(address_cleaned) or is_fqdn(address_cleaned):
tasmota_addresses_validated[address_cleaned] = comment
else:
self.logger.log_warning(f'The address "{address_cleaned}" (comment: "{comment}") is neither a valid IP address nor a FQDN / domain name; SKIPPING THIS ONE.')
return tasmota_addresses_validated self.tasmota_addresses = tasmota_addresses_validated
# some getters # some getters
def get_addresses(self): def get_addresses(self):
return self.tasmota_addresses return self.tasmota_addresses
def get_address(self, index): def get_address(self, index):
return self.tasmota_addresses[index] return {list(self.tasmota_addresses.keys())[index]: list(self.tasmota_addresses.values())[index]}
def run_custom_action(self, action): def run_custom_action(self, action):
# and finally turn on or off or toggle the lights! # and finally turn on or off or toggle the lights!
for address in self.tasmota_addresses: def thread_runner(address):
self.logger.log('Running the action "{action}" on the following device: {address}') self.logger.log(f'Running the action "{action}" on the following device: {address}')
try: try:
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}') answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}')
success = True success = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
self.logger.log_warning(f'Could not connect to "{address}". Skipping...') self.logger.log_warning(f'Could not connect to "{address}". Skipping...')
success = False success = False
if answer.status_code != 200: if not str(answer.status_code).startswith("20"):
self.logger.log_warning(f'{address} returned the following non-200 status code: {answer.status_code}. Skipping...') self.logger.log_warning(f'{address} returned the following non-20x status code: {answer.status_code}. Skipping...')
success = False success = False
if success: if success:
self.logger.log_success(f"Ran action {str(action).upper()} on {self.logger.COLORS['OKCYAN']}{address}{self.logger.COLORS['ENDC']} successfully.") self.logger.log_success(f"Ran action {str(action).upper()} on {self.logger.COLORS['OKCYAN']}{address}{self.logger.COLORS['ENDC']} successfully.")
threads = []
for address in list(self.tasmota_addresses.keys()):
t = threading.Thread(target=thread_runner, args=(address,))
threads.append(t)
t.start()
# wait for all threads to finish execution
for t in threads:
t.join()
def run(self): def run(self):
self.run_custom_action(self.action) self.run_custom_action(self.action)
def run_single_custom_action(self, index, action): def run_single_custom_action(self, index, action):
address = self.tasmota_addresses[index] address = list(self.tasmota_addresses.keys())[index]
self.logger.log('Running the action "{action}" on the following device: {address}') self.logger.log(f'Running the action "{action}" on the following device: {address}')
try: try:
answer = None answer = None
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}') answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(action).lower()}')
@@ -217,10 +222,10 @@ if __name__ == '__main__':
prog='Tasmotonov - simply toggle multiple tasmota lights', prog='Tasmotonov - simply toggle multiple tasmota lights',
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
description='A very simple script which allows you to turn on/off multiple tasmota devices specified.', description='A very simple script which allows you to turn on/off multiple tasmota devices specified.',
epilog='Info: if you choose a file as source, this files needs to contain the addresses of the tasmota devices either comma-separated, semicolon-separated, space-separated, or newline-separated!\n\n© Benjamin Burkhardt, 2025') epilog='Info: if you choose a file as source, this files needs to contain the addresses of the tasmota devices either comma-separated, semicolon-separated, or newline-separated (each entry can have a comment starting with a # (hashtag)!\n\n© Benjamin Burkhardt, 2025')
parser.add_argument('source', help='Select either to read the adresses (of the devices) from a "file" or from "inline"', choices=['file', 'inline']) parser.add_argument('source', help='Select either to read the adresses (of the devices) from a "file" or from "inline"', choices=['file', 'inline'])
parser.add_argument('data', help='Either the path to the file, or a comma-, space- or semicolon-separated list of tasmota adresses.') parser.add_argument('data', help='Either the path to the file, or a comma-, semicolon- or newline-separated list of tasmota adresses.')
parser.add_argument('action', help='Select to turn all tasmota devices "on" or "off" or "toggle" (case insensitive)', choices=['on', 'off', 'toggle']) parser.add_argument('action', help='Select to turn all tasmota devices "on" or "off" or "toggle" (case insensitive)', choices=['on', 'off', 'toggle'])
parser.add_argument('-v', '--verbose', help='Turn on verbose file output', action='store_true') parser.add_argument('-v', '--verbose', help='Turn on verbose file output', action='store_true')
parser.add_argument('--version', action='version', version=f'Tasmotonov.py {version}') parser.add_argument('--version', action='version', version=f'Tasmotonov.py {version}')