158 lines
6.3 KiB
Python
Executable File
158 lines
6.3 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
"""
|
|
Tasmotonov - A very simple script which allows you to turn on/off (or toggle) multiple tasmota devices specified.
|
|
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 # for exiting with the correct exit code
|
|
import argparse # to parse the cli args!
|
|
import requests # needed to access the http endpoints
|
|
import ipaddress # to validate IP addresses
|
|
from fqdn import FQDN # validate FQDNs
|
|
|
|
version = "v0.1.1"
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog='Tasmotonov - simply toggle multiple tasmota lights',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
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, or newline-separated!\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('data', help='Either the path to the file, or a comma- or semicolon-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('-v', '--verbose', help='Turn on verbose file output', action='store_true')
|
|
parser.add_argument('--version', action='version', version=f'Tasmotonov.py {version}')
|
|
|
|
|
|
# some helpers / utils
|
|
|
|
def is_ip(string):
|
|
try:
|
|
i = ipaddress.ip_address(string)
|
|
del i
|
|
except ValueError:
|
|
return False
|
|
return True
|
|
|
|
def is_fqdn(string):
|
|
return FQDN(string).is_valid
|
|
|
|
|
|
# logging
|
|
|
|
class COLORS:
|
|
HEADER = '\033[95m'
|
|
OKBLUE = '\033[94m'
|
|
OKCYAN = '\033[96m'
|
|
OKGREEN = '\033[92m'
|
|
WARNING = '\033[93m'
|
|
FAIL = '\033[91m'
|
|
ENDC = '\033[0m'
|
|
BOLD = '\033[1m'
|
|
UNDERLINE = '\033[4m'
|
|
|
|
def log(to_log, verbose = True, end = '\n'):
|
|
if (verbose and args.verbose) or not verbose:
|
|
print(to_log, end=end)
|
|
def log_error(to_log, end='\n'):
|
|
log(COLORS.FAIL + '[ERROR] ' + COLORS.ENDC + to_log, verbose=False, end=end)
|
|
def log_warning(to_log, end='\n'):
|
|
log(COLORS.WARNING + '[WARNING] ' + COLORS.ENDC + to_log, verbose=False, end=end)
|
|
def log_success(to_log, end='\n'):
|
|
log(COLORS.OKGREEN + '[SUCCESS] ' + COLORS.ENDC + to_log, verbose=False, end=end)
|
|
|
|
|
|
# the real program
|
|
|
|
if __name__ == '__main__':
|
|
args = parser.parse_args() # parse all given arguments
|
|
log(f'Parsed args: {args}')
|
|
|
|
# convert the given adresses to a list
|
|
tasmota_addresses = []
|
|
if args.source == 'file':
|
|
try:
|
|
log('Now trying to open the given file...', end='')
|
|
with open(args.data, 'r') as file:
|
|
contents = file.read()
|
|
if ';' in contents:
|
|
tasmota_addresses = contents.split(';')
|
|
elif ',' in contents:
|
|
tasmota_addresses = contents.split(',')
|
|
elif '\n' in contents:
|
|
tasmota_addresses = contents.split('\n')
|
|
else:
|
|
tasmota_addresses = [contents]
|
|
log('Done.')
|
|
log(f'Collected addresses: {tasmota_addresses}')
|
|
except FileNotFoundError:
|
|
log_error(f'Failed reading addresses. File with the path "{args.data} can\'t be opened because it doesn\'t exist. Exiting.')
|
|
sys.exit(1)
|
|
|
|
elif args.source == 'inline':
|
|
log('Now reading from inline... ', end='')
|
|
if ';' in args.data:
|
|
tasmota_addresses = args.data.split(';')
|
|
elif ',' in args.data:
|
|
tasmota_addresses = args.data.split(',')
|
|
elif '\n' in args.data:
|
|
tasmota_addresses = args.data.split('\n')
|
|
else:
|
|
tasmota_addresses = [args.data]
|
|
log('Done.')
|
|
log(f'Collected addresses: {tasmota_addresses}')
|
|
|
|
else:
|
|
log_error(f'You need to specify either "file" or "inline" as the data source for addresses!')
|
|
sys.exit(1)
|
|
|
|
|
|
# clean them up (e.g. remove newlines if the file has a random newline somewhere
|
|
tasmota_addresses_cleaned = []
|
|
for address in tasmota_addresses:
|
|
if address != '':
|
|
tasmota_addresses_cleaned.append(address.replace('\n', '').replace(';', '').replace(',', ''))
|
|
|
|
# now check data for consistency and integrity (is it really an ip address or a domain name?)
|
|
tasmota_addresses_validated = []
|
|
log('Now validating given addresses...')
|
|
for address in tasmota_addresses_cleaned:
|
|
if is_ip(address) or is_fqdn(address):
|
|
tasmota_addresses_validated.append(address)
|
|
else:
|
|
log_warning(f'The address "{address}" is neither a valid IP address nor a FQDN / domain name; SKIPPING THIS ONE.')
|
|
log('Validated given adddresses.')
|
|
|
|
|
|
# and finally turn on or off or toggle the lights!
|
|
for address in tasmota_addresses_validated:
|
|
log('Running the action "{args.action}" on the following device: {address}')
|
|
try:
|
|
answer = requests.get(f'http://{address}/cm?cmnd=Power%20{str(args.action).lower()}')
|
|
success = True
|
|
except requests.exceptions.ConnectionError:
|
|
log_warning(f'Could not connect to "{address}". Skipping...')
|
|
success = False
|
|
if answer.status_code != 200:
|
|
log_warning(f'{address} returned the following non-200 status code: {answer.status_code}. Skipping...')
|
|
success = False
|
|
if success:
|
|
log_success(f"Ran action {str(args.action).upper()} on {COLORS.OKCYAN}{address}{COLORS.ENDC} successfully.")
|
|
|
|
sys.exit(0)
|