From fe9999b42580d1e21705a0a8a6c37f0fd9ddfe5e Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 09:35:08 +0200 Subject: [PATCH 1/8] Support local API for getlist/pergola, and fix bioclimatic pergola commands --- .gitignore | 5 ++++ get_devices_url.py | 61 +++++++++++++++++++++++++++++++++++++++++++--- tahoma.py | 28 ++++++++++++++------- 3 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e095b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +temp/*.txt +temp/*.log +temp/newrelic.ini +__pycache__/ +*.pyc diff --git a/get_devices_url.py b/get_devices_url.py index ee61ee5..56b22d3 100644 --- a/get_devices_url.py +++ b/get_devices_url.py @@ -9,13 +9,14 @@ import argparse import os import requests +import aiohttp from datetime import datetime, timedelta import json from collections import defaultdict import re from getpass import getpass import time -from pyoverkiz.const import SUPPORTED_SERVERS +from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer from pyoverkiz.client import OverkizClient from pyoverkiz.enums import OverkizCommand from pyoverkiz.models import Command @@ -50,6 +51,31 @@ async def main() -> None: server_choosen = os.path.dirname(os.path.abspath(__file__))+'/temp/server_choosen.txt' init_file = os.path.dirname(os.path.abspath(__file__))+'/__init__.py' + token_file = os.path.dirname(os.path.abspath(__file__))+'/temp/token.txt' + gateway_id_file = os.path.dirname(os.path.abspath(__file__))+'/temp/gateway_id.txt' + local_remote_file = os.path.dirname(os.path.abspath(__file__))+'/temp/local_remote.txt' + + try : + f = open(local_remote_file, 'r') + local_remote = f.read().strip() + f.close() + except FileNotFoundError: + local_remote = "remote" + + try : + f = open(token_file, 'r') + token = f.read().strip() + f.close() + except FileNotFoundError: + token = "" + + try : + f = open(gateway_id_file, 'r') + gateway_id = f.read().strip() + f.close() + except FileNotFoundError: + gateway_id = "" + # consent_statistics = os.path.dirname(os.path.abspath(__file__))+'/temp/consent_statistics.txt' # print("Would you like to share anonymous statistics when you use this functionality?") @@ -321,12 +347,39 @@ def update_file(url, content, sha, message): if args.server: serverchoice = (f'{args.server}') - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: + if local_remote == 'local' and gateway_id and token: + client_kwargs = { + "username": "", + "password": "", + "token": token, + "verify_ssl": False, + "server": OverkizServer( + name="Somfy TaHoma (local)", + endpoint=f"https://gateway-{gateway_id}.local:8443/enduser-mobile-web/1/enduserAPI/", + manufacturer="Somfy", + configuration_url=None, + ), + } + session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) + client = OverkizClient(**client_kwargs, session=session) + try: + await client.login() + except Exception as exception: # pylint: disable=broad-except + await session.close() + print(exception) + print("\nCould not connect to the local API. Falling back to the cloud API.") + local_remote = "remote" + if local_remote != 'local' or not (gateway_id and token): + client = OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) + await client.__aenter__() try: await client.login() except Exception as exception: # pylint: disable=broad-except print(exception) + await client.__aexit__(None, None, None) return + + try: devices = await client.get_devices() scenarios = await client.get_scenarios() try : @@ -363,7 +416,7 @@ def update_file(url, content, sha, message): elif "Light" in device.widget: f12.write(device.label+","+device.id+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_lights) - elif "PositionableTiltedScreen" in device.widget or "PergolaHorizontalAwning" in device.widget: + elif "PositionableTiltedScreen" in device.widget or "PergolaHorizontalAwning" in device.widget or "BioclimaticPergola" in device.widget: f13.write(device.label+","+device.id+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_pergolas) else : @@ -422,6 +475,8 @@ def update_file(url, content, sha, message): i=i+1 except Exception as e : print(e) + finally: + await client.close() print("\nScenes :\n") try : f2.write(f"\nScenes :\n") diff --git a/tahoma.py b/tahoma.py index 4f9ca96..4ba0044 100644 --- a/tahoma.py +++ b/tahoma.py @@ -776,6 +776,7 @@ def remove_accent(old): except ValueError: print("\nDid you downloaded the list of Tahoma's devices ?.\nExecute tahoma --getlist \nFor more info execute tahoma -h or tahoma --info") exit() + widget_of=[] for i in master_list : bad_name.append(i.split(",")[0]) if str(name).startswith("[") : @@ -783,23 +784,28 @@ def remove_accent(old): url.append(i.split(",")[1]) too_many_urls.append(i.split(",")[0]) good_name.append(i.split(",")[0]) + widget_of.append(i.split(",")[2] if len(i.split(",")) > 2 else '') else : if remove_accent(i.split(",")[0]) in remove_accent(str(name)) or remove_accent(str(name)) in remove_accent(i.split(",")[0]) : url.append(i.split(",")[1]) too_many_urls.append(i.split(",")[0]) good_name.append(i.split(",")[0]) + widget_of.append(i.split(",")[2] if len(i.split(",")) > 2 else '') if len(url)== 0 : print("There is no match. The NAME you gave is not exact. Did you mean : "+str(bad_name)+" ? Choose a UNIQUE part of word from this results as NAME argument or use [''] with the full name\nIf you don't find your device in this results try tahoma --getlist\nSee tahoma --list-names for help.") exit() if len(url) > 1 : print("There is more than one match. The NAME you gave is not exact. Choose a UNIQUE part of word from this results as NAME argument or use [''] with the full name : "+str(too_many_urls)+"\nSee tahoma --list-names for help.") exit() + # Bioclimatic pergolas (slats that tilt) use openSlats/closeSlats/setOrientation. + # Other pergolas (awning-type, e.g. PergolaHorizontalAwning/PositionableTiltedScreen) behave like sunscreens/shutters. + is_bioclimatic = 'BioclimaticPergola' in widget_of[0] success = 1 if remove_accent(action).upper() == "OPEN" or remove_accent(action).upper() == "OUVRIR" : - fonction = Command(OverkizCommand.OPEN, [0]) + fonction = Command(OverkizCommand.OPEN_SLATS, []) if is_bioclimatic else Command(OverkizCommand.OPEN, [0]) success = 0 elif remove_accent(action).upper() == 'CLOSE' or remove_accent(action).upper() == "FERMER" : - fonction = Command(OverkizCommand.CLOSE, [0]) + fonction = Command(OverkizCommand.CLOSE_SLATS, []) if is_bioclimatic else Command(OverkizCommand.CLOSE, [0]) success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' function is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") @@ -810,15 +816,19 @@ def remove_accent(old): success = 0 elif str(action).isnumeric() == True : if 0 <= int(action) <= 100 : - fonction = Command(OverkizCommand.SET_CLOSURE, [int(action)]) + if is_bioclimatic : + fonction = Command(OverkizCommand.SET_ORIENTATION, [int(action)]) + print('Will set orientation to '+str(action)+' %') + else : + fonction = Command(OverkizCommand.SET_CLOSURE, [int(action)]) + print('Will close to '+str(action)+' %') + print("Be careful! This function is only available for IO protocols. It doesn't work with RTS devices...") success = 0 - print('Will close to '+str(action)+' %') - print("Be careful! This function is only available for IO protocols. It doesn't work with RTS devices...") else : print("Your ACTION must be between 0 and 100. You have entered: "+str(action)) else : print( "\n'"+action+"'"+" is not a valide action.\n") - print("Please provide one of this argument as action : [open close stop my]") + print("Please provide one of this argument as action : [open close stop my NUMBER]") str1 = " " if success == 0: # print("Output action : "+remove_accent(action).upper()+" "+remove_accent(category)+" "+str1.join(good_name)+ " \nwith url : "+str1.join(url)) @@ -828,7 +838,7 @@ def remove_accent(old): with open(log_place, "a") as f: f.write(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}-{message}\n") f.close() - except: + except: print('Could not access the log file. Permission denied') print("If you don’t want to see this message again, reconfigure Tahoma to not create a log file (tahoma --configure) \nor install Tahoma in an accessible folder.") print(message) @@ -1323,7 +1333,7 @@ async def main() -> None: great = 1 for value in [False,True]: # set verify_ssl to True then False if error occur. Verify_ssl to False if you don't use the .local hostname if great == 1: - if remove_accent(category) == 'sunscreen' or remove_accent(category) == 'rideau' or remove_accent(category) == 'shutter' or remove_accent(category) == 'volet'or remove_accent(category) == 'heater' or remove_accent(category) == 'chauffage': + if remove_accent(category) == 'sunscreen' or remove_accent(category) == 'rideau' or remove_accent(category) == 'shutter' or remove_accent(category) == 'volet'or remove_accent(category) == 'heater' or remove_accent(category) == 'chauffage' or remove_accent(category) == 'pergola': new_local_remote = 'local' if args.local: print("Tahoma has been executed with the 'local' config for the category '"+category.lower()+"'") @@ -1331,7 +1341,7 @@ async def main() -> None: session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=value)) great = 0 # print("verify_ssl="+str(value)) - overkiz_function = 'OverkizClient(username="", password="", token="'+token+'", session=session, server=OverkizServer(name="Somfy TaHoma (local)",endpoint="https://gateway-'+gateway_id+'.local:8443/enduser-mobile-web/1/enduserAPI/",manufacturer="Somfy",configuration_url=None,))' + overkiz_function = 'OverkizClient(username="", password="", token="'+token+'", session=session, verify_ssl='+str(value)+', server=OverkizServer(name="Somfy TaHoma (local)",endpoint="https://gateway-'+gateway_id+'.local:8443/enduser-mobile-web/1/enduserAPI/",manufacturer="Somfy",configuration_url=None,))' break except Exception as e: print("Error occure with verify_ssl="+str(value)+":\n"+e) From eba8676317a3b0a95c1b352b1a6b71492bc1c315 Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 09:35:08 +0200 Subject: [PATCH 2/8] fix claude.md --- CLAUDE.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..aae9e44 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,131 @@ +# tahoma + +Python CLI (`tahoma.py`) for controlling Somfy TaHoma home-automation devices +(shutters, sunscreens, pergolas, plugs, lights, alarms, heaters, sensors, +scenes) via the Overkiz API — either the cloud (`tahomalink.com` and friends) +or the TaHoma gateway's local API. + +## Repo layout + +- `tahoma.py` — the CLI entry point. Argument parsing, per-category action + dispatch (open/close/stop/my/NUMBER/on/off/arm/... depending on category), + and the actual `OverkizClient` command execution. +- `get_devices_url.py` — device discovery, invoked by `tahoma.py --getlist` + (`-g`). Logs into Overkiz, lists devices + scenarios, classifies each + device into a category by its `widget` string, and writes the per-category + files under `temp/`. +- `pyoverkiz/` — vendored Overkiz API client. Upstream: + https://github.com/iMicknl/python-overkiz-api. Treat as third-party code; + prefer not to hand-edit unless fixing something that also needs upstreaming. +- `temp/*.txt` — **all runtime state and secrets, gitignored.** Contains: + - `identifier_file.txt` — obfuscated (not strongly encrypted) cloud + username:password + - `token.txt` — local API token + - `gateway_id.txt` — gateway PIN + - `local_remote.txt` — `local` or `remote`, which API mode to prefer + - `server_choosen.txt` — which cloud server (`somfy_europe`, etc.) + - `list_of_tahoma_devices.txt` + per-category files (`shutters.txt`, + `pergolas.txt`, `plugs.txt`, ...) — the user's actual home layout/device + names + + **Never commit these, never print their full contents into commits, PR + descriptions, or logs.** `.gitignore` excludes `temp/*.txt`, `temp/*.log`, + `temp/newrelic.ini`, `__pycache__/`, `*.pyc`. + +## Local API vs cloud API + +Configured via `tahoma -c` (interactive) or `--pin`/`--token`/`--local`/ +`--remote` flags. When `temp/local_remote.txt == 'local'` and both +`gateway_id.txt` and `token.txt` are populated, both `get_devices_url.py` +(`--getlist`) and `tahoma.py`'s command dispatch try the local gateway first: + +``` +OverkizClient(username="", password="", token=, + verify_ssl=False, + server=OverkizServer( + endpoint="https://gateway-.local:8443/enduser-mobile-web/1/enduserAPI/", + ...)) +``` + +**Gotcha:** `verify_ssl=False` must be passed to the `OverkizClient` +constructor itself, not just to the `aiohttp.TCPConnector`. The gateway uses +a self-signed cert; `OverkizClient.__init__` builds its own strict SSL +context (pinned to Overkiz's public CA) whenever `verify_ssl` is left at its +default `True`, independent of whatever SSL settings the caller's session/ +connector has. Passing only `TCPConnector(verify_ssl=False)` silently does +nothing here. + +**Gotcha 2:** commands with zero parameters must be sent as +`Command(SomeOverkizCommand, [])`, not `Command(SomeOverkizCommand)` / +`parameters=None`. The local gateway API returns +`{'error': 'nil', 'errorCode': 'UNSPECIFIED_ERROR'}` for `parameters=None` +even on commands that take no arguments. + +If local API login fails, both scripts fall back to the cloud API using the +stored/`-u`/`-p` username+password. + +## Device categories are not 1:1 with hardware + +Categories (`shutter`, `sunscreen`, `plug`, `light`, `alarm`, `heater`, +`pergola`, `scene`, `sensor`, `spotalarm`) are assigned in +`get_devices_url.py` by matching substrings in the Overkiz `widget` name. +Multiple, behaviorally different widgets can land in the same category — +e.g. `pergola` covers both: + +- `BioclimaticPergola` — tilting slats, no `open`/`close`/`setClosure` + commands; uses `openSlats`/`closeSlats`/`setOrientation` instead. +- `PergolaHorizontalAwning` / `PositionableTiltedScreen` — behaves like a + sunscreen/shutter: `open`/`close`/`setClosure`. + +`tahoma.py`'s pergola action block branches on the matched device's widget +(stored as the 3rd CSV column in `temp/pergolas.txt`) to pick the right +command set. **Any category-wide behavior change must check the widget of +the specific device(s) involved** rather than assuming every device sharing +a category behaves the same — a category-wide change that isn't +widget-scoped can silently break other users' hardware even though it fixes +one case. + +## Testing changes + +There's no meaningful automated test suite (`test/test.py` is empty). +Practical workflow, cheapest/safest first: + +1. **Syntax check** — cheap sanity check before anything else: + ``` + python -c "import ast; ast.parse(open('tahoma.py',encoding='utf-8').read())" + python -c "import ast; ast.parse(open('get_devices_url.py',encoding='utf-8').read())" + ``` + +2. **Dry-test command selection logic in isolation** — no network calls, safe + any time. Copy just the branching logic you changed into a standalone + snippet, feed it every relevant widget string / action, and print what + `Command(...)` + params would result, e.g.: + ```python + from pyoverkiz.models import Command + from pyoverkiz.enums import OverkizCommand + # ...replicate the branch, call it for each widget type you care about... + ``` + This catches logic mistakes (wrong command chosen for a widget) and + protocol mistakes (e.g. the `parameters=None` vs `[]` gotcha above) before + they reach a real device. + +3. **Regression-check against the last known-good commit:** + ``` + git diff -- tahoma.py get_devices_url.py + ``` + For every changed branch, confirm the non-target-widget / "else" path is + byte-for-byte the same command+params as before. This matters because of + the shared-category-different-hardware issue above. + +4. **Read-only live checks** — safe against a real gateway, don't move + anything: + - `python tahoma.py --getlist` (rewrites `temp/*.txt` device lists) + - `python tahoma.py --list` + - `python tahoma.py -ln` / `-lnf` (list device names by category) + - `python tahoma.py -la` / `-laf` (list actions by category) + +5. **Live device commands** — physically move/trigger real hardware. Only + do this with explicit user go-ahead, one action at a time. Prefer a + no-op-ish action first when validating a new code path end to end (`stop` + or `my` before `open`/`close`), then confirm destructive actions once the + harmless one round-trips cleanly. From eb4b0bfa9db17d2b73ca15d62c40b558bbf61dcf Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 09:51:10 +0200 Subject: [PATCH 3/8] fully support parameters, local vs remote --- get_devices_url.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/get_devices_url.py b/get_devices_url.py index 56b22d3..dd78c2d 100644 --- a/get_devices_url.py +++ b/get_devices_url.py @@ -318,8 +318,21 @@ def update_file(url, content, sha, message): parser.add_argument("-s", "--server") parser.add_argument("-g", action='store_true') #store_true for not asking argument parser.add_argument("--getlist", action='store_true') #store_true for not asking argument + parser.add_argument("--token") + parser.add_argument("--pin") + parser.add_argument("--local", action='store_true') + parser.add_argument("--remote", action='store_true') args = parser.parse_args() + if args.token: + token = args.token + if args.pin: + gateway_id = args.pin + if args.local: + local_remote = "local" + if args.remote: + local_remote = "remote" + try: f = open(init_file, 'r') init = f.read() From 648047a8daccfcf991df094dc9765f82d640f447 Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 10:24:21 +0200 Subject: [PATCH 4/8] refactor to reduce code duplication. also fix cloud bug on stop --- get_devices_url.py | 120 +++++++---------------------------- tahoma.py | 152 ++++++++++++++++++-------------------------- tahoma_config.py | 153 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+), 188 deletions(-) create mode 100644 tahoma_config.py diff --git a/get_devices_url.py b/get_devices_url.py index dd78c2d..a7c641d 100644 --- a/get_devices_url.py +++ b/get_devices_url.py @@ -16,13 +16,11 @@ import re from getpass import getpass import time -from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer -from pyoverkiz.client import OverkizClient from pyoverkiz.enums import OverkizCommand from pyoverkiz.models import Command from pyoverkiz.models import Scenario import base64 -from hashlib import sha256 +import tahoma_config try: import __version__ str_newrelic='Github' @@ -33,8 +31,6 @@ async def main() -> None: - passwd_file = os.path.dirname(os.path.abspath(__file__))+'/temp/identifier_file.txt' - list_of_tahoma_devices = os.path.dirname(os.path.abspath(__file__))+'/temp/list_of_tahoma_devices.txt' list_of_tahoma_shutters = os.path.dirname(os.path.abspath(__file__))+'/temp/shutters.txt' list_of_tahoma_heaters = os.path.dirname(os.path.abspath(__file__))+'/temp/heaters.txt' @@ -48,33 +44,8 @@ async def main() -> None: list_of_tahoma_lights = os.path.dirname(os.path.abspath(__file__))+'/temp/lights.txt' list_of_tahoma_pergolas = os.path.dirname(os.path.abspath(__file__))+'/temp/pergolas.txt' - server_choosen = os.path.dirname(os.path.abspath(__file__))+'/temp/server_choosen.txt' - init_file = os.path.dirname(os.path.abspath(__file__))+'/__init__.py' - - token_file = os.path.dirname(os.path.abspath(__file__))+'/temp/token.txt' - gateway_id_file = os.path.dirname(os.path.abspath(__file__))+'/temp/gateway_id.txt' - local_remote_file = os.path.dirname(os.path.abspath(__file__))+'/temp/local_remote.txt' - - try : - f = open(local_remote_file, 'r') - local_remote = f.read().strip() - f.close() - except FileNotFoundError: - local_remote = "remote" - - try : - f = open(token_file, 'r') - token = f.read().strip() - f.close() - except FileNotFoundError: - token = "" - - try : - f = open(gateway_id_file, 'r') - gateway_id = f.read().strip() - f.close() - except FileNotFoundError: - gateway_id = "" + base_dir = os.path.dirname(os.path.abspath(__file__)) + app_config = tahoma_config.load_config(base_dir) # consent_statistics = os.path.dirname(os.path.abspath(__file__))+'/temp/consent_statistics.txt' @@ -292,13 +263,6 @@ def update_file(url, content, sha, message): print(f"Erreur: {str(e)}") - try : - f = open(server_choosen, 'r') - serverchoice = f.read() - f.close() - except : - serverchoice = "somfy_europe" - f2 = open(list_of_tahoma_devices, 'w') f3 = open(list_of_tahoma_shutters, 'w') f4 = open(list_of_tahoma_heaters, 'w') @@ -324,66 +288,27 @@ def update_file(url, content, sha, message): parser.add_argument("--remote", action='store_true') args = parser.parse_args() - if args.token: - token = args.token - if args.pin: - gateway_id = args.pin - if args.local: - local_remote = "local" - if args.remote: - local_remote = "remote" + tahoma_config.apply_cli_overrides(app_config, args) - try: - f = open(init_file, 'r') - init = f.read() - f.close() - init_str=sha256(b"init").hexdigest() - except: - init_str="None" + mode = tahoma_config.resolve_local_capability(app_config) + if mode == 'local': + ok, message = tahoma_config.check_local_credentials(app_config) + if not ok: + print(message + "Falling back to the cloud API.") + mode = 'remote' - try : - f = open(passwd_file, 'rb') - content = f.read() - f.close() - content_str = base64.b64decode(content).decode('utf-8') - if len(content_str.split(':')[0]) > 0 : - USERNAME = content_str.split(':')[0] - if len(content_str.split(':')[1]) > 0 : - PASSWORD = content_str.split(':')[1].replace(init_str, "") - except: pass - - for arg in sys.argv: - if args.password: - PASSWORD = (f'{args.password}') - if args.username: - USERNAME = (f'{args.username}') - if args.server: - serverchoice = (f'{args.server}') - - if local_remote == 'local' and gateway_id and token: - client_kwargs = { - "username": "", - "password": "", - "token": token, - "verify_ssl": False, - "server": OverkizServer( - name="Somfy TaHoma (local)", - endpoint=f"https://gateway-{gateway_id}.local:8443/enduser-mobile-web/1/enduserAPI/", - manufacturer="Somfy", - configuration_url=None, - ), - } + if mode == 'local': session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) - client = OverkizClient(**client_kwargs, session=session) + client = tahoma_config.build_client('local', app_config, session=session, verify_ssl=False) try: await client.login() except Exception as exception: # pylint: disable=broad-except await session.close() print(exception) print("\nCould not connect to the local API. Falling back to the cloud API.") - local_remote = "remote" - if local_remote != 'local' or not (gateway_id and token): - client = OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) + mode = 'remote' + if mode != 'local': + client = tahoma_config.build_client('remote', app_config) await client.__aenter__() try: await client.login() @@ -516,10 +441,11 @@ def update_file(url, content, sha, message): print( "\nIf you want to add a device you have found in this list but which is not controlled by tahoma yet, please provide info about this device from this file at \nhttps://github.com/pzim-devdata/tahoma/issues and I will update the plugin ;-). \nSonos products can't be had, use 'soco-cli' instead") print( "\nThe list of devices has been succesfully imported to the file : "+list_of_tahoma_devices+"\n" ) -try: - asyncio.run(main()) - exit(0) -except NameError as e: - print(e) - print("\nYou didn't specified any USERNAME or PASSWORD.\nExecute tahoma --config or provide a temporary USERNAME and PASSWORD by executing tahoma -u -p command") - exit(1) +if __name__ == '__main__': + try: + asyncio.run(main()) + exit(0) + except NameError as e: + print(e) + print("\nYou didn't specified any USERNAME or PASSWORD.\nExecute tahoma --config or provide a temporary USERNAME and PASSWORD by executing tahoma -u -p command") + exit(1) diff --git a/tahoma.py b/tahoma.py index 4ba0044..e2bab42 100644 --- a/tahoma.py +++ b/tahoma.py @@ -20,7 +20,6 @@ from getpass import getpass import time import datetime -from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer from pyoverkiz.client import OverkizClient, Command from pyoverkiz.enums import OverkizCommand from pyoverkiz.models import Command @@ -30,16 +29,15 @@ import requests import base64 from hashlib import sha256 +import tahoma_config try: import __version__ if __version__: - get_devices_url = "import get_devices_url" version = 'tahoma - portable Version '+ str(__version__.__version__)+' - by @pzim-devdata' except ImportError: from tahoma import __version__ if __version__: - get_devices_url = "from tahoma import get_devices_url" version = 'tahoma - Pypi version '+ str(__version__.__version__)+' - by @pzim-devdata' version_number=str(__version__.__version__) @@ -144,26 +142,13 @@ def main(): list_actions = ['[open,close,stop,my,NUMBER]','[on,off,toggle]','[on,off,toggle]','[on,off,toggle]','[arm,disarm,partial,arm_night,arm_away]','[comfort,comfort-1,comfort-2,eco,frostprotection,off,standby,manual,auto,prog,NUMBER]','[open,close,stop,my,NUMBER]','[open,close,stop,my,NUMBER]','[on,activate,launch,execute]','[get,get_state,get_position,get_lumens,get_temperature]'] list_actions_french = ['[ouvrir,fermer,stop,my,NOMBRE]','[allumer,eteindre,basculer]','[allumer,eteindre,basculer]','[allumer,eteindre,basculer]','[activer,desactiver,partiel,activer_nuit,activer_parti]','[confort,confort-1,confort-2,eco,horsgel,eteindre,veille,manuel,auto,prog,NOMBRE]','[ouvrir,fermer,stop,my,NOMBRE]','[ouvrir,fermer,stop,my,NOMBRE]','[lancer,activer,executer]','[obtenir,etat,position,luminosite,temperature]'] - try : - f = open(token_file, 'r') - token = f.read() - f.close() - except FileNotFoundError: - token = "" - - try : - f = open(gateway_id_file, 'r') - gateway_id = f.read() - f.close() - except FileNotFoundError: - gateway_id = "" - - try : - f = open(local_remote_file, 'r') - local_remote = f.read() - f.close() - except FileNotFoundError: - local_remote = "remote" + app_config = tahoma_config.load_config(os.path.dirname(os.path.abspath(__file__))) + token = app_config.token + gateway_id = app_config.gateway_id + local_remote = app_config.local_remote + serverchoice = app_config.server + USERNAME = app_config.username + PASSWORD = app_config.password try : f = open(notification_consent, 'r') @@ -180,13 +165,6 @@ def main(): except: init_str="None" - try : - f = open(server_choosen, 'r') - serverchoice = f.read() - f.close() - except FileNotFoundError: - serverchoice = "somfy_europe" - try : f = open(logs_consent, 'r') logs = f.read() @@ -277,12 +255,19 @@ def info(): for arg in sys.argv : if arg == '-g' or arg == '--getlist' : + try: + import get_devices_url + except ImportError: + from tahoma import get_devices_url try : - exec(get_devices_url) + asyncio.run(get_devices_url.main()) exit() - except Exception as e: + except NameError as e: + print(e) + print("\nYou didn't specified any USERNAME or PASSWORD.\nExecute tahoma --config or provide a temporary USERNAME and PASSWORD by executing tahoma -u -p command") + exit() + except Exception as e: print(e) -# exec((open(os.path.dirname(os.path.abspath(__file__))+"/get_devices_url.py")).read()) exit() for arg in sys.argv : @@ -371,7 +356,7 @@ def info(): print( "\nDo you want to encrypt your login in "+passwd_file+"? \nIf not, the file will be erased:\n(Y/n)") CONSENT = input() if CONSENT.lower() == 'y'or CONSENT.lower() == 'yes': - f = open(passwd_file, 'ab') + f = open(passwd_file, 'wb') f.write(base64.b64encode(str(USERNAME+":"+PASSWORD+init_str).encode('utf-8'))) f.close() print( "Your logins are encrypted in "+passwd_file ) @@ -530,16 +515,6 @@ def info(): except:pass info() exit() - try : - f = open(passwd_file, 'rb') - content = f.read() - f.close() - content_str = base64.b64decode(content).decode('utf-8') - if len(content_str.split(':')[0]) > 0 : - USERNAME = content_str.split(':')[0] - if len(content_str.split(':')[1]) > 0 : - PASSWORD = content_str.split(':')[1].replace(init_str, "") - except: pass parser = argparse.ArgumentParser() parser.add_argument("-u", "--username") @@ -563,30 +538,19 @@ def info(): args = parser.parse_args() # print(f'Input action(s) : {args.action} {args.category} {args.name} '+' '.join(args.suite) ) - if args.password: - PASSWORD = (f'{args.password}') - print("Your PASSWORD has been taken into account") - if args.username: - USERNAME = (f'{args.username}') - print("Your USERNAME has been taken into account") - - if args.token: - token = (f'{args.token}') - print("Your token: "+token+" has been taken into account") - if args.pin: - gateway_id = (f'{args.pin}') - print("Your gateway pin code: "+gateway_id+" has been taken into account") - - if args.local: - local_remote = "local" -# print("Will use tahoma with the 'local' config") - if args.remote: - local_remote = "remote" - print("Will use tahoma with the 'remote' config") - - if args.server: - serverchoice = (f'{args.server}') - print("The server: "+serverchoice+" has been taken into account") + app_config.username = USERNAME + app_config.password = PASSWORD + app_config.server = serverchoice + app_config.token = token + app_config.gateway_id = gateway_id + app_config.local_remote = local_remote + tahoma_config.apply_cli_overrides(app_config, args) + USERNAME = app_config.username + PASSWORD = app_config.password + serverchoice = app_config.server + token = app_config.token + gateway_id = app_config.gateway_id + local_remote = app_config.local_remote if args.notification: notification = "y" @@ -665,7 +629,7 @@ def remove_accent(old): success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' ACTION is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, [0]) + fonction = Command(OverkizCommand.STOP, []) success = 0 elif remove_accent(action).upper() == 'MY' : fonction = Command(OverkizCommand.MY, [0]) @@ -734,7 +698,7 @@ def remove_accent(old): success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' function is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, [0]) + fonction = Command(OverkizCommand.STOP, []) success = 0 elif remove_accent(action).upper() == 'MY' : fonction = Command(OverkizCommand.MY, [0]) @@ -809,7 +773,7 @@ def remove_accent(old): success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' function is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, [0]) + fonction = Command(OverkizCommand.STOP, []) success = 0 elif remove_accent(action).upper() == 'MY' : fonction = Command(OverkizCommand.MY, [0]) @@ -904,7 +868,7 @@ def remove_accent(old): f.close() if str1.join(good_name).upper()+"," in content.upper(): async def state() -> None: - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: + async with tahoma_config.build_client('remote', app_config) as client: await client.login() # get_state = await client.get_state(str(url[0])) get_state = await asyncio.wait_for( client.get_state(str(url[0])), timeout=10.0) @@ -1271,7 +1235,7 @@ async def state() -> None: ##########################GENERATE NEW TOKEN AND GATEWAY.ID FOR 'LOCAL' CONFIG async def get_token_or_gateway_id() -> None: - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: + async with tahoma_config.build_client('remote', app_config) as client: await client.login() gateways = await client.get_gateways() gateway_id_list = [] @@ -1329,34 +1293,40 @@ async def get_token_or_gateway_id() -> None: try: async def main() -> None: + app_config.token = token + app_config.gateway_id = gateway_id + app_config.local_remote = local_remote + app_config.username = USERNAME + app_config.password = PASSWORD + app_config.server = serverchoice if local_remote == 'local': great = 1 for value in [False,True]: # set verify_ssl to True then False if error occur. Verify_ssl to False if you don't use the .local hostname if great == 1: - if remove_accent(category) == 'sunscreen' or remove_accent(category) == 'rideau' or remove_accent(category) == 'shutter' or remove_accent(category) == 'volet'or remove_accent(category) == 'heater' or remove_accent(category) == 'chauffage' or remove_accent(category) == 'pergola': - new_local_remote = 'local' + new_local_remote = tahoma_config.resolve_local_capability( + app_config, + category=remove_accent(category), + action=remove_accent(action), + local_capable_categories={'sunscreen','rideau','shutter','volet','heater','chauffage','pergola'}, + ) + if new_local_remote == 'local': if args.local: print("Tahoma has been executed with the 'local' config for the category '"+category.lower()+"'") + if not (token and gateway_id): + print("Local API selected for category '"+category.lower()+"' but no token/gateway_id configured yet. Tahoma will try to auto-provision one; run 'tahoma -c' or pass --token/--pin to avoid this.") try: session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=value)) great = 0 -# print("verify_ssl="+str(value)) - overkiz_function = 'OverkizClient(username="", password="", token="'+token+'", session=session, verify_ssl='+str(value)+', server=OverkizServer(name="Somfy TaHoma (local)",endpoint="https://gateway-'+gateway_id+'.local:8443/enduser-mobile-web/1/enduserAPI/",manufacturer="Somfy",configuration_url=None,))' + client_factory = lambda value=value, session=session: tahoma_config.build_client('local', app_config, session=session, verify_ssl=value) break except Exception as e: print("Error occure with verify_ssl="+str(value)+":\n"+e) else: - new_local_remote = 'remote' - great =0 - if remove_accent(action) != 'wait' and remove_accent(action) != 'attendre': - if remove_accent(action) != 'cancel' and remove_accent(action) != 'annuler': - print("Tahoma has been executed with the 'global' config because the '"+category.lower()+"' category is not yet supported for local use") - else: - print("\nCan't perform a CANCEL action when using a local API of tahoma: \nRun tahoma with the '--remote' argument.\n") - overkiz_function = 'OverkizClient(username="'+str(USERNAME)+'", password="'+str(PASSWORD)+'", server=SUPPORTED_SERVERS["'+str(serverchoice)+'"])' + great = 0 + client_factory = lambda: tahoma_config.build_client('remote', app_config) else: new_local_remote = 'remote' - overkiz_function = 'OverkizClient(username="'+str(USERNAME)+'", password="'+str(PASSWORD)+'", server=SUPPORTED_SERVERS["'+str(serverchoice)+'"])' + client_factory = lambda: tahoma_config.build_client('remote', app_config) try : j=0 for device_url in url : @@ -1393,8 +1363,8 @@ async def main() -> None: else : if remove_accent(category) == 'scene' or remove_accent(category) == 'scenario': try : - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: -# async with eval(overkiz_function) as client: + async with tahoma_config.build_client('remote', app_config) as client: +# async with client_factory() as client: await client.login() exec_id = await client.execute_scenario(device_url) # error = 0 @@ -1415,7 +1385,7 @@ async def main() -> None: except : pass elif remove_accent(category) == 'sensor' or remove_accent(category) == 'capteur': try: - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: + async with tahoma_config.build_client('remote', app_config) as client: await client.login() # get_state = await client.get_state(device_url) get_state = await asyncio.wait_for( client.get_state( device_url ), timeout=10.0) @@ -1433,7 +1403,7 @@ async def main() -> None: except :pass elif remove_accent(action).upper() == 'CANCEL' or remove_accent(action).upper() == 'ANNULER': try: - async with OverkizClient(USERNAME, PASSWORD, SUPPORTED_SERVERS[serverchoice]) as client: + async with tahoma_config.build_client('remote', app_config) as client: await client.login() executions = await client.get_current_executions() for execution in executions: @@ -1455,7 +1425,7 @@ async def main() -> None: else : try : if token and gateway_id and local_remote == 'local' or token and gateway_id and new_local_remote == 'local' or local_remote == 'remote' or new_local_remote == 'remote': - async with eval(overkiz_function) as client: + async with client_factory() as client: await client.login() # print("Local API connection succesfull!") # print(str(fonction)) diff --git a/tahoma_config.py b/tahoma_config.py new file mode 100644 index 0000000..9375c0f --- /dev/null +++ b/tahoma_config.py @@ -0,0 +1,153 @@ +"""Shared config loading, CLI-override, and local/remote API-mode resolution +for tahoma.py and get_devices_url.py. See CLAUDE.md for the local-vs-cloud +API background and the two OverkizClient gotchas (verify_ssl constructor arg, +parameters=[] for zero-arg commands). +""" + +import base64 +import os +from dataclasses import dataclass +from hashlib import sha256 + +from pyoverkiz.client import OverkizClient +from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer + + +@dataclass +class Config: + username: str = "" + password: str = "" + server: str = "somfy_europe" + token: str = "" + gateway_id: str = "" + local_remote: str = "remote" + + +def _read_file(path, default=""): + try: + with open(path, 'r') as f: + return f.read().strip() + except FileNotFoundError: + return default + + +def load_config(base_dir): + """Load persisted config from temp/*.txt. Written only by `tahoma -c`.""" + temp = os.path.join(base_dir, 'temp') + config = Config() + config.token = _read_file(os.path.join(temp, 'token.txt')) + config.gateway_id = _read_file(os.path.join(temp, 'gateway_id.txt')) + config.local_remote = _read_file(os.path.join(temp, 'local_remote.txt'), "remote") + config.server = _read_file(os.path.join(temp, 'server_choosen.txt'), "somfy_europe") + + init_file = os.path.join(base_dir, '__init__.py') + try: + with open(init_file, 'r'): + pass + init_str = sha256(b"init").hexdigest() + except FileNotFoundError: + init_str = "None" + + passwd_file = os.path.join(temp, 'identifier_file.txt') + try: + with open(passwd_file, 'rb') as f: + content_str = base64.b64decode(f.read()).decode('utf-8') + parts = content_str.split(':') + if len(parts[0]) > 0: + config.username = parts[0] + if len(parts) > 1 and len(parts[1]) > 0: + config.password = parts[1].replace(init_str, "") + except FileNotFoundError: + pass + + return config + + +def apply_cli_overrides(config, args): + """Apply --username/--password/--server/--token/--pin/--local/--remote + on top of the loaded config, in-memory only (never rewrites temp/*.txt). + """ + if getattr(args, 'username', None): + config.username = args.username + print("Your USERNAME has been taken into account") + if getattr(args, 'password', None): + config.password = args.password + print("Your PASSWORD has been taken into account") + if getattr(args, 'server', None): + config.server = args.server + print("The server: " + config.server + " has been taken into account") + if getattr(args, 'token', None): + config.token = args.token + print("Your token: " + config.token + " has been taken into account") + if getattr(args, 'pin', None): + config.gateway_id = args.pin + print("Your gateway pin code: " + config.gateway_id + " has been taken into account") + if getattr(args, 'local', False): + config.local_remote = "local" + print("Will use tahoma with the 'local' config") + if getattr(args, 'remote', False): + config.local_remote = "remote" + print("Will use tahoma with the 'remote' config") + return config + + +def resolve_local_capability(config, category=None, action=None, local_capable_categories=None): + """Decide 'local' vs 'remote' based purely on config.local_remote and + whether `category` is supported locally. Does NOT check whether + token/gateway_id are actually present - see check_local_credentials for + that, kept separate so callers can still attempt auto-provisioning + (tahoma.py's get_token_or_gateway_id flow) instead of a hard fallback. + Always prints why, when downgrading from local to remote. Returns mode. + """ + if config.local_remote != 'local': + return 'remote' + + if local_capable_categories is not None and category is not None \ + and category not in local_capable_categories: + if action not in ('wait', 'attendre'): + if action not in ('cancel', 'annuler'): + print("Tahoma has been executed with the 'global' config because the '" + + category.lower() + "' category is not yet supported for local use") + else: + print("\nCan't perform a CANCEL action when using a local API of tahoma: " + "\nRun tahoma with the '--remote' argument.\n") + return 'remote' + + return 'local' + + +def check_local_credentials(config): + """Explicit check for whether local mode actually has what it needs. + Returns (ok, message). message is set whenever ok is False, so callers + can always surface *why* local isn't usable instead of failing silently. + """ + missing = [] + if not config.token: + missing.append('token') + if not config.gateway_id: + missing.append('gateway_id (pin)') + if missing: + return False, ( + "Local API selected but " + " and ".join(missing) + " not configured. " + "Run 'tahoma -c' or pass --token/--pin. " + ) + return True, "" + + +def build_client(mode, config, session=None, verify_ssl=False): + """Construct a real OverkizClient directly - no eval().""" + if mode == 'local': + return OverkizClient( + username="", + password="", + token=config.token, + verify_ssl=verify_ssl, + session=session, + server=OverkizServer( + name="Somfy TaHoma (local)", + endpoint=f"https://gateway-{config.gateway_id}.local:8443/enduser-mobile-web/1/enduserAPI/", + manufacturer="Somfy", + configuration_url=None, + ), + ) + return OverkizClient(config.username, config.password, SUPPORTED_SERVERS[config.server]) From 9b8bd586ec7ed7556b13c67c1135f9939fb78adf Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 11:00:12 +0200 Subject: [PATCH 5/8] Replace vendored pyoverkiz with real pyoverkiz==2.1.0 dependency Deletes the hand-copied pyoverkiz/ directory (was shadowing the real pip dependency already declared in requirements.txt) and migrates all call sites to the current published API: - OverkizClient now takes keyword-only credentials=/server= built from UsernamePasswordCredentials/LocalTokenCredentials and ServerConfig - Command is attrs kw_only=True, so all constructions need name=/parameters= - execute_command -> execute_action_group(actions=[Action(...)]) - execute_scenario -> execute_persisted_action_group - cancel_command -> cancel_execution - get_scenarios -> get_action_groups, Scenario -> PersistedActionGroup - Device.id -> Device.device_url, Gateway.id -> Gateway.gateway_id - NotAuthenticatedException -> NotAuthenticatedError Also pins pyoverkiz==2.1.0 in both requirements files and adds .github/dependabot.yml (pip, weekly) to track future releases. Co-Authored-By: Claude Sonnet 5 --- .github/dependabot.yml | 6 + get_devices_url.py | 54 +- pyoverkiz/__init__.py | 0 pyoverkiz/client.py | 1046 ----------------- pyoverkiz/const.py | 113 -- pyoverkiz/enums/__init__.py | 11 - .../enums/__pycache__/__init__.cpython-39.pyc | Bin 345 -> 0 bytes .../enums/__pycache__/command.cpython-39.pyc | Bin 9645 -> 0 bytes .../__pycache__/execution.cpython-39.pyc | Bin 2287 -> 0 bytes .../enums/__pycache__/gateway.cpython-39.pyc | Bin 3576 -> 0 bytes .../enums/__pycache__/general.cpython-39.pyc | Bin 18573 -> 0 bytes .../measured_value_type.cpython-39.pyc | Bin 2566 -> 0 bytes .../enums/__pycache__/protocol.cpython-39.pyc | Bin 1528 -> 0 bytes .../enums/__pycache__/server.cpython-39.pyc | Bin 1155 -> 0 bytes .../enums/__pycache__/state.cpython-39.pyc | Bin 14529 -> 0 bytes pyoverkiz/enums/__pycache__/ui.cpython-39.pyc | Bin 18334 -> 0 bytes pyoverkiz/enums/command.py | 281 ----- pyoverkiz/enums/execution.py | 70 -- pyoverkiz/enums/gateway.py | 116 -- pyoverkiz/enums/general.py | 435 ------- pyoverkiz/enums/measured_value_type.py | 51 - pyoverkiz/enums/protocol.py | 55 - pyoverkiz/enums/server.py | 34 - pyoverkiz/enums/state.py | 275 ----- pyoverkiz/enums/ui.py | 424 ------- pyoverkiz/exceptions.py | 109 -- pyoverkiz/models.py | 831 ------------- pyoverkiz/obfuscate.py | 74 -- pyoverkiz/overkiz-root-ca-2048.crt | 20 - pyoverkiz/py.typed | 0 pyoverkiz/types.py | 20 - pyoverkiz/utils.py | 26 - requirements.txt | 2 +- requirements_tahoma-gpt.txt | 2 +- tahoma.py | 99 +- tahoma_config.py | 19 +- 36 files changed, 96 insertions(+), 4077 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 pyoverkiz/__init__.py delete mode 100644 pyoverkiz/client.py delete mode 100644 pyoverkiz/const.py delete mode 100644 pyoverkiz/enums/__init__.py delete mode 100644 pyoverkiz/enums/__pycache__/__init__.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/command.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/execution.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/gateway.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/general.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/measured_value_type.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/protocol.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/server.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/state.cpython-39.pyc delete mode 100644 pyoverkiz/enums/__pycache__/ui.cpython-39.pyc delete mode 100644 pyoverkiz/enums/command.py delete mode 100644 pyoverkiz/enums/execution.py delete mode 100644 pyoverkiz/enums/gateway.py delete mode 100644 pyoverkiz/enums/general.py delete mode 100644 pyoverkiz/enums/measured_value_type.py delete mode 100644 pyoverkiz/enums/protocol.py delete mode 100644 pyoverkiz/enums/server.py delete mode 100644 pyoverkiz/enums/state.py delete mode 100644 pyoverkiz/enums/ui.py delete mode 100644 pyoverkiz/exceptions.py delete mode 100644 pyoverkiz/models.py delete mode 100644 pyoverkiz/obfuscate.py delete mode 100644 pyoverkiz/overkiz-root-ca-2048.crt delete mode 100644 pyoverkiz/py.typed delete mode 100644 pyoverkiz/types.py delete mode 100644 pyoverkiz/utils.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6a7695c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/get_devices_url.py b/get_devices_url.py index a7c641d..0cdaa01 100644 --- a/get_devices_url.py +++ b/get_devices_url.py @@ -18,7 +18,7 @@ import time from pyoverkiz.enums import OverkizCommand from pyoverkiz.models import Command -from pyoverkiz.models import Scenario +from pyoverkiz.models import PersistedActionGroup import base64 import tahoma_config try: @@ -319,74 +319,74 @@ def update_file(url, content, sha, message): try: devices = await client.get_devices() - scenarios = await client.get_scenarios() + scenarios = await client.get_action_groups() try : f2.write(f"Devices :\n") for device in devices: - print("\n"+device.label+","+device.id+","+device.widget+","+device.ui_class+","+device.controllable_name+"") - print(f"{device.label},{device.id},{device.widget},{device.ui_class},{device.controllable_name}") - f2.write("\n"+device.label+","+device.id+","+device.widget+","+device.ui_class+","+device.controllable_name+"\n") - f2.write(f"{device.label},{device.id},{device.widget},{device.ui_class},{device.controllable_name}\n") + print("\n"+device.label+","+device.device_url+","+device.widget+","+device.ui_class+","+device.controllable_name+"") + print(f"{device.label},{device.device_url},{device.widget},{device.ui_class},{device.controllable_name}") + f2.write("\n"+device.label+","+device.device_url+","+device.widget+","+device.ui_class+","+device.controllable_name+"\n") + f2.write(f"{device.label},{device.device_url},{device.widget},{device.ui_class},{device.controllable_name}\n") if "Shutter" in device.widget or "PositionableTiltedScreen" in device.widget: - f3.write(device.label+","+device.id+","+device.widget+"\n") + f3.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_shutters) elif "Heater" in device.widget : - f4.write(device.label+","+device.id+","+device.widget+"\n") + f4.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_heaters) elif "MyFoxAlarm" in device.widget : - f5.write(device.label+","+device.id+","+device.widget+"\n") + f5.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_alarms) elif "StatefulOnOffLight" in device.widget : - f6.write(device.label+","+device.id+","+device.widget+"\n") + f6.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_spotalarms) elif "StatelessOnOff" in device.widget : - f7.write(device.label+","+device.id+","+device.widget+"\n") + f7.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_plugs) elif "StatefulOnOff" in device.widget : - f7.write(device.label+","+device.id+","+device.widget+"\n") + f7.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_plugs) elif "PositionableScreen" in device.widget or "PositionableHorizontalAwning" in device.widget: - f8.write(device.label+","+device.id+","+device.widget+"\n") + f8.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_sunscreens) elif "Sensor" in device.widget: - f10.write(device.label+","+device.id+","+device.widget+"\n") + f10.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_sensors) elif "Light" in device.widget: - f12.write(device.label+","+device.id+","+device.widget+"\n") + f12.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_lights) elif "PositionableTiltedScreen" in device.widget or "PergolaHorizontalAwning" in device.widget or "BioclimaticPergola" in device.widget: - f13.write(device.label+","+device.id+","+device.widget+"\n") + f13.write(device.label+","+device.device_url+","+device.widget+"\n") print( "Device "+device.label+" controled by tahoma. Added to : "+list_of_tahoma_pergolas) else : print( "Device '"+device.label+"' NOT controlled by tahoma yet") -# get_state = await client.get_state( device.id ) - get_state = await asyncio.wait_for( client.get_state( device.id ), timeout=10.0) +# get_state = await client.get_state( device.device_url ) + get_state = await asyncio.wait_for( client.get_state( device.device_url ), timeout=10.0) i=0 for i in range(len(get_state)) : # print(i) # print(str(get_state[i].value)) if "closed" in str(get_state[i].value) or "open" in str(get_state[i].value) : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['closed','open'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['closed','open'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) if "StatefulOnOff" == device.widget or "StatefulOnOffLight" == device.widget : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) if "armed" in str(get_state[i].value) or "disarmed" in str(get_state[i].value) : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['armed','disarmed'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['armed','disarmed'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) if "Heater" not in device.widget and 'DomesticHotWaterTank' not in device.widget and "StatefulOnOff" not in device.widget and "StatefulOnOffLight" not in device.widget and 'TSKAlarmController' not in device.widget: if str(get_state[i].value) == 'on' or str(get_state[i].value) =='off': - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) if 'io:TargetHeatingLevelState' not in str(get_state[i].name) and 'io:LastTargetHeatingLevelState' not in str(get_state[i].name) and 'core:OnOffState' not in str(get_state[i].name): if 'TSKAlarmController' not in device.widget and 'DomesticHotWaterTank' not in device.widget: if str(get_state[i].value) == 'eco' or str(get_state[i].value) =='comfort' or str(get_state[i].value) =='frostprotection' or str(get_state[i].value) =='off': - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['eco','comfort','frostprotection','off'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['eco','comfort','frostprotection','off'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) if 'TemperatureSensor' == device.widget : try: if str(get_state[i].name) == 'core:TemperatureState': - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['°C'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['°C'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) except : pass if 'LuminanceSensor' == device.widget : @@ -394,19 +394,19 @@ def update_file(url, content, sha, message): try: if str(get_state[i].name) == 'core:LuminanceState' : #if str(int(get_state[i].value)).isnumeric() == True : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['Lumens'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['Lumens'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) except : pass if 'CumulativeElectricPowerConsumptionSensor' == device.widget : try: if str(get_state[i].name) == 'core:ElectricEnergyConsumptionState' : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['kWh'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['kWh'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) except : pass if 'DomesticHotWaterTank' == device.widget : try: if str(get_state[i].name) == 'io:ForceHeatingState' : - f11.write(device.label+","+device.id+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") + f11.write(device.label+","+device.device_url+","+device.widget+",get_state["+str(i)+"].value,['on','off'],"+str(get_state[i].value)+"\n") print("States for "+device.label+" added to : "+ list_of_tahoma_states) except : pass diff --git a/pyoverkiz/__init__.py b/pyoverkiz/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyoverkiz/client.py b/pyoverkiz/client.py deleted file mode 100644 index 8e62338..0000000 --- a/pyoverkiz/client.py +++ /dev/null @@ -1,1046 +0,0 @@ -""" Python wrapper for the OverKiz API """ -from __future__ import annotations - -import asyncio -import datetime -import os -import ssl -import urllib.parse -from collections.abc import Mapping -from json import JSONDecodeError -from types import TracebackType -from typing import Any, cast - -import backoff -import boto3 -import humps -from aiohttp import ( - ClientConnectorError, - ClientResponse, - ClientSession, - FormData, - ServerDisconnectedError, -) -from botocore.config import Config -from warrant_lite import WarrantLite - -from pyoverkiz.const import ( - COZYTOUCH_ATLANTIC_API, - COZYTOUCH_CLIENT_ID, - LOCAL_API_PATH, - NEXITY_API, - NEXITY_COGNITO_CLIENT_ID, - NEXITY_COGNITO_REGION, - NEXITY_COGNITO_USER_POOL, - SOMFY_API, - SOMFY_CLIENT_ID, - SOMFY_CLIENT_SECRET, - SUPPORTED_SERVERS, -) -from pyoverkiz.enums import APIType, Server -from pyoverkiz.exceptions import ( - AccessDeniedToGatewayException, - BadCredentialsException, - CozyTouchBadCredentialsException, - CozyTouchServiceException, - ExecutionQueueFullException, - InvalidCommandException, - InvalidEventListenerIdException, - InvalidTokenException, - MaintenanceException, - MissingAuthorizationTokenException, - NexityBadCredentialsException, - NexityServiceException, - NoRegisteredEventListenerException, - NotAuthenticatedException, - NotSuchTokenException, - OverkizException, - ServiceUnavailableException, - SessionAndBearerInSameRequestException, - SomfyBadCredentialsException, - SomfyServiceException, - TooManyAttemptsBannedException, - TooManyConcurrentRequestsException, - TooManyExecutionsException, - TooManyRequestsException, - UnknownObjectException, - UnknownUserException, -) -from pyoverkiz.models import ( - Command, - Device, - Event, - Execution, - Gateway, - HistoryExecution, - LocalToken, - Option, - OptionParameter, - OverkizServer, - Place, - Scenario, - Setup, - State, -) -from pyoverkiz.obfuscate import obfuscate_sensitive_data -from pyoverkiz.types import JSON - - -async def relogin(invocation: Mapping[str, Any]) -> None: - await invocation["args"][0].login() - - -async def refresh_listener(invocation: Mapping[str, Any]) -> None: - await invocation["args"][0].register_event_listener() - - -# pylint: disable=too-many-instance-attributes, too-many-branches - - -class OverkizClient: - """Interface class for the Overkiz API""" - - username: str - password: str - server: OverkizServer - setup: Setup | None - devices: list[Device] - gateways: list[Gateway] - event_listener_id: str | None - session: ClientSession - api_type: APIType - - _refresh_token: str | None = None - _expires_in: datetime.datetime | None = None - _access_token: str | None = None - _ssl_context: ssl.SSLContext | None = None - - def __init__( - self, - username: str, - password: str, - server: OverkizServer, - token: str | None = None, - session: ClientSession | None = None, - verify_ssl: bool | None = True, - ) -> None: - """ - Constructor - - :param username: the username - :param password: the password - :param server: OverkizServer - :param session: optional ClientSession - """ - - self.username = username - self.password = password - self.server = server - self._access_token = token - - self.setup: Setup | None = None - self.devices: list[Device] = [] - self.gateways: list[Gateway] = [] - self.event_listener_id: str | None = None - - self.session = session if session else ClientSession() - - if LOCAL_API_PATH in self.server.endpoint: - self.api_type = APIType.LOCAL - - if verify_ssl: - # To avoid security issues, we add the following authority to - # our HTTPS client trust store: https://ca.overkiz.com/overkiz-root-ca-2048.crt - self._ssl_context = ssl.create_default_context( - cafile=os.path.dirname(os.path.realpath(__file__)) - + "/overkiz-root-ca-2048.crt" - ) - else: - self.api_type = APIType.CLOUD - - async def __aenter__(self) -> OverkizClient: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.close() - - async def close(self) -> None: - """Close the session.""" - if self.event_listener_id: - await self.unregister_event_listener() - - await self.session.close() - - async def login( - self, - register_event_listener: bool | None = True, - ) -> bool: - """ - Authenticate and create an API session allowing access to the other operations. - Caller must provide one of [userId+userPassword, userId+ssoToken, accessToken, jwt] - """ - # Local authentication - if self.api_type == APIType.LOCAL: - if register_event_listener: - await self.register_event_listener() - else: - # Call a simple endpoint to verify if our token is correct - # Since local API does not have a /login endpoint - await self.get_gateways() - - return True - - # Somfy TaHoma authentication using access_token - if self.server == SUPPORTED_SERVERS[Server.SOMFY_EUROPE]: - await self.somfy_tahoma_get_access_token() - - if register_event_listener: - await self.register_event_listener() - - return True - - # CozyTouch authentication using jwt - if self.server == SUPPORTED_SERVERS[Server.ATLANTIC_COZYTOUCH]: - jwt = await self.cozytouch_login() - payload = {"jwt": jwt} - - # Nexity authentication using ssoToken - elif self.server == SUPPORTED_SERVERS[Server.NEXITY]: - sso_token = await self.nexity_login() - user_id = self.username.replace("@", "_-_") # Replace @ for _-_ - payload = {"ssoToken": sso_token, "userId": user_id} - - # Regular authentication using userId+userPassword - else: - payload = {"userId": self.username, "userPassword": self.password} - - response = await self.__post("login", data=payload) - - if response.get("success"): - if register_event_listener: - await self.register_event_listener() - return True - - return False - - async def somfy_tahoma_get_access_token(self) -> str: - """ - Authenticate via Somfy identity and acquire access_token. - """ - # Request access token - async with self.session.post( - SOMFY_API + "/oauth/oauth/v2/token", - data=FormData( - { - "grant_type": "password", - "username": self.username, - "password": self.password, - "client_id": SOMFY_CLIENT_ID, - "client_secret": SOMFY_CLIENT_SECRET, - } - ), - headers={ - "Content-Type": "application/x-www-form-urlencoded", - }, - ) as response: - token = await response.json() - - # { "message": "error.invalid.grant", "data": [], "uid": "xxx" } - if "message" in token and token["message"] == "error.invalid.grant": - raise SomfyBadCredentialsException(token["message"]) - - if "access_token" not in token: - raise SomfyServiceException("No Somfy access token provided.") - - self._access_token = cast(str, token["access_token"]) - self._refresh_token = token["refresh_token"] - self._expires_in = datetime.datetime.now() + datetime.timedelta( - seconds=token["expires_in"] - 5 - ) - - return self._access_token - - async def refresh_token(self) -> None: - """ - Update the access and the refresh token. The refresh token will be valid 14 days. - """ - if self.server != SUPPORTED_SERVERS[Server.SOMFY_EUROPE]: - return - - if not self._refresh_token: - raise ValueError("No refresh token provided. Login method must be used.") - - # &grant_type=refresh_token&refresh_token=REFRESH_TOKEN - # Request access token - async with self.session.post( - SOMFY_API + "/oauth/oauth/v2/token", - data=FormData( - { - "grant_type": "refresh_token", - "refresh_token": self._refresh_token, - "client_id": SOMFY_CLIENT_ID, - "client_secret": SOMFY_CLIENT_SECRET, - } - ), - headers={ - "Content-Type": "application/x-www-form-urlencoded", - }, - ) as response: - token = await response.json() - # { "message": "error.invalid.grant", "data": [], "uid": "xxx" } - if "message" in token and token["message"] == "error.invalid.grant": - raise SomfyBadCredentialsException(token["message"]) - - if "access_token" not in token: - raise SomfyServiceException("No Somfy access token provided.") - - self._access_token = cast(str, token["access_token"]) - self._refresh_token = token["refresh_token"] - self._expires_in = datetime.datetime.now() + datetime.timedelta( - seconds=token["expires_in"] - 5 - ) - - async def cozytouch_login(self) -> str: - """ - Authenticate via CozyTouch identity and acquire JWT token. - """ - # Request access token - async with self.session.post( - COZYTOUCH_ATLANTIC_API + "/token", - data=FormData( - { - "grant_type": "password", - "username": "GA-PRIVATEPERSON/" + self.username, - "password": self.password, - } - ), - headers={ - "Authorization": f"Basic {COZYTOUCH_CLIENT_ID}", - "Content-Type": "application/x-www-form-urlencoded", - }, - ) as response: - token = await response.json() - - # {'error': 'invalid_grant', - # 'error_description': 'Provided Authorization Grant is invalid.'} - if "error" in token and token["error"] == "invalid_grant": - raise CozyTouchBadCredentialsException(token["error_description"]) - - if "token_type" not in token: - raise CozyTouchServiceException("No CozyTouch token provided.") - - # Request JWT - async with self.session.get( - COZYTOUCH_ATLANTIC_API + "/magellan/accounts/jwt", - headers={"Authorization": f"Bearer {token['access_token']}"}, - ) as response: - jwt = await response.text() - - if not jwt: - raise CozyTouchServiceException("No JWT token provided.") - - jwt = jwt.strip('"') # Remove surrounding quotes - - return jwt - - async def nexity_login(self) -> str: - """ - Authenticate via Nexity identity and acquire SSO token. - """ - loop = asyncio.get_event_loop() - - def _get_client() -> boto3.session.Session.client: - return boto3.client( - "cognito-idp", config=Config(region_name=NEXITY_COGNITO_REGION) - ) - - # Request access token - client = await loop.run_in_executor(None, _get_client) - - aws = WarrantLite( - username=self.username, - password=self.password, - pool_id=NEXITY_COGNITO_USER_POOL, - client_id=NEXITY_COGNITO_CLIENT_ID, - client=client, - ) - - try: - tokens = await loop.run_in_executor(None, aws.authenticate_user) - except Exception as error: - raise NexityBadCredentialsException() from error - - id_token = tokens["AuthenticationResult"]["IdToken"] - - async with self.session.get( - NEXITY_API + "/deploy/api/v1/domotic/token", - headers={ - "Authorization": id_token, - }, - ) as response: - token = await response.json() - - if "token" not in token: - raise NexityServiceException("No Nexity SSO token provided.") - - return cast(str, token["token"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_setup(self, refresh: bool = False) -> Setup: - """ - Get all data about the connected user setup - -> gateways data (serial number, activation state, ...): - -> setup location: - -> house places (rooms and floors): - -> setup devices: - - A gateway may be in different modes (mode) regarding to the activated functions (functions). - A house may be composed of several floors and rooms. The house, floors and rooms are viewed as a place. - Devices in the house are grouped by type called uiClass. Each device has an associated widget. - The widget is used to control or to know the device state, whatever the device protocol (controllable): IO, RTS, X10, ... . - A device can be either an actuator (type=1) or a sensor (type=2). - Data of one or several devices can be also get by setting the device(s) url as request parameter. - - Per-session rate-limit : 1 calls per 1d period for this particular operation (bulk-load) - """ - if self.setup and not refresh: - return self.setup - - response = await self.__get("setup") - - setup = Setup(**humps.decamelize(response)) - - # Cache response - self.setup = setup - self.gateways = setup.gateways - self.devices = setup.devices - - return setup - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_diagnostic_data(self) -> JSON: - """ - Get all data about the connected user setup - -> gateways data (serial number, activation state, ...): - -> setup location: - -> house places (rooms and floors): - -> setup devices: - - This data will be masked to not return any confidential or PII data. - """ - response = await self.__get("setup") - - return obfuscate_sensitive_data(response) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_devices(self, refresh: bool = False) -> list[Device]: - """ - List devices - Per-session rate-limit : 1 calls per 1d period for this particular operation (bulk-load) - """ - if self.devices and not refresh: - return self.devices - - response = await self.__get("setup/devices") - devices = [Device(**d) for d in humps.decamelize(response)] - - # Cache response - self.devices = devices - if self.setup: - self.setup.devices = devices - - return devices - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_gateways(self, refresh: bool = False) -> list[Gateway]: - """ - Get every gateways of a connected user setup - Per-session rate-limit : 1 calls per 1d period for this particular operation (bulk-load) - """ - if self.gateways and not refresh: - return self.gateways - - response = await self.__get("setup/gateways") - gateways = [Gateway(**g) for g in humps.decamelize(response)] - - # Cache response - self.gateways = gateways - if self.setup: - self.setup.gateways = gateways - - return gateways - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_execution_history(self) -> list[HistoryExecution]: - """ - List execution history - """ - response = await self.__get("history/executions") - execution_history = [HistoryExecution(**h) for h in humps.decamelize(response)] - - return execution_history - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_device_definition(self, deviceurl: str) -> JSON | None: - """ - Retrieve a particular setup device definition - """ - response: dict = await self.__get( - f"setup/devices/{urllib.parse.quote_plus(deviceurl)}" - ) - - return response.get("definition") - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_state(self, deviceurl: str) -> list[State]: - """ - Retrieve states of requested device - """ - response = await self.__get( - f"setup/devices/{urllib.parse.quote_plus(deviceurl)}/states" - ) - state = [State(**s) for s in humps.decamelize(response)] - - return state - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def refresh_states(self) -> None: - """ - Ask the box to refresh all devices states for protocols supporting that operation - """ - await self.__post("setup/devices/states/refresh") - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def refresh_device_states(self, deviceurl: str) -> None: - """ - Ask the box to refresh all states of the given device for protocols supporting that operation - """ - await self.__post( - f"setup/devices/{urllib.parse.quote_plus(deviceurl)}/states/refresh" - ) - - @backoff.on_exception(backoff.expo, TooManyConcurrentRequestsException, max_tries=5) - async def register_event_listener(self) -> str: - """ - Register a new setup event listener on the current session and return a new - listener id. - Only one listener may be registered on a given session. - Registering an new listener will invalidate the previous one if any. - Note that registering an event listener drastically reduces the session - timeout : listening sessions are expected to call the /events/{listenerId}/fetch - API on a regular basis. - """ - response = await self.__post("events/register") - listener_id = cast(str, response.get("id")) - self.event_listener_id = listener_id - - return listener_id - - @backoff.on_exception(backoff.expo, TooManyConcurrentRequestsException, max_tries=5) - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - @backoff.on_exception( - backoff.expo, - (InvalidEventListenerIdException, NoRegisteredEventListenerException), - max_tries=2, - on_backoff=refresh_listener, - ) - async def fetch_events(self) -> list[Event]: - """ - Fetch new events from a registered event listener. Fetched events are removed - from the listener buffer. Return an empty response if no event is available. - Per-session rate-limit : 1 calls per 1 SECONDS period for this particular - operation (polling) - """ - await self._refresh_token_if_expired() - response = await self.__post(f"events/{self.event_listener_id}/fetch") - events = [Event(**e) for e in humps.decamelize(response)] - - return events - - async def unregister_event_listener(self) -> None: - """ - Unregister an event listener. - API response status is always 200, even on unknown listener ids. - """ - await self._refresh_token_if_expired() - await self.__post(f"events/{self.event_listener_id}/unregister") - self.event_listener_id = None - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_current_execution(self, exec_id: str) -> Execution: - """Get an action group execution currently running""" - response = await self.__get(f"exec/current/{exec_id}") - execution = Execution(**humps.decamelize(response)) - - return execution - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_current_executions(self) -> list[Execution]: - """Get all action groups executions currently running""" - response = await self.__get("exec/current") - executions = [Execution(**e) for e in humps.decamelize(response)] - - return executions - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_api_version(self) -> str: - """Get the API version (local only)""" - response = await self.__get("apiVersion") - - return cast(str, response["protocolVersion"]) - - @backoff.on_exception(backoff.expo, TooManyExecutionsException, max_tries=10) - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def execute_command( - self, - device_url: str, - command: Command | str, - label: str | None = "python-overkiz-api", - ) -> str: - """Send a command""" - if isinstance(command, str): - command = Command(command) - - response: str = await self.execute_commands(device_url, [command], label) - - return response - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def cancel_command(self, exec_id: str) -> None: - """Cancel a running setup-level execution""" - await self.__delete(f"/exec/current/setup/{exec_id}") - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def execute_commands( - self, - device_url: str, - commands: list[Command], - label: str | None = "python-overkiz-api", - ) -> str: - """Send several commands in one call""" - payload = { - "label": label, - "actions": [{"deviceURL": device_url, "commands": commands}], - } - response: dict = await self.__post("exec/apply", payload) - return cast(str, response["execId"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_scenarios(self) -> list[Scenario]: - """List the scenarios""" - response = await self.__get("actionGroups") - return [Scenario(**scenario) for scenario in response] - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_places(self) -> Place: - """List the places""" - response = await self.__get("setup/places") - places = Place(**humps.decamelize(response)) - return places - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def generate_local_token(self, gateway_id: str) -> str: - """ - Generates a new token - Access scope : Full enduser API access (enduser/*) - """ - response = await self.__get(f"config/{gateway_id}/local/tokens/generate") - - return cast(str, response["token"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def activate_local_token( - self, gateway_id: str, token: str, label: str, scope: str = "devmode" - ) -> str: - """ - Create a token - Access scope : Full enduser API access (enduser/*) - """ - response = await self.__post( - f"config/{gateway_id}/local/tokens", - {"label": label, "token": token, "scope": scope}, - ) - - return cast(str, response["requestId"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_local_tokens( - self, gateway_id: str, scope: str = "devmode" - ) -> list[LocalToken]: - """ - Get all gateway tokens with the given scope - Access scope : Full enduser API access (enduser/*) - """ - response = await self.__get(f"config/{gateway_id}/local/tokens/{scope}") - local_tokens = [LocalToken(**lt) for lt in humps.decamelize(response)] - - return local_tokens - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def delete_local_token(self, gateway_id: str, uuid: str) -> bool: - """ - Delete a token - Access scope : Full enduser API access (enduser/*) - """ - await self.__delete(f"config/{gateway_id}/local/tokens/{uuid}") - - return True - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def execute_scenario(self, oid: str) -> str: - """Execute a scenario""" - response = await self.__post(f"exec/{oid}") - return cast(str, response["execId"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def execute_scheduled_scenario(self, oid: str, timestamp: int) -> str: - """Execute a scheduled scenario""" - response = await self.__post(f"exec/schedule/{oid}/{timestamp}") - return cast(str, response["triggerId"]) - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_setup_options(self) -> list[Option]: - """ - This operation returns all subscribed options of a given setup. - Per-session rate-limit : 1 calls per 1d period for this particular operation (bulk-load) - Access scope : Full enduser API access (enduser/*) - """ - response = await self.__get("setup/options") - options = [Option(**o) for o in humps.decamelize(response)] - - return options - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_setup_option(self, option: str) -> Option | None: - """ - This operation returns the selected subscribed option of a given setup. - For example `developerMode-{gateway_id}` to understand if developer mode is on. - """ - response = await self.__get(f"setup/options/{option}") - - if response: - return Option(**humps.decamelize(response)) - - return None - - @backoff.on_exception( - backoff.expo, - (NotAuthenticatedException, ServerDisconnectedError, ClientConnectorError), - max_tries=2, - on_backoff=relogin, - ) - async def get_setup_option_parameter( - self, option: str, parameter: str - ) -> OptionParameter | None: - """ - This operation returns the selected parameters of a given setup and option. - For example `developerMode-{gateway_id}` and `gatewayId` to understand if developer mode is on. - - If the option is not available, an OverkizException will be thrown. - If the parameter is not available you will receive None. - """ - response = await self.__get(f"setup/options/{option}/{parameter}") - - if response: - return OptionParameter(**humps.decamelize(response)) - - return None - - async def __get(self, path: str) -> Any: - """Make a GET request to the OverKiz API""" - headers = {} - - await self._refresh_token_if_expired() - if self._access_token: - headers["Authorization"] = f"Bearer {self._access_token}" - - async with self.session.get( - f"{self.server.endpoint}{path}", - headers=headers, - ssl_context=self._ssl_context, - ) as response: - await self.check_response(response) - return await response.json() - - async def __post( - self, path: str, payload: JSON | None = None, data: JSON | None = None - ) -> Any: - """Make a POST request to the OverKiz API""" - headers = {} - - if path != "login" and self._access_token: - await self._refresh_token_if_expired() - headers["Authorization"] = f"Bearer {self._access_token}" - - async with self.session.post( - f"{self.server.endpoint}{path}", - data=data, - json=payload, - headers=headers, - ssl_context=self._ssl_context, - ) as response: - await self.check_response(response) - return await response.json() - - async def __delete(self, path: str) -> None: - """Make a DELETE request to the OverKiz API""" - headers = {} - - await self._refresh_token_if_expired() - - if self._access_token: - headers["Authorization"] = f"Bearer {self._access_token}" - - async with self.session.delete( - f"{self.server.endpoint}{path}", - headers=headers, - ssl_context=self._ssl_context, - ) as response: - await self.check_response(response) - - @staticmethod - async def check_response(response: ClientResponse) -> None: - """Check the response returned by the OverKiz API""" - if response.status in [200, 204]: - return - - try: - result = await response.json(content_type=None) - except JSONDecodeError as error: - result = await response.text() - - if "is down for maintenance" in result: - raise MaintenanceException("Server is down for maintenance") from error - - if response.status == 503: - raise ServiceUnavailableException(result) from error - - raise OverkizException( - f"Unknown error while requesting {response.url}. {response.status} - {result}" - ) from error - - if result.get("errorCode"): - # Error messages between cloud and local Overkiz servers can be slightly different - # To make it easier to have a strict match for these errors, we remove the double quotes and the period at the end. - - if message := result.get("error"): - message = message.strip( - '".' - ) # Change to .removesuffix when Python 3.8 support is dropped - else: - # An error message can have an empty (None) message - message = "" - - # {"errorCode": "AUTHENTICATION_ERROR", - # "error": "Too many requests, try again later : login with xxx@xxx.tld"} - if "Too many requests" in message: - raise TooManyRequestsException(message) - - # {"errorCode": "AUTHENTICATION_ERROR", "error": "Bad credentials"} - if message == "Bad credentials": - raise BadCredentialsException(message) - - # {"errorCode": "RESOURCE_ACCESS_DENIED", "error": "Not authenticated"} - if message == "Not authenticated": - raise NotAuthenticatedException(message) - - # {"error":"Missing authorization token.","errorCode":"RESOURCE_ACCESS_DENIED"} - if message == "Missing authorization token": - raise MissingAuthorizationTokenException(message) - - # {"error": "Server busy, please try again later. (Too many executions)"} - if message == "Server busy, please try again later. (Too many executions)": - raise TooManyExecutionsException(message) - - # {"error": "UNSUPPORTED_OPERATION", "error": "No such command : ..."} - if "No such command" in message: - raise InvalidCommandException(message) - - # {'errorCode': 'UNSPECIFIED_ERROR', 'error': 'Invalid event listener id : ...'} - if "Invalid event listener id" in message: - raise InvalidEventListenerIdException(message) - - # {'errorCode': 'UNSPECIFIED_ERROR', 'error': 'No registered event listener'} - if message == "No registered event listener": - raise NoRegisteredEventListenerException(message) - - # {"errorCode": "RESOURCE_ACCESS_DENIED", "error": "too many concurrent requests"} - if message == "too many concurrent requests": - raise TooManyConcurrentRequestsException(message) - - # {'errorCode': 'EXEC_QUEUE_FULL', 'error': 'Execution queue is full on gateway: #xxx-yyyy-zzzz (soft limit: 10)'} - if "Execution queue is full on gateway" in message: - raise ExecutionQueueFullException(message) - - if message == "Cannot use JSESSIONID and bearer token in same request": - raise SessionAndBearerInSameRequestException(message) - - if message == "Too many attempts with an invalid token, temporarily banned": - raise TooManyAttemptsBannedException(message) - - if "Invalid token : " in message: - raise InvalidTokenException(message) - - if "Not such token with UUID: " in message: - raise NotSuchTokenException(message) - - if "Unknown user :" in message: - raise UnknownUserException(message) - - # {"error":"Unknown object.","errorCode":"UNSPECIFIED_ERROR"} - if message == "Unknown object": - raise UnknownObjectException(message) - - # {'errorCode': 'RESOURCE_ACCESS_DENIED', 'error': 'Access denied to gateway #1234-5678-1234 for action ADD_TOKEN'} - if "Access denied to gateway" in message: - raise AccessDeniedToGatewayException(message) - - # General Overkiz exception - raise OverkizException(result) - - async def _refresh_token_if_expired(self) -> None: - """Check if token is expired and request a new one.""" - if ( - self._expires_in - and self._refresh_token - and self._expires_in <= datetime.datetime.now() - ): - await self.refresh_token() - - if self.event_listener_id: - await self.register_event_listener() diff --git a/pyoverkiz/const.py b/pyoverkiz/const.py deleted file mode 100644 index c318c57..0000000 --- a/pyoverkiz/const.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -from pyoverkiz.enums import Server -from pyoverkiz.models import OverkizServer - -COZYTOUCH_ATLANTIC_API = "https://apis.groupe-atlantic.com" -COZYTOUCH_CLIENT_ID = ( - "Q3RfMUpWeVRtSUxYOEllZkE3YVVOQmpGblpVYToyRWNORHpfZHkzNDJVSnFvMlo3cFNKTnZVdjBh" -) - -NEXITY_API = "https://api.egn.prd.aws-nexity.fr" -NEXITY_COGNITO_CLIENT_ID = "3mca95jd5ase5lfde65rerovok" -NEXITY_COGNITO_USER_POOL = "eu-west-1_wj277ucoI" -NEXITY_COGNITO_REGION = "eu-west-1" - -SOMFY_API = "https://accounts.somfy.com" -SOMFY_CLIENT_ID = "0d8e920c-1478-11e7-a377-02dd59bd3041_1ewvaqmclfogo4kcsoo0c8k4kso884owg08sg8c40sk4go4ksg" -SOMFY_CLIENT_SECRET = "12k73w1n540g8o4cokg0cw84cog840k84cwggscwg884004kgk" - -LOCAL_API_PATH = "/enduser-mobile-web/1/enduserAPI/" - -SERVERS_WITH_LOCAL_API = [ - Server.SOMFY_EUROPE, - Server.SOMFY_OCEANIA, - Server.SOMFY_AMERICA, -] - -SUPPORTED_SERVERS: dict[str, OverkizServer] = { - Server.ATLANTIC_COZYTOUCH: OverkizServer( - name="Atlantic Cozytouch", - endpoint="https://ha110-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Atlantic", - configuration_url=None, - ), - Server.BRANDT: OverkizServer( - name="Brandt Smart Control", - endpoint="https://ha3-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Brandt", - configuration_url=None, - ), - Server.FLEXOM: OverkizServer( - name="Flexom", - endpoint="https://ha108-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Bouygues", - configuration_url=None, - ), - Server.HEXAOM_HEXACONNECT: OverkizServer( - name="Hexaom HexaConnect", - endpoint="https://ha5-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Hexaom", - configuration_url=None, - ), - Server.HI_KUMO_ASIA: OverkizServer( - name="Hitachi Hi Kumo (Asia)", - endpoint="https://ha203-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Hitachi", - configuration_url=None, - ), - Server.HI_KUMO_EUROPE: OverkizServer( - name="Hitachi Hi Kumo (Europe)", - endpoint="https://ha117-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Hitachi", - configuration_url=None, - ), - Server.HI_KUMO_OCEANIA: OverkizServer( - name="Hitachi Hi Kumo (Oceania)", - endpoint="https://ha203-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Hitachi", - configuration_url=None, - ), - Server.NEXITY: OverkizServer( - name="Nexity Eugénie", - endpoint="https://ha106-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Nexity", - configuration_url=None, - ), - Server.REXEL: OverkizServer( - name="Rexel Energeasy Connect", - endpoint="https://ha112-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Rexel", - configuration_url="https://utilisateur.energeasyconnect.com/user/#/zone/equipements", - ), - Server.SIMU_LIVEIN2: OverkizServer( # alias of https://tahomalink.com - name="SIMU (LiveIn2)", - endpoint="https://ha101-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Somfy", - configuration_url=None, - ), - Server.SOMFY_EUROPE: OverkizServer( # alias of https://tahomalink.com - name="Somfy (Europe)", - endpoint="https://ha101-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Somfy", - configuration_url="https://www.tahomalink.com", - ), - Server.SOMFY_AMERICA: OverkizServer( - name="Somfy (North America)", - endpoint="https://ha401-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Somfy", - configuration_url=None, - ), - Server.SOMFY_OCEANIA: OverkizServer( - name="Somfy (Oceania)", - endpoint="https://ha201-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Somfy", - configuration_url=None, - ), - Server.UBIWIZZ: OverkizServer( - name="Ubiwizz", - endpoint="https://ha129-1.overkiz.com/enduser-mobile-web/enduserAPI/", - manufacturer="Decelect", - configuration_url=None, - ), -} diff --git a/pyoverkiz/enums/__init__.py b/pyoverkiz/enums/__init__.py deleted file mode 100644 index a7bccb4..0000000 --- a/pyoverkiz/enums/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# flake8: noqa: F401, F403 - -from .command import * -from .execution import * -from .gateway import * -from .general import * -from .measured_value_type import * -from .protocol import * -from .server import * -from .state import * -from .ui import * diff --git a/pyoverkiz/enums/__pycache__/__init__.cpython-39.pyc b/pyoverkiz/enums/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 1f3c013b3873f5f70d08061711895ee2921e61de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 345 zcmY+=y-EZz5CGuKu6Mg990)$e3T}$44&<=25gV$ zPE+WF%$FI+1j3ig(*k4j3Gjiw9gDwFVsVYz-(nhzk;vlVSCTEWRrVx1%g(cPwn5AL zqOINuj%-OH7kBL<)q_!r>#<+JGYl~Zqx*Wx13d8w51_$u*`F)mJ~|lLfy)RiOcqjO zoe5?zGR-}>1GqHv0dMwY6t9J=@tL3WbwhVX0kwj5(hOWuDITev!p`Vbx29eMxU_uO o^DTJn8yvM4FBEiCo-!u12#j@hO3jrrW0bI|j$d diff --git a/pyoverkiz/enums/__pycache__/command.cpython-39.pyc b/pyoverkiz/enums/__pycache__/command.cpython-39.pyc deleted file mode 100644 index e27f2befb5727e298f336ef000710f5e850f2f11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9645 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 Kls93NRPq-piv`pG diff --git a/pyoverkiz/enums/__pycache__/gateway.cpython-39.pyc b/pyoverkiz/enums/__pycache__/gateway.cpython-39.pyc deleted file mode 100644 index f482d6ebac3970eb5e25cff88246cf465dae81df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3576 zcmcImS##US5eC5f(0#8j+TO$TA+F_IyV*DfNmztM0xAHMtga*zN;8(ZrbzC9+>KPu z)A~7oU^i9PqyA7H@R}$7f>iBG(hZUlr6g5bsSQ~8i0SD$x_kPYZcI(36?~rlLkKsk zD1WEM<(q(Z$ZuDVJ- zGr&D5R$dDdX1iy!`$s{%-)?@@7kO;DZwC}a_6 zT!jeNAj%^U<57t77$kTck~{$^o`f_{L58Ox%QG;=voOu4V1`e_ET4fnJ`3}F4i@-4 zEb;|d;)}4%mtci2!zy2aHNFZT@HM!`KY;7}8rQUxA;xS@mp}4=im;%4R`q+ z_>kX)kNAi1G5-iY;UB|2{t4vyJ-E;F@POZkhx`FN66^dSfAmg4-xb8C;xS+UUgZV; z>ANUOHuz)o(ngTkZ1lu8jbrb)D+d3>N2+MXQ|j34@r<4aTdwELd&Ughdr^*cPTZk%nR-x70FLaN4uRHB0>z@_; zW=}loHV)o24uy~M&euY|X`U1Wo>;$dIE>wT_c%zgqo(iUdh9o38693lTUJk&CgQt( zu5+da@!`%7DBREA*G{$f+NttqbQHQP!lS3!=o^!C_>JyLSb@rZ zP;(IX8faF7{wwq!ae6+2=c0$A?Z%M^5>j+qjRO(Hdd*%-pmUsWErY^^6*1Mw&I# zGGV4-vaNc}W_nrQ_6+A@o(-6M9z zmC4pd1yUs&_B^{@3O_B&cxl@fq+htZtfbcrM~A8Ni5Lb0<|o&w*=1ULW>RMFY(pIF zA$QDHbkEZrojJN$Wo6TXc~a(qb?4ZQX)7 z!!n#rLwEOB*j@5q0ex+-ZO8VEk~bc_a(S>`cRfe1nmVj_6~n37uBW4}U3@l17=3w=Vs5KV@+@G!VhDw zqBd?4Vn)@pJ(xX@26Oc~9fa7f>Ehp5-Q6`k4At^*(<9@}9h!Q}^dNgN9^*cDWf#m1 zG6$`O?;i=|3NF9E6C5PFveOl^cN|3go|MFt6S+iuBPpR|0)fcg{{uo&i>m*JpQfql zsHTQ%5g%WA&hOSXihs(xRV?y(z7XDdOJZ59@I}1ymiTgb=ix1jciw6+J>mt`bF#6t*3sX4Y$v8fFm@Rid znL|RbOUyH>n5eZ-G|p~JOvA#|J5Tp9(<8rx+|R}uNKC^emqs19f@>Oehq<2Z7;t+m z)=k&881jr|I?V7G@>SXNOk{|~@st(Y@mQ&%BY9TONe77!Zj3dIQr#g&a4)GYT#r2rdy$>;ukgdafxkx@F+xPMD$%AIHU`u7liTsmVSrh=JzmtzT2 z?EJ~?HF|wNn6hk-)wj#Krx&Y6Fyk0{d5?MaP-X--&ILtdBrCHbddF}uUct(FcQ}VZ zw~t<}?gi=kHqC3#1!!8*?2vUclS49OOhdZMBILKnb=QlfT z)@);iNn8YT`&bWBLF%s~%ZHN?JxK~pxYB>xUXc7^So=JsVEI(WC)K1ntEHo9by~|} MeCI#M_qL|~3kj4`YybcN diff --git a/pyoverkiz/enums/__pycache__/general.cpython-39.pyc b/pyoverkiz/enums/__pycache__/general.cpython-39.pyc deleted file mode 100644 index 4e0e7a51c6db1a75e9196650bdf3e42a5ca15887..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18573 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 XubR`Tjxjj8YxeYk;dTstXc_u{OEMx& diff --git a/pyoverkiz/enums/__pycache__/state.cpython-39.pyc b/pyoverkiz/enums/__pycache__/state.cpython-39.pyc deleted file mode 100644 index 9aba9c3f2a9f1f72bbecb15221360551d210e00f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14529 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 diff --git a/pyoverkiz/enums/__pycache__/ui.cpython-39.pyc b/pyoverkiz/enums/__pycache__/ui.cpython-39.pyc deleted file mode 100644 index 74e0f83727897babb69e64421bf0a58594ff84eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18334 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 diff --git a/pyoverkiz/enums/command.py b/pyoverkiz/enums/command.py deleted file mode 100644 index 616609e..0000000 --- a/pyoverkiz/enums/command.py +++ /dev/null @@ -1,281 +0,0 @@ -import sys -from enum import unique - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class OverkizCommand(StrEnum): - """Device commands used by Overkiz.""" - - ACTIVATE_OPTION = "activateOption" - ADVANCED_REFRESH = "advancedRefresh" - ALARM_OFF = "alarmOff" - ALARM_ON = "alarmOn" - ALARM_PARTIAL_1 = "alarmPartial1" - ALARM_PARTIAL_2 = "alarmPartial2" - ALARM_ZONE_ON = "alarmZoneOn" - ARM = "arm" - ARM_PARTIAL_DAY = "armPartialDay" - ARM_PARTIAL_NIGHT = "armPartialNight" - BIP = "bip" - CLOSE = "close" - CLOSE_SLATS = "closeSlats" - CYCLE = "cycle" - DEACTIVATE_OPTION = "deactivateOption" - DEPLOY = "deploy" - DISARM = "disarm" - DING_DONG = "dingDong" - DOWN = "down" - EXIT_DEROGATION = "exitDerogation" - FAST_BIP_SEQUENCE = "fastBipSequence" - GET_LEVEL = "getLevel" - GET_NAME = "getName" - GLOBAL_CONTROL = "globalControl" - GO_TO_ALIAS = "goToAlias" - IDENTIFY = "identify" - LOCK = "lock" - MEMORIZED_VOLUME = "memorizedVolume" - MY = "my" - OFF = "off" - OFFLINE = "offline" - ON = "on" - ONLINE = "online" - ON_WITH_TIMER = "onWithTimer" - OPEN = "open" - OPEN_SLATS = "openSlats" - PARTIAL = "partial" - PARTIAL_POSITION = "partialPosition" - REFRESH_BOOST_MODE_DURATION = "refreshBoostModeDuration" - REFRESH_COMFORT_HEATING_TARGET_TEMPERATURE = ( - "refreshComfortHeatingTargetTemperature" - ) - REFRESH_COMFORT_TARGET_DWH_TEMPERATURE = "refreshComfortTargetDHWTemperature" - REFRESH_DEROGATION_REMAINING_TIME = "refreshDerogationRemainingTime" - REFRESH_ECO_HEATING_TARGET_TEMPERATURE = "refreshEcoHeatingTargetTemperature" - REFRESH_ECO_TARGET_DWH_TEMPERATURE = "refreshEcoTargetDHWTemperature" - REFRESH_DHW_MODE = "refreshDHWMode" - REFRESH_PASS_APC_HEATING_MODE = "refreshPassAPCHeatingMode" - REFRESH_PASS_APC_HEATING_PROFILE = "refreshPassAPCHeatingProfile" - REFRESH_STATE = "refreshState" - REFRESH_TARGET_DWH_TEMPERATURE = "refreshTargetDHWTemperature" - REFRESH_TARGET_TEMPERATURE = "refreshTargetTemperature" - REFRESH_VENTILATION_CONFIGURATION_MODE = "refreshVentilationConfigurationMode" - REFRESH_VENTILATION_STATE = "refreshVentilationState" - REFRESH_WATER_TARGET_TEMPERATURE = "refreshWaterTargetTemperature" - RIGHT = "right" - RING = "ring" - RING_WITH_SINGLE_SIMPLE_SEQUENCE = "ringWithSingleSimpleSequence" - SAVE_ALIAS = "saveAlias" - SET_ABSENCE_MODE = "setAbsenceMode" - SET_ACTIVE_MODE = "setActiveMode" - SET_AIR_DEMAND_MODE = "setAirDemandMode" - SET_ALARM_STATUS = "setAlarmStatus" - SET_ALL_MODE_TEMPERATURES = "setAllModeTemperatures" - SET_AWAY_MODE_DURATION = "setAwayModeDuration" - SET_BOOST_MODE = "setBoostMode" - SET_BOOST_MODE_DURATION = "setBoostModeDuration" - SET_BOOST_ON_OFF_STATE = "setBoostOnOffState" - SET_CLOSURE = "setClosure" - SET_CLOSURE_AND_LINEAR_SPEED = "setClosureAndLinearSpeed" - SET_CLOSURE_AND_ORIENTATION = "setClosureAndOrientation" - SET_COMFORT_HEATING_TARGET_TEMPERATURE = "setComfortHeatingTargetTemperature" - SET_COMFORT_TARGET_DHW_TEMPERATURE = "setComfortTargetDHWTemperature" - SET_COMFORT_TEMPERATURE = "setComfortTemperature" - SET_CONTROL_DHW = "setControlDHW" - SET_CONTROL_DHW_SETTING_TEMPERATURE = "setControlDHWSettingTemperature" - SET_CURRENT_OPERATING_MODE = "setCurrentOperatingMode" - SET_DEPLOYMENT = "setDeployment" - SET_DEROGATED_TARGET_TEMPERATURE = "setDerogatedTargetTemperature" - SET_DEROGATION = "setDerogation" - SET_DEROGATION_ON_OFF_STATE = "setDerogationOnOffState" - SET_DEROGATION_TIME = "setDerogationTime" - SET_DHW_MODE = "setDHWMode" - SET_DHW_ON_OFF_STATE = "setDHWOnOffState" - SET_ECO_HEATING_TARGET_TEMPERATURE = "setEcoHeatingTargetTemperature" - SET_ECO_TARGET_DHW_TEMPERATURE = "setEcoTargetDHWTemperature" - SET_ECO_TEMPERATURE = "setEcoTemperature" - SET_EXPECTED_NUMBER_OF_SHOWER = "setExpectedNumberOfShower" - SET_FORCE_HEATING = "setForceHeating" - SET_HEATING_LEVEL = "setHeatingLevel" - SET_HEATING_ON_OFF = "setHeatingOnOffState" - SET_HEATING_TARGET_TEMPERATURE = "setHeatingTargetTemperature" - SET_HOLIDAYS = "setHolidays" - SET_INTENSITY = "setIntensity" - SET_LEVEL = "setLevel" - SET_MANU_AND_SET_POINT_MODES = "setManuAndSetPointModes" - SET_MEMORIZED_1_POSITION = "setMemorized1Position" - SET_MEMORIZED_SIMPLE_VOLUME = "setMemorizedSimpleVolume" - SET_MODE_TEMPERATURE = "setModeTemperature" - SET_NAME = "setName" - SET_OPERATING_MODE = "setOperatingMode" - SET_ORIENTATION = "setOrientation" - SET_PASS_APC_DHW_MODE = "setPassAPCDHWMode" - SET_PASS_APC_HEATING_MODE = "setPassAPCHeatingMode" - SET_PASS_APC_OPERATING_MODE = "setPassAPCOperatingMode" - SET_PEDESTRIAN_POSITION = "setPedestrianPosition" - SET_RGB = "setRGB" - SET_SCHEDULING_TYPE = "setSchedulingType" - SET_SECURED_POSITION_TEMPERATURE = "setSecuredPositionTemperature" - SET_TARGET_ALARM_MODE = "setTargetAlarmMode" - SET_TARGET_TEMPERATURE = "setTargetTemperature" - SET_TOWEL_DRYER_OPERATING_MODE = "setTowelDryerOperatingMode" - SET_TOWEL_DRYER_TEMPORARY_STATE = "setTowelDryerTemporaryState" - SET_VALVE_SETTINGS = "setValveSettings" - SET_VENTILATION_CONFIGURATION_MODE = "setVentilationConfigurationMode" - SET_VENTILATION_MODE = "setVentilationMode" - SET_WATER_TARGET_TEMPERATURE = "setWaterTargetTemperature" - STANDARD = "standard" - START_IDENTIFY = "startIdentify" - STOP = "stop" - STOP_IDENTIFY = "stopIdentify" - TEST = "test" - TILT = "tilt" - TILT_DOWN = "tiltDown" - TILT_UP = "tiltUp" - UNDEPLOY = "undeploy" - UNINSTALLED = "uninstalled" - UNLOCK = "unlock" - UP = "up" - UPDATE = "update" - WINK = "wink" - - -@unique -class OverkizCommandParam(StrEnum): - """Parameter used by Overkiz commands and/or states.""" - - A = "A" - ABSENCE = "absence" - ACTIVE = "active" - ADJUSTMENT = "adjustment" - ARMED = "armed" - ARMED_DAY = "armedDay" - ARMED_NIGHT = "armedNight" - AT_HOME_MODE = "atHomeMode" - AUTO = "auto" - AUTOCOOLING = "autocooling" - AUTOHEATING = "autoheating" - AUTO_MODE = "autoMode" - AVAILABLE = "available" - AWAY = "away" - AWAY_MODE = "awayMode" - B = "B" - BASIC = "basic" - BY_PASS = "by_pass" - BOOST = "boost" - BOTH = "both" - C = "C" - CLOSE = "close" - CLOSED = "closed" - COMFORT = "comfort" - COMFORT_1 = "comfort-1" - COMFORT_2 = "comfort-2" - COOLING = "cooling" - DAY_OFF = "day-off" - DEROGATION = "derogation" - DEAD = "dead" - DEHUMIDIFY = "dehumidify" - DETECTED = "detected" - DISARMED = "disarmed" - DISABLE = "disable" - DISABLED = "disabled" - DRYING = "drying" - ECO = "eco" - ENABLE = "enable" - ENABLED = "enabled" - ENERGY_DEMAND_STATUS = "energyDemandStatus" - EXTERNAL = "external" - EXTERNAL_GATEWAY = "externalGateway" - EXTERNAL_SCHEDULING = "externalScheduling" - EXTERNAL_SETPOINT = "externalSetpoint" - FAN = "fan" - FINISHED = "finished" - FREE = "free" - FREEZE_MODE = "freezeMode" - FROSTPROTECTION = "frostprotection" - FULL = "full" - FULL_CLOSED = "full_closed" - FULL_OPEN = "full_open" - FURTHER_NOTICE = "further_notice" - GEOFENCING_MODE = "geofencingMode" - HEATING = "heating" - HI = "hi" - HIGH = "high" - HIGHEST = "highest" - HIGH_DEMAND = "high demand" # not a typo... - HOLIDAYS = "holidays" - HORIZONTAL = "horizontal" - INACTIVE = "inactive" - INTERNAL = "internal" - INTERNAL_SCHEDULING = "internalScheduling" - LO = "lo" - LOCAL_USER = "localUser" - LOCKED = "locked" - LOCK_KEY = "lock_key" - LOW = "low" - LOWSPEED = "lowspeed" - LOW_BATTERY = "lowBattery" - LSC = "LSC" - MAINTENANCE_REQUIRED = "maintenanceRequired" - MANU = "manu" - MANUAL = "manual" - MANUAL_ECO_ACTIVE = "manualEcoActive" - MANUAL_ECO_INACTIVE = "manualEcoInactive" - MANUAL_MODE = "manualMode" - MAX_SETPOINT = "max_setpoint" - MED = "med" - MEDIUM = "medium" - MEMORIZED_VOLUME = "memorizedVolume" - MIN_SETPOINT = "min_setpoint" - NORMAL = "normal" - NONE = "none" - NOT_DETECTED = "notDetected" - NO_DEFECT = "noDefect" - OFF = "off" - ON = "on" - OPEN = "open" - OPENED = "opened" - OPEN_WINDOW = "open_window" - PAIRING = "pairing" - PARTIAL = "partial" - PARTIAL_1 = "partial1" - PARTIAL_2 = "partial2" - PEDESTRIAN = "pedestrian" - PENDING = "pending" - PERMANENT_HEATING = "permanentHeating" - PERSON_INSIDE = "personInside" - PROG = "prog" - RELAUNCH = "relaunch" - RESET = "reset" - SAAC = "SAAC" - SECURED = "secured" - SFC = "SFC" - SILENCE = "silence" - SILENT = "silent" - SLEEPING_MODE = "sleepingMode" - STANDARD = "standard" - STANDBY = "standby" - STOP = "stop" - SUDDEN_DROP_MODE = "suddenDropMode" - TEMPERATURE_OFFSET = "temperature_offset" - TILT = "tilt" - TOTAL = "total" - UNDETECTED = "undetected" - UPS = "UPS" - VERTICAL = "vertical" - VERY_LOW = "verylow" - ZONE_1 = "zone1" - ZONE_2 = "zone2" - - -@unique -class CommandMode(StrEnum): - HIGH_PRIORITY = "highPriority" - GEOLOCATED = "geolocated" - INTERNAL = "internal" diff --git a/pyoverkiz/enums/execution.py b/pyoverkiz/enums/execution.py deleted file mode 100644 index 9c85542..0000000 --- a/pyoverkiz/enums/execution.py +++ /dev/null @@ -1,70 +0,0 @@ -import logging -import sys -from enum import unique - -_LOGGER = logging.getLogger(__name__) - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class ExecutionType(StrEnum): - UNKNOWN = "UNKNOWN" - IMMEDIATE_EXECUTION = "Immediate execution" - DELAYED_EXECUTION = "Delayed execution" - TECHNICAL_EXECUTION = "Technical execution" - PLANNING = "Planning" - RAW_TRIGGER_SERVER = "Raw trigger (Server)" - RAW_TRIGGER_GATEWAY = "Raw trigger (Gateway)" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - -@unique -class ExecutionState(StrEnum): - UNKNOWN = "UNKNOWN" - INITIALIZED = "INITIALIZED" - NOT_TRANSMITTED = "NOT_TRANSMITTED" - TRANSMITTED = "TRANSMITTED" - IN_PROGRESS = "IN_PROGRESS" - COMPLETED = "COMPLETED" - FAILED = "FAILED" - QUEUED_GATEWAY_SIDE = "QUEUED_GATEWAY_SIDE" - QUEUED_SERVER_SIDE = "QUEUED_SERVER_SIDE" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - -@unique -class ExecutionSubType(StrEnum): - UNKNOWN = "UNKNOWN" - _ = "-" - ACTION_GROUP = "ACTION_GROUP" - ACTION_GROUP_SEQUENCE = "ACTION_GROUP_SEQUENCE" - DAWN_TRIGGER = "DAWN_TRIGGER" - DUSK_TRIGGER = "DUSK_TRIGGER" - DISCRETE_TRIGGER = "DISCRETE_TRIGGER" - DISCRETE_TRIGGER_USER = "DISCRETE_TRIGGER_USER" - GENERIC_COMMAND_SCHEDULING = "GENERIC_COMMAND_SCHEDULING" - IFT_CONDITION = "IFT_CONDITION" - INTERNAL = "INTERNAL" - MANUAL_CONTROL = "MANUAL_CONTROL" - NO_ERROR = "NO_ERROR" - P2P_COMMAND_REGULATION = "P2P_COMMAND_REGULATION" - TIME_TRIGGER = "TIME_TRIGGER" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN diff --git a/pyoverkiz/enums/gateway.py b/pyoverkiz/enums/gateway.py deleted file mode 100644 index 5485d39..0000000 --- a/pyoverkiz/enums/gateway.py +++ /dev/null @@ -1,116 +0,0 @@ -import logging -import sys -from enum import IntEnum, unique - -_LOGGER = logging.getLogger(__name__) - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class GatewayType(IntEnum): - UNKNOWN = -1 - VIRTUAL_KIZBOX = 0 - KIZBOX_V1 = 2 - TAHOMA = 15 - VERISURE_ALARM_SYSTEM = 20 - KIZBOX_MINI = 21 - HI_KUMO_ADAPTER = 22 # Hi Kumo Adapter SPX-WFG01 (constant added manually) - KIZBOX_V2 = 24 - MYFOX_ALARM_SYSTEM = 25 - KIZBOX_MINI_VMBUS = 27 - KIZBOX_MINI_IO = 28 - TAHOMA_V2 = 29 - KIZBOX_V2_3H = 30 - KIZBOX_V2_2H = 31 - COZYTOUCH = 32 - CONNEXOON = 34 - JSW_CAMERA = 35 - TAHOMA_V2_RTS = 41 - KIZBOX_MINI_MODBUS = 42 - KIZBOX_MINI_OVP = 43 - HI_BOX = 44 # Hi Kumo AHP-SMB01 Hi Box (constant added manually) - HATTARA_RAIL_DIN = 47 - WIZZ_BOX = 52 - CONNEXOON_RTS = 53 - OPENDOORS_LOCK_SYSTEM = 54 - CONNEXOON_RTS_JAPAN = 56 - ENERGEASY_CONNECT = 57 - HOME_PROTECT_SYSTEM = 58 - CONNEXOON_RTS_AUSTRALIA = 62 - THERMOSTAT_SOMFY_SYSTEM = 63 - SMARTLY_MINI_DAUGHTERBOARD_ZWAVE = 65 - SMARTLY_MINIBOX_RAILDIN = 66 - TAHOMA_BEE = 67 - TAHOMA_RAIL_DIN = 72 - NEXITY_RAIL_DIN = 74 - TAHOMA_BEECON = 75 - ELIOT = 77 - COZYTOUCH_SAUTER = 83 - WISER = 88 - TAHOMA_SWITCH = 98 - SOMFY_CONNECTIVITY_KIT = 99 - COZYTOUCH_V2 = 105 - TAHOMA_RAIL_DIN_S = 108 - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - @property - def beautify_name(self) -> str: - name = self.name.replace("_", " ").title() - name = name.replace("Tahoma", "TaHoma") - name = name.replace("Rts", "RTS") - return name - - -@unique -class GatewaySubType(IntEnum): - UNKNOWN = -1 - TAHOMA_BASIC = 1 - TAHOMA_BASIC_PLUS = 2 - TAHOMA_PREMIUM = 3 - SOMFY_BOX = 4 - HITACHI_BOX = 5 - MONDIAL_BOX = 6 - MAROC_TELECOM_BOX = 7 - TAHOMA_SERENITY = 8 - TAHOMA_VERISURE = 9 - TAHOMA_SERENITY_PREMIUM = 10 - TAHOMA_MONSIEUR_STORE = 11 - TAHOMA_MAISON_AVENIR_ET_TRADITION = 12 - TAHOMA_SHORT_CHANNEL = 13 - TAHOMA_PRO = 14 - TAHOMA_SECURITY_SHORT_CHANNEL = 15 - TAHOMA_SECURITY_PRO = 16 - # TAHOMA_BOX_C_IO = 12 That’s probably 17, but tahomalink.com says it’s 12 - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - @property - def beautify_name(self) -> str: - name = self.name.replace("_", " ").title() - name = name.replace("Tahoma", "TaHoma") - name = name.replace("Rts", "RTS") - return name - - -@unique -class UpdateBoxStatus(StrEnum): - NOT_UPDATABLE = "NOT_UPDATABLE" - READY_TO_UPDATE = "READY_TO_UPDATE" - READY_TO_BE_UPDATED_BY_SERVER = "READY_TO_BE_UPDATED_BY_SERVER" - READY_TO_UPDATE_LOCALLY = "READY_TO_UPDATE_LOCALLY" - UP_TO_DATE = "UP_TO_DATE" - UNKNOWN = "UNKNOWN" - UPDATING = "UPDATING" diff --git a/pyoverkiz/enums/general.py b/pyoverkiz/enums/general.py deleted file mode 100644 index 497e840..0000000 --- a/pyoverkiz/enums/general.py +++ /dev/null @@ -1,435 +0,0 @@ -import logging -import sys -from enum import IntEnum, unique - -_LOGGER = logging.getLogger(__name__) - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class ProductType(IntEnum): - NONE = 0 - ACTUATOR = 1 - SENSOR = 2 - VIDEO = 3 - CONTROLLABLE = 4 - GATEWAY = 5 - INFRASTRUCTURE_COMPONENT = 6 - GROUP = 7 - - -@unique -class DataType(IntEnum): - NONE = 0 - INTEGER = 1 - FLOAT = 2 - STRING = 3 - BLOB = 4 - DATE = 5 - BOOLEAN = 6 - PASSWORD = 9 - JSON_ARRAY = 10 - JSON_OBJECT = 11 - - -@unique -class FailureType(IntEnum): - UNKNOWN = -1 - NO_FAILURE = 0 - NON_EXECUTING = 11 - ERROR_WHILE_EXECUTING = 12 - ACTUATORUNKNOWN = 101 - ACTUATORNOANSWER = 102 - ERRORREADWRITEACCESS = 103 - ERRORCOMMAND = 104 - CMDUNKNOWN = 105 - CMDCANCELLED = 106 - NOREMOTECONTROL = 107 - ERROR_TRANSFER_KEY = 108 - ERRORDATABASE = 109 - MODELOCALENABLED = 110 - BAD_CMD = 111 - BAD_HD = 112 - BAD_LEN = 113 - BAD_ADDRESS = 114 - BAD_PARAM = 115 - NOT_FOUND_ETX = 116 - BAD_CRC_SERIAL = 117 - BAD_STATUS = 118 - KEY_NOT_RECEIVE = 119 - INSERTION_ERROR = 120 - NODE_NOT_VERIFY_WITH_NEW_KEY = 121 - POOL_FULL = 122 - ADDRESS_UNKNOWN = 123 - NODE_CANT_PAIRED = 124 - NODE_CANT_UPDATE_TRANSFER_STATUS = 125 - UNKNOWN_ERROR = 126 - INVALID_CHANNEL = 127 - INVALID_COMMAND = 128 - SERIAL_IO_ERROR = 129 - OPERATION_NOT_ALLOWED = 130 - RESTART_STACK = 131 - INCOMPLETE_DISCOVER = 132 - TRANFER_KEY_NO_REMOTE_CONTROLLER = 133 - TRANFER_KEY_MULTI_REMOTE_CONTROLLER = 134 - RF_PROTOCOL_FATAL_ERROR = 135 - INTERNAL_ERROR = 136 - BUSY_RADIO_ERROR = 137 - BAD_MAC_ERROR = 138 - SETUP_REQUIRED = 139 - MASTER_AUTHENTICATION_FAILED_ERROR = 140 - END_OF_RECEIVING_CONFIGURATION_MODE = 141 - DATA_TRANSPORT_SERVICE_ERROR = 142 - DATA_TRANSPORT_SERVICE_ABORTED_BY_RECIPIENT = 143 - STOPPED_BY_CONFIGURATION_OPERATION_ERROR = 144 - COMMAND_NAME_TYPE_INVALID = 145 - COMMAND_NAME_NOT_INSTALLED_OR_INVALID = 146 - COMMAND_INVALID_LEN_ON_FRAME = 147 - COMMAND_ZONE_INVALID_OR_NOT_INSTALLED = 148 - COMMAND_SENSOR_VALUE_INVALID = 149 - COMMAND_ZONE_TEMPERATURE_INVALID = 150 - COMMAND_DHW_NOT_INSTALLED_OR_INVALID = 151 - COMMAND_INSERTION_FAILED_ERROR = 152 - NONEXEC_BLOCKED_BY_HAZARD = 153 - NONEXEC_OVERHEATING_PROTECTION = 154 - NONEXEC_DEVICE_LIMITATION = 155 - NONEXEC_DOOR_IS_OPENED = 156 - NONEXEC_MAINTENANCE_REQUIRED = 157 - DEAD_SENSOR = 158 - SENSOR_MAINTENANCE_REQUIRED = 159 - NONEXEC_OTHER = 160 - WHILEEXEC_BLOCKED_BY_HAZARD = 161 - WHILEEXEC_OVERHEATING_PROTECTION = 162 - WHILEEXEC_DEVICE_LIMITATION = 163 - WHILEEXEC_DOOR_IS_OPENED = 164 - WHILEEXEC_MAINTENANCE_REQUIRED = 165 - WHILEEXEC_OTHER = 166 - PRIORITY_LOCK__LOCAL_USER = 167 - PRIORITY_LOCK__USER = 168 - PRIORITY_LOCK__RAIN = 169 - PRIORITY_LOCK__TIMER = 170 - PRIORITY_LOCK__SECURITY = 171 - PRIORITY_LOCK__UPS = 172 - PRIORITY_LOCK__SFC = 173 - PRIORITY_LOCK__LSC = 174 - PRIORITY_LOCK__SAAC = 175 - PRIORITY_LOCK__WIND = 176 - PRIORITY_LOCK__EXTERNAL_ACCESS = 177 - PRIORITY_LOCK__EMERGENCY = 178 - NO_DISTANT_FOR_DISCOVER = 179 - ANOTHER_COMMAND_IS_RUNNING = 180 - PROBLEM_WITH_BOILER_COMMUNICATION = 181 - LOCKED_BY_RCM = 182 - RCM_NO_REMOTE_CONTROL = 183 - DISCOVER_NO_REMOTE_CONTROLLER_ERROR = 184 - COMMAND_INTERRUPTED = 185 - PRIORITY_LOCK__WIND_FORCING_AVAILABLE = 190 - PRIORITY_LOCK__WIND_FORCING_UNAVAILABLE = 191 - PRIORITY_LOCK__NO_SECURITY_DEVICE = 192 - PRIORITY_LOCK__DEAD_SENSOR = 193 - PRIORITY_LOCK__UNKNOWN_ERROR = 194 - CMDDEPRECATED = 197 - DBUS_ERROR = 200 - DBUS_NO_MEMORY = 201 - DBUS_SERVICE_UNKNOWN = 202 - DBUS_NAME_HAS_NO_OWNER = 203 - DBUS_NO_REPLY = 204 - DBUS_IO_ERROR = 205 - DBUS_BAD_ADDRESS = 206 - DBUS_NOT_SUPPORTED = 207 - DBUS_LIMITS_EXCEEDED = 208 - DBUS_ACCESS_DENIED = 209 - DBUS_AUTH_FAILED = 210 - DBUS_NO_SERVER = 211 - DBUS_TIMEOUT = 212 - DBUS_NO_NETWORK = 213 - DBUS_ADDRESS_IN_USE = 214 - DBUS_DISCONNECTED = 215 - DBUS_INVALID_ARGS = 216 - DBUS_FILE_NOT_FOUND = 217 - DBUS_FILE_EXISTS = 218 - DBUS_UNKNOWN_METHOD = 219 - DBUS_UNKNOWN_OBJECT = 220 - DBUS_UNKNOWN_INTERFACE = 221 - DBUS_UNKNOWN_PROPERTY = 222 - DBUS_PROPERTY_READ_ONLY = 223 - DBUS_TIMED_OUT = 224 - DBUS_MATCH_RULE_NOT_FOUND = 225 - DBUS_MATCH_RULE_INVALID = 226 - DBUS_SPAWN_EXEC_FAILED = 227 - DBUS_SPAWN_FORK_FAILED = 228 - DBUS_SPAWN_CHILD_EXITED = 229 - DBUS_SPAWN_CHILD_SIGNALED = 230 - DBUS_SPAWN_FAILED = 231 - DBUS_SPAWN_SETUP_FAILED = 232 - DBUS_SPAWN_CONFIG_INVALID = 233 - DBUS_SPAWN_SERVICE_INVALID = 234 - DBUS_SPAWN_SERVICE_NOT_FOUND = 235 - DBUS_SPAWN_PERMISSIONS_INVALID = 236 - DBUS_SPAWN_FILE_INVALID = 237 - DBUS_SPAWN_NO_MEMORY = 238 - DBUS_UNIX_PROCESS_ID_UNKNOWN = 239 - DBUS_INVALID_SIGNATURE = 240 - DBUS_INVALID_FILE_CONTENT = 241 - DBUS_SELINUX_SECURITY_CONTEXT_UNKNOWN = 242 - DBUS_ADT_AUDIT_DATA_UNKNOWN = 243 - DBUS_OBJECT_PATH_IN_USE = 244 - DBUS_INCONSISTENT_MESSAGE = 245 - NOT_IMPLEMENTED_YET = 300 - MODULE_NOT_LOADED = 301 - APPLICATION_NOT_RUNNING = 302 - NONEXEC_MANUALLY_CONTROLLED = 400 - NONEXEC_AUTOMATIC_CYCLE = 401 - NONEXEC_BATTERY_LEVEL = 402 - NONEXEC_WRONG_LOAD_CONNECTED = 403 - NONEXEC_HIGH_CONSUMPTION = 404 - NONEXEC_LOW_CONSUMPTION = 405 - NONEXEC_COLOUR_NOT_REACHABLE = 406 - NONEXEC_USER_ACTION_NEEDED = 407 - NONEXEC_COMMAND_INCOMPATIBLE_WITH_MOVEMENT = 408 - NONEXEC_CANNOT_CHANGE_STATE = 409 - NONEXEC_FILTER_MAINTENANCE = 410 - NONEXEC_OPERATING_MODE_NOT_SUPPORTED = 411 - WHILEEXEC_MANUALLY_CONTROLLED = 420 - WHILEEXEC_AUTOMATIC_CYCLE = 421 - WHILEEXEC_BATTERY_LEVEL = 422 - WHILEEXEC_WRONG_LOAD_CONNECTED = 423 - WHILEEXEC_HIGH_CONSUMPTION = 424 - WHILEEXEC_LOW_CONSUMPTION = 425 - WHILEEXEC_COLOUR_NOT_REACHABLE = 426 - WHILEEXEC_USER_ACTION_NEEDED = 427 - WHILEEXEC_COMMAND_INCOMPATIBLE_WITH_MOVEMENT = 428 - WHILEEXEC_CANNOT_CHANGE_STATE = 429 - WHILEEXEC_FILTER_MAINTENANCE = 430 - WHILEEXEC_OPERATING_MODE_NOT_SUPPORTED = 431 - OVERRIDEMODE_ERROR = 450 - CAMERA_INVALID_CREDENTIALS = 500 - UNSUPPORTED_CAMERA_TYPE = 501 - NETWORK_COULDNT_RESOLVE_HOST = 601 - NETWORK_COULDNT_CONNECT = 602 - NETWORK_OPERATION_TIMEDOUT = 603 - LPB_APP_OUT_OF_RANGE = 701 - LPB_APP_OUT_OF_MAXRANGE = 702 - LPB_APP_OUT_OF_MINRANGE = 703 - LPB_APP_MEMORY_ERROR = 704 - LPB_APP_READ_ONLY = 705 - LPB_APP_ILLEGAL_CMD = 706 - LPB_APP_VOID_DP = 707 - LPB_APP_TYPE_CONFLICT = 708 - LPB_APP_READ_CMD_INCORRECT = 709 - LPB_APP_WRITE_CMD_INCORRECT = 710 - LPB_APP_CMD_TYPE_INCORRECT = 711 - LPB_APP_WRITE_TIMEOUT = 712 - LPB_APP_CANNOT_WRITE_GW = 713 - LPB_APP_UNKNOWN_GATEWAY = 714 - LPB_APP_GATEWAY_UNREACHABLE = 715 - APPLICATION_ERROR = 800 - HUE_INVALID_CREDENTIALS = 900 - HUE_LINK_BUTTON_NOT_PRESSED = 901 - HUE_DEVICE_IS_OFF = 902 - TIMED_OUT = 10001 - CANCELLED = 10002 - UNKNOWN_ERROR_CODE = 10003 - SERVER_FAILURE = 10004 - PEER_DOWN = 10005 - GATEWAY_BUFFER_OVERFLOW = 10006 - UNKNOWN_DETAILED_ERROR = 10007 - TIME_OUT_ON_TRANSMIT = 20001 - TIME_OUT_ON_TRANSMITTED_COMMAND = 20002 - TIME_OUT_ON_COMMAND_PROGRESS = 20003 - DEVICE_NO_ANSWER = 60004 - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - -@unique -class EventName(StrEnum): - UNKNOWN = "Unknown" - ACTION_GROUP_CREATED = "ActionGroupCreatedEvent" - ACTION_GROUP_DELETED = "ActionGroupDeletedEvent" - ACTION_GROUP_UPDATED = "ActionGroupUpdatedEvent" - CALENDAR_DAY_CREATED = "CalendarDayCreatedEvent" - CALENDAR_DAY_UPDATED = "CalendarDayUpdatedEvent" - CALENDAR_RULE_CREATED = "CalendarRuleCreatedEvent" - CALENDAR_RULE_DELETED = "CalendarRuleDeletedEvent" - CALENDAR_RULE_UPDATED = "CalendarRuleUpdatedEvent" - CAMERA_DISCOVERED = "CameraDiscoveredEvent" - CAMERA_DISCOVER_FAILED = "CameraDiscoverFailedEvent" - CAMERA_UPLOAD_PHOTO = "CameraUploadPhotoEvent" - CLOUD_SYNCHRONIZATION_FAILED = "CloudSynchronizationFailedEvent" - COMMAND_EXECUTION_STATE_CHANGE = "CommandExecutionStateChangedEvent" - CONDITION_GROUP_CREATED = "ConditionGroupCreatedEvent" - CONDITION_GROUP_DELETED = "ConditionGroupDeletedEvent" - CONDITION_GROUP_UPDATED = "ConditionGroupUpdatedEvent" - DELAYED_TRIGGER_CANCELLED = "DelayedTriggerCancelledEvent" - DELAYED_TRIGGER_SET_EVENT = "DelayedTriggerSetEvent" - DEVICE_ATTRIBUTE_CHANGED = "DeviceAttributeChangedEvent" - DEVICE_AVAILABLE = "DeviceAvailableEvent" - DEVICE_CREATED = "DeviceCreatedEvent" - DEVICE_DELETION_FAILED = "DeviceDeletionFailedEvent" - DEVICE_DISABLED = "DeviceDisabledEvent" - DEVICE_EVENT = "DeviceEventEvent" - DEVICE_FIRMWARE_UPDATE_COMPLETED = "DeviceFirmwareUpdateCompletedEvent" - DEVICE_FIRMWARE_UPDATE_FAILED = "DeviceFirmwareUpdateFailedEvent" - DEVICE_PROTOCOL_AVAILABLE = "DeviceProtocolAvailableEvent" - DEVICE_PROTOCOL_UNAVAILABLE = "DeviceProtocolUnavailableEvent" - DEVICE_REMOVED = "DeviceRemovedEvent" - DEVICE_SHORTCUT_UPDATED = "DeviceShortcutUpdatedEvent" - DEVICE_STATE_CHANGED = "DeviceStateChangedEvent" - DEVICE_STATE_HISTORY_RESULT = "DeviceStateHistoryResultEvent" - DEVICE_UNAVAILABLE = "DeviceUnavailableEvent" - DEVICE_UPDATED = "DeviceUpdatedEvent" - DISCOVER_COMPLETE = "DiscoverCompleteEvent" - DISCOVER_FAILED = "DiscoverFailedEvent" - DOWNLOAD_RECORDED_FILE = "DownloadRecordedFileEvent" - ELIOT_DISCOVER_GATEWAYS_COMPLETED = "EliotDiscoverGatewaysCompletedEvent" - ELIOT_DISCOVER_GATEWAYS_FAILED = "EliotDiscoverGatewaysFailedEvent" - ELIOT_DISCOVER_GATEWAY_COMPLETED = "EliotDiscoverGatewayCompletedEvent" - ELIOT_DISCOVER_GATEWAY_FAILED = "EliotDiscoverGatewayFailedEvent" - ELIOT_REFRESH_CURRENT_TOKEN_COMPLETED = "EliotRefreshCurrentTokenCompletedEvent" - ELIOT_REFRESH_CURRENT_TOKEN_FAILED = "EliotRefreshCurrentTokenFailedEvent" - EMAIL_NOTIFICATION = "EmailNotificationEvent" - END_USER_ACCOUNT_CREATED = "EndUserAccountCreatedEvent" - END_USER_ACCOUNT_DELETED = "EndUserAccountDeletedEvent" - END_USER_ACCOUNT_UPDATED = "EndUserAccountUpdatedEvent" - END_USER_LOGIN = "EndUserLoginEvent" - END_USER_PREFERENCE_CREATED = "EndUserPreferenceCreatedEvent" - END_USER_PREFERENCE_DELETED = "EndUserPreferenceDeletedEvent" - END_USER_PREFERENCE_UPDATED = "EndUserPreferenceUpdatedEvent" - ENOCEAN_BAD_DEVICE_STIMULATION = "EnOceanBadDeviceStimulationEvent" - ENOCEAN_KNOWN_DEVICE_FOUND = "EnOceanKnownDeviceFoundEvent" - ENOCEAN_LEARN_STARTED = "EnOceanLearnStartedEvent" - ENOCEAN_LEARN_STOPPED = "EnOceanLearnStoppedEvent" - EXECUTION_HISTORY_DELETED = "ExecutionHistoryDeletedEvent" - EXECUTION_REGISTERED = "ExecutionRegisteredEvent" - EXECUTION_STATE_CHANGED = "ExecutionStateChangedEvent" - EXTERNAL_SERVICE_CREDENTIALS = "ExternalServiceCredentialsEvent" - EXTERNAL_SERVICE_URL = "ExternalServiceURLEvent" - GATEWAY_ACTIVATED = "GatewayActivatedEvent" - GATEWAY_ACTIVE_PROTOCOLS_CHANGED = "GatewayActiveProtocolsChangedEvent" - GATEWAY_ALIVE = "GatewayAliveEvent" - GATEWAY_ASSOCIATED = "GatewayAssociatedEvent" - GATEWAY_ATTACHED = "GatewayAttachedEvent" - GATEWAY_BOOT = "GatewayBootEvent" - GATEWAY_DEACTIVATED = "GatewayDeactivatedEvent" - GATEWAY_DETACHED = "GatewayDetachedEvent" - GATEWAY_DISSOCIATED = "GatewayDissociatedEvent" - GATEWAY_DOWN = "GatewayDownEvent" - GATEWAY_FIRMWARE_UPDATED = "GatewayFirmwareUpdatedEvent" - GATEWAY_FIRMWARE_UPDATE_COMPLETED = "GatewayFirmwareUpdateCompletedEvent" - GATEWAY_FUNCTION_CHANGED = "GatewayFunctionChangedEvent" - GATEWAY_MIGRATED = "GatewayMigratedEvent" - GATEWAY_MODE_CHANGED = "GatewayModeChangedEvent" - GATEWAY_PLACE_UPDATED = "GatewayPlaceUpdatedEvent" - GATEWAY_PROTOCOL_DOWN = "GatewayProtocolDownEvent" - GATEWAY_PROTOCOL_READY = "GatewayProtocolReadyEvent" - GATEWAY_SYNCHRONIZATION_ENDED = "GatewaySynchronizationEndedEvent" - GATEWAY_SYNCHRONIZATION_FAILED = "GatewaySynchronizationFailedEvent" - GATEWAY_SYNCHRONIZATION_STARTED = "GatewaySynchronizationStartedEvent" - GATEWAY_TIME_RELIABILITY_CHANGED = "GatewayTimeReliabilityChangedEvent" - GLOBAL_EVENT = "GlobalEventEvent" - GROUP_TRIGGERED = "GroupTriggeredEvent" - HISTORY_DELETED = "HistoryDeletedEvent" - INVALID_ADDRESS = "InvalidAddressEvent" - IO_CHANGED_KEY = "IOChangedKeyEvent" - LIST_RECORDED_FILES = "ListRecordedFilesEvent" - LOCATION_UPDATED = "LocationUpdatedEvent" - MAIN_ACCOUNT_UPDATED = "MainAccountUpdatedEvent" - NEW_DEVICE_FOUND = "NewDeviceFoundEvent" - NOTIFICATION_EVENT_LOG = "NotificationEventLogEvent" - OGP = "OGPEvent" - OPENDOORS_DISCOVER_COMPLETED = "OpenDoorsDiscoverCompletedEvent" - OPENDOORS_DISCOVER_FAILED = "OpenDoorsDiscoverFailedEvent" - OPENDOORS_GENERATE_OAUTH_TOKENS_COMPLETED = ( - "OpenDoorsGenerateOAuthTokensCompletedEvent" - ) - OPENDOORS_GENERATE_OAUTH_TOKENS_FAILED = "OpenDoorsGenerateOAuthTokensFailedEvent" - PLACE_CREATED = "PlaceCreatedEvent" - PLACE_DELETED = "PlaceDeletedEvent" - PLACE_UPDATED = "PlaceUpdatedEvent" - PURGE_PARTIAL_RAW_DEVICES = "PurgePartialRawDevicesEvent" - PUSH_NOTIFICATION = "PushNotificationEvent" - PUSH_SUBSCRIPTION_CREATED = "PushSubscriptionCreatedEvent" - PUSH_SUBSCRIPTION_DELETED = "PushSubscriptionDeletedEvent" - REAL_TIME_SETUP_NOTIFICATION = "RealTimeSetupNotificationEvent" - REFRESH_ALL_DEVICES_STATES_COMPLETED = "RefreshAllDevicesStatesCompletedEvent" - REFRESH_ALL_DEVICES_STATES_FAILED = "RefreshAllDevicesStatesFailedEvent" - RESELLER_DELEGATION = "ResellerDelegationEvent" - SECONDARY_ACCOUNT_UPDATED = "SecondaryAccountUpdatedEvent" - SETUP_JOB_COMPLETED = "SetupJobCompletedEvent" - SETUP_JOB_FAILED = "SetupJobFailedEvent" - SETUP_QUOTA_UPDATED = "SetupQuotaUpdatedEvent" - SETUP_TRIGGER_CREATED = "SetupTriggerCreatedEvent" - SETUP_TRIGGER_CREATION_FAILED = "SetupTriggerCreationFailedEvent" - SETUP_TRIGGER_DELETED = "SetupTriggerDeletedEvent" - SETUP_TRIGGER_DELETION_FAILED = "SetupTriggerDeletionFailedEvent" - SETUP_TRIGGER_MODE_CHANGED = "SetupTriggerModeChangedEvent" - SETUP_TRIGGER_MODE_CHANGE_FAILED = "SetupTriggerModeChangeFailedEvent" - SETUP_TRIGGER_TRIGGERED = "SetupTriggerTriggeredEvent" - SETUP_TRIGGER_UPDATED = "SetupTriggerUpdatedEvent" - SETUP_TRIGGER_UPDATE_FAILED = "SetupTriggerUpdateFailedEvent" - SMS_NOTIFICATION = "SmsNotificationEvent" - SOMFY_PROTECT_CURRENT_TOKEN_COMPLETED = "SomfyProtectCurrentTokenCompletedEvent" - SOMFY_PROTECT_CURRENT_TOKEN_FAILED = "SomfyProtectCurrentTokenFailedEvent" - SOMFY_PROTECT_GET_SITES_COMPLETED = "SomfyProtectGetSitesCompletedEvent" - SOMFY_THERMOSTAT_DISCOVER_FAILED = "SomfyThermostatDiscoverFailedEvent" - SOMFY_THERMOSTAT_GATEWAY_DISCOVER_FAILED = ( - "SomfyThermostatGatewayDiscoverFailedEvent" - ) - SOMFY_THERMOSTAT_REGISTER_WEBHOOK_FAILED = ( - "SomfyThermostatRegisterWebhookFailedEvent" - ) - SOMFY_THERMOSTAT_REGISTER_WEBHOOK_SUCCESS = ( - "SomfyThermostatRegisterWebhookSuccessEvent" - ) - SOMFY_THERMOSTAT_TOKENS_COMPLETED = "SomfyThermostatCurrentTokensCompletedEvent" - SOMFY_THERMOSTAT_TOKENS_FAILED = "SomfyThermostatCurrentTokensFailedEvent" - SOMFY_THERMOSTAT_TOKENS_GENERATION_FAILED = ( - "SomfyThermostatGenerateOAuthTokensFailedEvent" - ) - SOMFY_THERMOSTAT_TOKENS_GENERATION_SUCCESS = ( - "SomfyThermostatGenerateOAuthTokensCompletedEvent" - ) - SONOS_GET_TOPOLOGY_FAILED = "SonosGetTopologyFailedEvent" - SONOS_GET_TOPOLOGY_SUCCESS = "SonosGetTopologySuccessEvent" - SONOS_TOPOLOGY_CHANGED = "SonosTopologyChangedEvent" - TOKEN_CREATED = "TokenCreatedEvent" - TOKEN_CREATION_FAILED = "TokenCreationFailedEvent" - TOKEN_DELETION_FAILED = "TokenDeletionFailedEvent" - TOKEN_REMOVED = "TokenRemovedEvent" - TRIGGER_EXPIRED = "TriggerExpiredEvent" - UNPN_CONTROL_CURRENT_ACTIONS = "UPnPControlCurrentActionsEvent" - UNPN_CONTROL_GROUP_MUTE_STATUS = "UPnPControlGroupMuteStatusEvent" - UNPN_CONTROL_GROUP_VOLUME = "UPnPControlGroupVolumeEvent" - UNPN_CONTROL_MEDIA_INFO = "UPnPControlMediaInfoEvent" - UNPN_CONTROL_POSITION_INFO = "UPnPControlPositionInfoEvent" - UNPN_CONTROL_SONOS_FAVOURITES = "UPnPControlSonosFavoritesEvent" - UNPN_CONTROL_TRANSPORT_INFO = "UPnPControlTransportInfoEvent" - UNPN_DISCOVER_COMPLETE = "UPNPDiscoverCompleteEvent" - UNPN_DISCOVER_FAILED = "UPNPDiscoverFailedEvent" - VALID_ADDRESS = "ValidAddressEvent" - ZIGBEE_BIND_NETWORK_COMPLETED = "ZigbeeBindNetworkCompletedEvent" - ZIGBEE_BIND_NETWORK_FAILED = "ZigbeeBindNetworkFailedEvent" - ZIGBEE_CREATE_NETWORK_COMPLETED = "ZigbeeCreateNetworkCompletedEvent" - ZIGBEE_CREATE_NETWORK_FAILED = "ZigbeeCreateNetworkFailedEvent" - ZIGBEE_JOIN_NETWORK_FAILED = "ZigbeeJoinNetworkFailedEvent" - ZIGBEE_LEAVE_NETWORK_COMPLETED = "ZigbeeLeaveNetworkCompletedEvent" - ZIGBEE_LEAVE_NETWORK_FAILED = "ZigbeeLeaveNetworkFailedEvent" - ZIGBEE_REFRESH_NETWORK_COMPLETED = "ZigbeeRefreshNetworkCompletedEvent" - ZONE_CREATED = "ZoneCreatedEvent" - ZONE_DELETED = "ZoneDeletedEvent" - ZONE_UPDATED = "ZoneUpdatedEvent" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN diff --git a/pyoverkiz/enums/measured_value_type.py b/pyoverkiz/enums/measured_value_type.py deleted file mode 100644 index 8f99810..0000000 --- a/pyoverkiz/enums/measured_value_type.py +++ /dev/null @@ -1,51 +0,0 @@ -import sys -from enum import unique - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class MeasuredValueType(StrEnum): - """See https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode/issues/68#issuecomment-1281474879""" - - ABSOLUTE_VALUE = "core:AbsoluteValue" - ANGLE_IN_DEGREES = "core:AngleInDegrees" - ANGULAR_SPEED_IN_DEGREES_PER_SECOND = "core:AngularSpeedInDegreesPerSecond" - ELECTRICAL_ENERGY_IN_KWH = "core:ElectricalEnergyInKWh" - ELECTRICAL_ENERGY_IN_WH = "core:ElectricalEnergyInWh" - ELECTRICAL_POWER_IN_KW = "core:ElectricalPowerInKW" - ELECTRICAL_POWER_IN_W = "core:ElectricalPowerInW" - ELECTRIC_CURRENT_IN_AMPERE = "core:ElectricCurrentInAmpere" - ELECTRIC_CURRENT_IN_MILLI_AMPERE = "core:ElectricCurrentInMilliAmpere" - ENERGY_IN_CAL = "core:EnergyInCal" - ENERGY_IN_KCAL = "core:EnergyInkCal" - FLOW_IN_LITRE_PER_SECOND = "core:FlowInLitrePerSecond" - FLOW_IN_METER_CUBE_PER_HOUR = "core:FlowInMeterCubePerHour" - FLOW_IN_METER_CUBE_PER_SECOND = "core:FlowInMeterCubePerSecond" - FOSSIL_ENERGY_IN_WH = "core:FossilEnergyInWh" - GRADIENT_IN_PERCENTAGE_PER_SECOND = "core:GradientInPercentagePerSecond" - LENGTH_IN_METER = "core:LengthInMeter" - LINEAR_SPEED_IN_METER_PER_SECOND = "core:LinearSpeedInMeterPerSecond" - LUMINANCE_IN_LUX = "core:LuminanceInLux" - PARTS_PER_BILLION = "core:PartsPerBillion" - PARTS_PER_MILLION = "core:PartsPerMillion" - PARTS_PER_QUADRILLION = "core:PartsPerQuadrillion" - PARTS_PER_TRILLION = "core:PartsPerTrillion" - POWER_PER_SQUARE_METER = "core:PowerPerSquareMeter" - PRESSURE_IN_HPA = "core:PressureInHpa" - PRESSURE_IN_MILLI_BAR = "core:PressureInMilliBar" - RELATIVE_VALUE_IN_PERCENTAGE = "core:RelativeValueInPercentage" - TEMPERATURE_IN_CELCIUS = "core:TemperatureInCelcius" - TEMPERATURE_IN_KELVIN = "core:TemperatureInKelvin" - TIME_IN_SECOND = "core:TimeInSecond" - VECTOR_COORDINATE = "core:VectorCoordinate" - VOLTAGE_IN_MILLI_VOLT = "core:VoltageInMilliVolt" - VOLTAGE_IN_VOLT = "core:VoltageInVolt" - VOLUME_IN_CUBIC_METER = "core:VolumeInCubicMeter" - VOLUME_IN_GALLON = "core:VolumeInGallon" - VOLUME_IN_LITER = "core:VolumeInLiter" diff --git a/pyoverkiz/enums/protocol.py b/pyoverkiz/enums/protocol.py deleted file mode 100644 index ccc5a71..0000000 --- a/pyoverkiz/enums/protocol.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -import sys -from enum import unique - -_LOGGER = logging.getLogger(__name__) - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class Protocol(StrEnum): - """Protocol used by Overkiz. - - Values have been retrieved from /reference/protocolTypes - """ - - UNKNOWN = "unknown" - IO = "io" - RTS = "rts" - RTD = "rtd" - RTDS = "rtds" - RAMSES = "ramses" - ENOCEAN = "enocean" - ZWAVE = "zwave" - CAMERA = "camera" - OVP = "ovp" - MODBUS = "modbus" - MODBUSLINK = "modbuslink" - HUE = "hue" - VERISURE = "verisure" - INTERNAL = "internal" - JSW = "jsw" - OPENDOORS = "opendoors" - MYFOX = "myfox" - SOMFY_THERMOSTAT = "somfythermostat" - ZIGBEE = "zigbee" - UPNP_CONTROL = "upnpcontrol" - ELIOT = "eliot" - WISER = "wiser" - PROFALUX_868 = "profalux868" - OGP = "ogp" - HOMEKIT = "homekit" - AUGUST = "august" - HLRR_WIFI = "hlrrwifi" - RTN = "rtn" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported protocol {value} has been returned for {cls}") - return cls.UNKNOWN diff --git a/pyoverkiz/enums/server.py b/pyoverkiz/enums/server.py deleted file mode 100644 index df6ae2c..0000000 --- a/pyoverkiz/enums/server.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys -from enum import unique - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class APIType(StrEnum): - CLOUD = "cloud" - LOCAL = "local" - - -@unique -class Server(StrEnum): - ATLANTIC_COZYTOUCH = "atlantic_cozytouch" - BRANDT = "brandt" - FLEXOM = "flexom" - HEXAOM_HEXACONNECT = "hexaom_hexaconnect" - HI_KUMO_ASIA = "hi_kumo_asia" - HI_KUMO_EUROPE = "hi_kumo_europe" - HI_KUMO_OCEANIA = "hi_kumo_oceania" - NEXITY = "nexity" - REXEL = "rexel" - SIMU_LIVEIN2 = "simu_livein2" - SOMFY_DEVELOPER_MODE = "somfy_developer_mode" - SOMFY_EUROPE = "somfy_europe" - SOMFY_AMERICA = "somfy_america" - SOMFY_OCEANIA = "somfy_oceania" - UBIWIZZ = "ubiwizz" diff --git a/pyoverkiz/enums/state.py b/pyoverkiz/enums/state.py deleted file mode 100644 index 7240008..0000000 --- a/pyoverkiz/enums/state.py +++ /dev/null @@ -1,275 +0,0 @@ -import sys -from enum import unique - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class OverkizAttribute(StrEnum): - """Device attributes used by Overkiz.""" - - CORE_FIRMWARE_REVISION = "core:FirmwareRevision" - CORE_MANUFACTURER = "core:Manufacturer" - CORE_MANUFACTURER_REFERENCE = "core:ManufacturerReference" - CORE_MAX_SETTABLE_VALUE = "core:MaxSettableValue" - CORE_MEASURED_VALUE_TYPE = "core:MeasuredValueType" - CORE_MIN_SETTABLE_VALUE = "core:MinSettableValue" - CORE_ELECTRIC_POWER_CONSUMPTION_STATE_MEASURED_VALUE_TYPE = ( - "core:ElectricPowerConsumptionStateMeasuredValueType" - ) - CORE_SUPPORTED_ALIASES = "core:SupportedAliases" - CORE_SUPPORTED_OPTIONS = "core:SupportedOptions" - CORE_TECHNOLOGY = "core:Technology" - HOMEKIT_SETUP_CODE = "homekit:SetupCode" - OGP_FEATURES = "ogp:Features" - - -@unique -class OverkizState(StrEnum): - """Device states used by Overkiz.""" - - CORE_ACTIVE_ZONES = "core:ActiveZonesState" - CORE_ACTIVATED_OPTIONS = "core:ActivatedOptionsState" - CORE_ASSEMBLY = "core:AssemblyState" - CORE_AUTO_MANU_MODE = "core:AutoManuModeState" - CORE_AVAILABILITY = "core:AvailabilityState" - CORE_BATTERY = "core:BatteryState" - CORE_BATTERY_LEVEL = "core:BatteryLevelState" - CORE_BLUE_COLOR_INTENSITY = "core:BlueColorIntensityState" - CORE_BOOST_ELECTRIC_POWER_CONSUMPTION = "core:BoostElectricPowerConsumptionState" - CORE_BOOST_END_DATE = "core:BoostEndDateState" - CORE_BOOST_MODE_DURATION = "core:BoostModeDurationState" - CORE_BOOST_ON_OFF = "core:BoostOnOffState" - CORE_BOOST_START_DATE = "core:BoostStartDateState" - CORE_BOTTOM_TANK_WATER_TEMPERATURE = "core:BottomTankWaterTemperatureState" - CORE_CLOSURE = "core:ClosureState" - CORE_CLOSURE_OR_ROCKER_POSITION = "core:ClosureOrRockerPositionState" - CORE_CLOUD_DEVICE_STATUS = "core:CloudDeviceStatusState" - CORE_CO2_CONCENTRATION = "core:CO2ConcentrationState" - CORE_COMFORT_HEATING_TARGET_TEMPERATURE = ( - "core:ComfortHeatingTargetTemperatureState" - ) - CORE_COMFORT_ROOM_TEMPERATURE = "core:ComfortRoomTemperatureState" - CORE_COMFORT_TARGET_DWH_TEMPERATURE = "core:ComfortTargetDHWTemperatureState" - CORE_CONSUMPTION_TARIFF1 = "core:ConsumptionTariff1State" - CORE_CONSUMPTION_TARIFF2 = "core:ConsumptionTariff2State" - CORE_CONSUMPTION_TARIFF3 = "core:ConsumptionTariff3State" - CORE_CONSUMPTION_TARIFF4 = "core:ConsumptionTariff4State" - CORE_CONSUMPTION_TARIFF5 = "core:ConsumptionTariff5State" - CORE_CONSUMPTION_TARIFF6 = "core:ConsumptionTariff6State" - CORE_CONSUMPTION_TARIFF7 = "core:ConsumptionTariff7State" - CORE_CONSUMPTION_TARIFF8 = "core:ConsumptionTariff8State" - CORE_CONSUMPTION_TARIFF9 = "core:ConsumptionTariff9State" - CORE_CONTACT = "core:ContactState" - CORE_CONTROL_WATER_TARGET_TEMPERATURE = "core:ControlWaterTargetTemperatureState" - CORE_CO_CONCENTRATION = "core:COConcentrationState" - CORE_DEPLOYMENT = "core:DeploymentState" - CORE_DEROGATED_TARGET_TEMPERATURE = "core:DerogatedTargetTemperatureState" - CORE_DEROGATION_ACTIVATION = "core:DerogationActivationState" - CORE_DEROGATION_ON_OFF = "core:DerogationOnOffState" - CORE_DHW_DEROGATION_AVAILABILITY = "core:DHWDerogationAvailabilityState" - CORE_DHW_TEMPERATURE = "core:DHWTemperatureState" - CORE_DISCRETE_RSSI_LEVEL = "core:DiscreteRSSILevelState" - CORE_DWH_ON_OFF = "core:DHWOnOffState" - CORE_ECO_HEATING_TARGET_TEMPERATURE = "core:EcoHeatingTargetTemperatureState" - CORE_ECO_ROOM_TEMPERATURE = "core:EcoRoomTemperatureState" - CORE_ECO_TARGET_DWH_TEMPERATURE = "core:EcoTargetDHWTemperatureState" - CORE_ELECTRIC_ENERGY_CONSUMPTION = "core:ElectricEnergyConsumptionState" - CORE_ELECTRIC_POWER_CONSUMPTION = "core:ElectricPowerConsumptionState" - CORE_EXPECTED_NUMBER_OF_SHOWER = "core:ExpectedNumberOfShowerState" - CORE_FIRMWARE_REVISION = "core:FirmwareRevision" - CORE_FOSSIL_ENERGY_CONSUMPTION = "core:FossilEnergyConsumptionState" - CORE_GAS_CONSUMPTION = "core:GasConsumptionState" - CORE_GAS_DETECTION = "core:GasDetectionState" - CORE_GREEN_COLOR_INTENSITY = "core:GreenColorIntensityState" - CORE_HOLIDAYS_MODE = "core:HolidaysModeState" - CORE_HEATING_ON_OFF = "core:HeatingOnOffState" - CORE_HEATING_STATUS = "core:HeatingStatusState" - CORE_HEATING_TARGET_TEMPERATURE = "core:HeatingTargetTemperatureState" - CORE_INTRUSION = "core:IntrusionState" - CORE_LEVEL = "core:LevelState" - CORE_LIGHT_INTENSITY = "core:LightIntensityState" - CORE_LOCKED_UNLOCKED = "core:LockedUnlockedState" - CORE_LUMINANCE = "core:LuminanceState" - CORE_MANUFACTURER_NAME = "core:ManufacturerNameState" - CORE_MIN_SETPOINT = "core:MinSetpointState" - CORE_MAX_SETPOINT = "core:MaxSetpointState" - CORE_MAXIMAL_SHOWER_MANUAL_MODE = "core:MaximalShowerManualModeState" - CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE = "core:MaximalTemperatureManualModeState" - CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE = ( - "core:MaximumHeatingTargetTemperatureState" - ) - CORE_MAXIMUM_TEMPERATURE = "core:MaximumTemperatureState" - CORE_MEMORIZED_1_POSITION = "core:Memorized1PositionState" - CORE_MIDDLE_WATER_TEMPERATURE_IN = "core:MiddleWaterTemperatureInState" - CORE_MINIMAL_SHOWER_MANUAL_MODE = "core:MinimalShowerManualModeState" - CORE_MINIMAL_TEMPERATURE_MANUAL_MODE = "core:MinimalTemperatureManualModeState" - CORE_MINIMUM_HEATING_TARGET_TEMPERATURE = ( - "core:MinimumHeatingTargetTemperatureState" - ) - CORE_MINIMUM_TEMPERATURE = "core:MinimumTemperatureState" - CORE_MODEL = "core:ModelState" - CORE_MOVING = "core:MovingState" - CORE_NAME = "core:NameState" - CORE_NUMBER_OF_SHOWER_REMAINING = "core:NumberOfShowerRemainingState" - CORE_NUMBER_OF_TANK = "core:NumberOfTankState" - CORE_OCCUPANCY = "core:OccupancyState" - CORE_ON_OFF = "core:OnOffState" - CORE_OPEN_CLOSED = "core:OpenClosedState" - CORE_OPEN_CLOSED_PARTIAL = "core:OpenClosedPartialState" - CORE_OPEN_CLOSED_PEDESTRIAN = "core:OpenClosedPedestrianState" - CORE_OPEN_CLOSED_UNKNOWN = "core:OpenClosedUnknownState" - CORE_OPEN_CLOSED_VALVE = "core:OpenClosedValveState" - CORE_OPEN_WINDOW_DETECTION_ACTIVATION = "core:OpenWindowDetectionActivationState" - CORE_OPERATING_MODE = "core:OperatingModeState" - CORE_PEDESTRIAN_POSITION = "core:PedestrianPositionState" - CORE_POWER_HEAT_ELECTRICAL_IN = "core:PowerHeatElectricalInState" - CORE_POWER_SUPPLY_UP_DOWN = "core:PowerSupplyUpDownState" - CORE_PRIORITY_LOCK_TIMER = "core:PriorityLockTimerState" - CORE_PRODUCT_MODEL_NAME = "core:ProductModelNameState" - CORE_PROGRAMMING_AVAILABLE = "core:ProgrammingAvailableState" - CORE_RAIN = "core:RainState" - CORE_RED_COLOR_INTENSITY = "core:RedColorIntensityState" - CORE_RELATIVE_HUMIDITY = "core:RelativeHumidityState" - CORE_REMAINING_HOT_WATER = "core:RemainingHotWaterState" - CORE_RSSI_LEVEL = "core:RSSILevelState" - CORE_SECURED_POSITION_TEMPERATURE = "core:SecuredPositionTemperatureState" - CORE_SENSOR_DEFECT = "core:SensorDefectState" - CORE_SLATE_ORIENTATION = "core:SlateOrientationState" - CORE_SLATS_OPEN_CLOSED = "core:SlatsOpenClosedState" - CORE_SLATS_ORIENTATION = "core:SlatsOrientationState" - CORE_SMOKE = "core:SmokeState" - CORE_STATUS = "core:StatusState" - CORE_STOP_RELAUNCH = "core:StopRelaunchState" - CORE_SUN_ENERGY = "core:SunEnergyState" - CORE_TARGET_CLOSURE = "core:TargetClosureState" - CORE_TARGET_DWH_TEMPERATURE = "core:TargetDHWTemperatureState" - CORE_TARGET_TEMPERATURE = "core:TargetTemperatureState" - CORE_TEMPERATURE = "core:TemperatureState" - CORE_THERMAL_CONFIGURATION = "core:ThermalConfigurationState" - CORE_THERMAL_ENERGY_CONSUMPTION = "core:ThermalEnergyConsumptionState" - CORE_THREE_WAY_HANDLE_DIRECTION = "core:ThreeWayHandleDirectionState" - CORE_V40_WATER_VOLUME_ESTIMATION = "core:V40WaterVolumeEstimationState" - CORE_VIBRATION = "core:VibrationState" - CORE_WATER_CONSUMPTION = "core:WaterConsumptionState" - CORE_WATER_DETECTION = "core:WaterDetectionState" - CORE_WATER_TARGET_TEMPERATURE = "core:WaterTargetTemperatureState" - CORE_WEATHER_STATUS = "core:WeatherStatusState" - CORE_WIND_SPEED = "core:WindSpeedState" - HLRRWIFI_FAN_SPEED = "hlrrwifi:FanSpeedState" - HLRRWIFI_LEAVE_HOME = "hlrrwifi:LeaveHomeState" - HLRRWIFI_MAIN_OPERATION = "hlrrwifi:MainOperationState" - HLRRWIFI_MODE_CHANGE = "hlrrwifi:ModeChangeState" - HLRRWIFI_OUTDOOR_TEMPERATURE = "hlrrwifi:OutdoorTemperatureState" - HLRRWIFI_ROOM_TEMPERATURE = "hlrrwifi:RoomTemperatureState" - HLRRWIFI_SWING = "hlrrwifi:SwingState" - INTERNAL_CURRENT_ALARM_MODE = "internal:CurrentAlarmModeState" - INTERNAL_INTRUSION_DETECTED = "internal:IntrusionDetectedState" - INTERNAL_TARGET_ALARM_MODE = "internal:TargetAlarmModeState" - IO_AIR_DEMAND_MODE = "io:AirDemandModeState" - IO_AWAY_MODE_DURATION = "io:AwayModeDurationState" - IO_BYPASS_ACTIVATION = "io:ByPassActivationState" - IO_DEROGATION_HEATING_MODE = "io:DerogationHeatingModeState" - IO_DEROGATION_REMAINING_TIME = "io:DerogationRemainingTimeState" - IO_DHW_ABSENCE_MODE = "io:DHWAbsenceModeState" - IO_DHW_BOOST_MODE = "io:DHWBoostModeState" - IO_DHW_MODE = "io:DHWModeState" - IO_EFFECTIVE_TEMPERATURE_SETPOINT = "io:EffectiveTemperatureSetpointState" - IO_ELECTRIC_BOOSTER_OPERATING_TIME = "io:ElectricBoosterOperatingTimeState" - IO_FORCE_HEATING = "io:ForceHeatingState" - IO_HEAT_PUMP_OPERATING_TIME = "io:HeatPumpOperatingTimeState" - IO_INLET_ENGINE = "io:InletEngineState" - IO_LOCK_KEY_ACTIVATION = "io:LockKeyActivationState" - IO_MEMORIZED_SIMPLE_VOLUME = "io:MemorizedSimpleVolumeState" - IO_MIDDLE_WATER_TEMPERATURE = "io:MiddleWaterTemperatureState" - IO_MODEL = "io:ModelState" - IO_OPERATING_MODE_CAPABILITIES = "io:OperatingModeCapabilitiesState" - IO_OUTLET_ENGINE = "io:OutletEngineState" - IO_PASS_APCDHW_CONFIGURATION = "io:PassAPCDHWConfigurationState" - IO_PASS_APCDHW_PROFILE = "io:PassAPCDHWProfileState" - IO_PASS_APCDWH_MODE = "io:PassAPCDHWModeState" - IO_PASS_APC_HEATING_MODE = "io:PassAPCHeatingModeState" - IO_PASS_APC_HEATING_PROFILE = "io:PassAPCHeatingProfileState" - IO_PASS_APC_OPERATING_MODE = "io:PassAPCOperatingModeState" - IO_PRIORITY_LOCK_LEVEL = "io:PriorityLockLevelState" - IO_PRIORITY_LOCK_ORIGINATOR = "io:PriorityLockOriginatorState" - IO_SENSOR_ROOM = "io:SensorRoomState" - IO_TARGET_HEATING_LEVEL = "io:TargetHeatingLevelState" - IO_TOWEL_DRYER_TEMPORARY_STATE = "io:TowelDryerTemporaryStateState" - IO_VALVE_INSTALLATION_MODE = "io:ValveInstallationModeState" - IO_VENTILATION_CONFIGURATION_MODE = "io:VentilationConfigurationModeState" - IO_VENTILATION_MODE = "io:VentilationModeState" - IO_VIBRATION_DETECTED = "io:VibrationDetectedState" - MODBUSLINK_ANTI_LEGIONELLOSIS = "modbuslink:AntiLegionellosisState" - MODBUSLINK_DHW_ABSENCE_MODE = "modbuslink:DHWAbsenceModeState" - MODBUSLINK_DHW_BOOST_MODE = "modbuslink:DHWBoostModeState" - MODBUSLINK_DHW_CAPACITY = "modbuslink:DHWCapacityState" - MODBUSLINK_DHW_ERROR = "modbuslink:DHWErrorState" - MODBUSLINK_DHW_MODE = "modbuslink:DHWModeState" - MODBUSLINK_ELECTRIC_BOOSTER_OPERATING_TIME = ( - "modbuslink:ElectricBoosterOperatingTimeState" - ) - MODBUSLINK_HEAT_PUMP_OPERATING_TIME = "modbuslink:HeatPumpOperatingTimeState" - MODBUSLINK_IHM_TYPE = "modbuslink:IHMTypeState" - MODBUSLINK_MANUFACTURER = "modbuslink:ManufacturerState" - MODBUSLINK_MIDDLE_WATER_TEMPERATURE = "modbuslink:MiddleWaterTemperatureState" - MODBUSLINK_NUMBER_CONTROL_SHOWER_REQUEST = ( - "modbuslink:NumberControlShowerRequestState" - ) - MODBUSLINK_OPERATING_RANGE = "modbuslink:OperatingRangeState" - MODBUSLINK_POWER_HEAT_ELECTRICAL = "modbuslink:PowerHeatElectricalState" - MODBUSLINK_POWER_HEAT_PUMP = "modbuslink:PowerHeatPumpState" - MODBUSLINK_PROGRAMMING_SLOTS = "modbuslink:ProgrammingSlotsState" - MODBUSLINK_SMART_GRID_OPTION = "modbuslink:SmartGridOptionState" - MODBUS_CONTROL_DHW = "modbus:ControlDHWState" - MODBUS_CONTROL_DHW_SETTING_TEMPERATURE = "modbus:ControlDHWSettingTemperatureState" - MODBUS_DHW_MODE = "modbus:DHWModeState" - MYFOX_ALARM_STATUS = "myfox:AlarmStatusState" - MYFOX_ALERT_TRESPASS = "myfox:AlertTrespassState" - MYFOX_SHUTTER_STATUS = "myfox:ShutterStatusState" - OVP_FAN_SPEED = "ovp:FanSpeedState" - OVP_HEATING_TEMPERATURE_INTERFACE_ACTIVE_MODE = ( - "ovp:HeatingTemperatureInterfaceActiveModeState" - ) - OVP_HEATING_TEMPERATURE_INTERFACE_OPERATING_MODE = ( - "ovp:HeatingTemperatureInterfaceOperatingModeState" - ) - OVP_HEATING_TEMPERATURE_INTERFACE_SETPOINT_MODE = ( - "ovp:HeatingTemperatureInterfaceSetPointModeState" - ) - OVP_LEAVE_HOME = "ovp:LeaveHomeState" - OVP_MAIN_OPERATION = "ovp:MainOperationState" - OVP_MODE_CHANGE = "ovp:ModeChangeState" - OVP_OUTDOOR_TEMPERATURE = "ovp:OutdoorTemperatureState" - OVP_ROOM_TEMPERATURE = "ovp:RoomTemperatureState" - OVP_SWING = "ovp:SwingState" - OVP_TEMPERATURE_CHANGE = "ovp:TemperatureChangeState" - RAMSES_RAMSES_OPERATING_MODE = "ramses:RAMSESOperatingModeState" - RTDS_CONTROLLER_BATTERY = "rtds:ControllerBatteryState" - RTDS_CONTROLLER_BIP = "rtds:ControllerBipState" - RTDS_CONTROLLER_ORDER_TYPE = "rtds:ControllerOrderTypeState" - RTDS_CONTROLLER_ORIGINATOR = "rtds:ControllerOriginatorState" - RTDS_CONTROLLER_SENSING = "rtds:ControllerSensingState" - RTDS_CONTROLLER_SIREN = "rtds:ControllerSirenState" - SOMFY_THERMOSTAT_AT_HOME_TARGET_TEMPERATURE = ( - "somfythermostat:AtHomeTargetTemperatureState" - ) - SOMFY_THERMOSTAT_AWAY_MODE_TARGET_TEMPERATURE = ( - "somfythermostat:AwayModeTargetTemperatureState" - ) - SOMFY_THERMOSTAT_DEROGATION_HEATING_MODE = ( - "somfythermostat:DerogationHeatingModeState" - ) - SOMFY_THERMOSTAT_FREEZE_MODE_TARGET_TEMPERATURE = ( - "somfythermostat:FreezeModeTargetTemperatureState" - ) - SOMFY_THERMOSTAT_HEATING_MODE = "somfythermostat:HeatingModeState" - SOMFY_THERMOSTAT_SLEEPING_MODE_TARGET_TEMPERATURE = ( - "somfythermostat:SleepingModeTargetTemperatureState" - ) - VERISURE_ALARM_PANEL_MAIN_ARM_TYPE = "verisure:AlarmPanelMainArmTypeState" diff --git a/pyoverkiz/enums/ui.py b/pyoverkiz/enums/ui.py deleted file mode 100644 index 0423fd2..0000000 --- a/pyoverkiz/enums/ui.py +++ /dev/null @@ -1,424 +0,0 @@ -import logging -import sys -from enum import unique - -_LOGGER = logging.getLogger(__name__) - -# Since we support Python versions lower than 3.11, we use -# a backport for StrEnum when needed. -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -@unique -class UIClass(StrEnum): - # A list of all defined UI classes from /reference/ui/classes - ADJUSTABLE_SLATS_ROLLER_SHUTTER = "AdjustableSlatsRollerShutter" - AIR_SENSOR = "AirSensor" - ALARM = "Alarm" - AWNING = "Awning" - BALLAST = "Ballast" - CAMERA = "Camera" - CAR_BUTTON_SENSOR = "CarButtonSensor" - CIRCUIT_BREAKER = "CircuitBreaker" - CONFIGURATION_COMPONENT = "ConfigurationComponent" - CONSUMPTION_SENSOR = "ConsumptionSensor" - CONTACT_SENSOR = "ContactSensor" - CURTAIN = "Curtain" - DOCK = "Dock" - DOOR_LOCK = "DoorLock" - ELECTRICITY_SENSOR = "ElectricitySensor" - EVO_HOME = "EvoHome" - EXTERIOR_HEATING_SYSTEM = "ExteriorHeatingSystem" - EXTERIOR_SCREEN = "ExteriorScreen" - EXTERIOR_VENETIAN_BLIND = "ExteriorVenetianBlind" - GARAGE_DOOR = "GarageDoor" - GAS_SENSOR = "GasSensor" - GATE = "Gate" - GENERIC = "Generic" - GENERIC_SENSOR = "GenericSensor" - GROUP_CONFIGURATION = "GroupConfiguration" - HEAT_PUMP_SYSTEM = "HeatPumpSystem" - HEATING_SYSTEM = "HeatingSystem" - HITACHI_HEATING_SYSTEM = "HitachiHeatingSystem" - HUMIDITY_SENSOR = "HumiditySensor" - IR_BLASTER_CONTROLLER = "IRBlasterController" - INTRUSION_SENSOR = "IntrusionSensor" - LIGHT = "Light" - LIGHT_SENSOR = "LightSensor" - MUSIC_PLAYER = "MusicPlayer" - NETWORK_COMPONENT = "NetworkComponent" - OCCUPANCY_SENSOR = "OccupancySensor" - ON_OFF = "OnOff" - OVEN = "Oven" - PERGOLA = "Pergola" - POD = "Pod" - PROTOCOL_GATEWAY = "ProtocolGateway" - RAIN_SENSOR = "RainSensor" - REMOTE_CONTROLLER = "RemoteController" - ROLLER_SHUTTER = "RollerShutter" - SCREEN = "Screen" - SHUTTER = "Shutter" - SIREN = "Siren" - SMOKE_SENSOR = "SmokeSensor" - SUN_INTENSITY_SENSOR = "SunIntensitySensor" - SUN_SENSOR = "SunSensor" - SWIMMING_POOL = "SwimmingPool" - SWINGING_SHUTTER = "SwingingShutter" - TEMPERATURE_SENSOR = "TemperatureSensor" - THERMAL_ENERGY_SENSOR = "ThermalEnergySensor" - THIRD_PARTY_GATEWAY = "ThirdPartyGateway" - VENETIAN_BLIND = "VenetianBlind" - VENTILATION_SYTEM = "VentilationSystem" - WASHING_MACHINE = "WashingMachine" - WATER_HEATING_SYSTEM = "WaterHeatingSystem" - WATER_SENSOR = "WaterSensor" - WEATHER_SENSOR = "WeatherSensor" - WIFI = "Wifi" - WIND_SENSOR = "WindSensor" - WINDOW = "Window" - WINDOW_HANDLE = "WindowHandle" - - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN - - -@unique -class UIWidget(StrEnum): - # A list of all defined UI widgets from /reference/ui/widgets - AIR_FLOW_SENSOR = "AirFlowSensor" - AIR_QUALITY_SENSOR = "AirQualitySensor" - ALARM_PANEL_CONTROLLER = "AlarmPanelController" - ALARM_REMOTE_CONTROLLER = "AlarmRemoteController" - ATLANTIC_ELECTRICAL_HEATER = "AtlanticElectricalHeater" - ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT = ( - "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" - ) - ATLANTIC_ELECTRICAL_TOWEL_DRYER = "AtlanticElectricalTowelDryer" - ATLANTIC_HEAT_RECOVERY_VENTILATION = "AtlanticHeatRecoveryVentilation" - ATLANTIC_MULTI_METER_ELECTRIC_CONFIGURATION = ( - "AtlanticMultiMeterElectricConfiguration" - ) - ATLANTIC_MULTI_METER_ELECTRIC_HEAT_PUMP = "AtlanticMultiMeterElectricHeatPump" - ATLANTIC_MULTI_METER_ELECTRIC_SENSOR = "AtlanticMultiMeterElectricSensor" - ATLANTIC_PASS_APC_BOILER = "AtlanticPassAPCBoiler" - ATLANTIC_PASS_APC_DHW = "AtlanticPassAPCDHW" - ATLANTIC_PASS_APC_HEAT_PUMP = "AtlanticPassAPCHeatPump" - ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE = "AtlanticPassAPCHeatingAndCoolingZone" - ATLANTIC_PASS_APC_HEATING_ZONE = "AtlanticPassAPCHeatingZone" - ATLANTIC_PASS_APC_ZONE_CONTROL = "AtlanticPassAPCZoneControl" - AWNING_VALANCE = "AwningValance" - BALLAST = "Ballast" - BATTERY_SENSOR = "BatterySensor" - BIOCLIMATIC_PERGOLA = "BioclimaticPergola" - CO2_SENSOR = "CO2Sensor" - CO_SENSOR = "COSensor" - CAR_BUTTON_SENSOR = "CarButtonSensor" - CAR_LOCK = "CarLock" - CARD_SWITCH = "CardSwitch" - CIRCUIT_BREAKER = "CircuitBreaker" - CONTACT_SENSOR = "ContactSensor" - COTHERM_THERMOSTAT = "CothermThermostat" - CUMULATIVE_ELECTRIC_POWER_CONSUMPTION_SENSOR = ( - "CumulativeElectricPowerConsumptionSensor" - ) - CUMULATIVE_ELECTRIC_POWER_PRODUCTION_SENSOR = ( - "CumulativeElectricPowerProductionSensor" - ) - CUMULATIVE_FOSSIL_ENERGY_CONSUMPTION_SENSOR = ( - "CumulativeFossilEnergyConsumptionSensor" - ) - CUMULATIVE_GAS_CONSUMPTION_SENSOR = "CumulativeGasConsumptionSensor" - CUMULATIVE_THERMAL_ENERGY_CONSUMPTION_SENSOR = ( - "CumulativeThermalEnergyConsumptionSensor" - ) - CUMULATIVE_WATER_CONSUMPTION_SENSOR = "CumulativeWaterConsumptionSensor" - CURTAIN_TRACK_UNO = "CurtainTrackUno" - CYCLIC_GARAGE_DOOR = "CyclicGarageDoor" - CYCLIC_GENERIC = "CyclicGeneric" - DHW_SET_POINT = "DHWSetPoint" - DE_DIETRICH_BOILER = "DeDietrichBoiler" - DE_DIETRICH_DHW = "DeDietrichDHW" - DE_DIETRICH_HEATING_CIRCUIT = "DeDietrichHeatingCircuit" - DE_DIETRICH_MODBUS_GATEWAY = "DeDietrichModbusGateway" - DE_DIETRICH_SWIMMING_POOL = "DeDietrichSwimmingPool" - DIMMER_CIE_COLOR_SPACE_XY_LIGHT = "DimmerCIEColorSpaceXYLight" - DIMMER_COLOR_TEMPERATURE_LIGHT = "DimmerColorTemperatureLight" - DIMMER_EXTERIOR_HEATING = "DimmerExteriorHeating" - DIMMER_HUE_SAT_OR_CT_LIGHT = "DimmerHueSatOrCTLight" - DIMMER_HUE_SATURATION_LIGHT = "DimmerHueSaturationLight" - DIMMER_LIGHT = "DimmerLight" - DIMMER_ON_OFF = "DimmerOnOff" - DIMMER_ON_OFF_LIGHT = "DimmerOnOffLight" - DIMMER_RGBCOLOURED_LIGHT = "DimmerRGBColouredLight" - DIMPLEX_VENTILATION_INLET_OUTLET = "DimplexVentilationInletOutlet" - DISCRETE_EXTERIOR_HEATING = "DiscreteExteriorHeating" - DISCRETE_POSITIONABLE_GARAGE_DOOR = "DiscretePositionableGarageDoor" - DISCRETE_POSITIONABLE_GATE = "DiscretePositionableGate" - DOCK = "Dock" - DOMESTIC_HOT_WATER_PRODUCTION = "DomesticHotWaterProduction" - DOMESTIC_HOT_WATER_TANK = "DomesticHotWaterTank" - DOOR_LOCK = "DoorLock" - DROP_ARM_AWNING = "DropArmAwning" - DYNAMIC_AIR_VENT = "DynamicAirVent" - DYNAMIC_ALARM = "DynamicAlarm" - DYNAMIC_AUDIO_PLAYER = "DynamicAudioPlayer" - DYNAMIC_AWNING = "DynamicAwning" - DYNAMIC_BRIDGE = "DynamicBridge" - DYNAMIC_CIRCUIT_BREAKER = "DynamicCircuitBreaker" - DYNAMIC_CURTAIN = "DynamicCurtain" - DYNAMIC_GARAGE_DOOR = "DynamicGarageDoor" - DYNAMIC_GATE = "DynamicGate" - DYNAMIC_GATEWAY = "DynamicGateway" - DYNAMIC_HEATER = "DynamicHeater" - DYNAMIC_HUMIDITY_SENSOR = "DynamicHumiditySensor" - DYNAMIC_HVAC_CENTRAL_UNIT = "DynamicHvacCentralUnit" - DYNAMIC_INTRUSION_SENSOR = "DynamicIntrusionSensor" - DYNAMIC_LIGHT = "DynamicLight" - DYNAMIC_LIGHT_SENSOR = "DynamicLightSensor" - DYNAMIC_OCCUPANCY_SENSOR = "DynamicOccupancySensor" - DYNAMIC_OPENING_SENSOR = "DynamicOpeningSensor" - DYNAMIC_OUTLET = "DynamicOutlet" - DYNAMIC_OVEN = "DynamicOven" - DYNAMIC_PERGOLA = "DynamicPergola" - DYNAMIC_RAIN_SENSOR = "DynamicRainSensor" - DYNAMIC_ROLLER_SHUTTER = "DynamicRollerShutter" - DYNAMIC_SCREEN = "DynamicScreen" - DYNAMIC_SHUTTER = "DynamicShutter" - DYNAMIC_TEMPERATURE_SENSOR = "DynamicTemperatureSensor" - DYNAMIC_THERMOSTAT = "DynamicThermostat" - DYNAMIC_THIRD_PARTY_GATEWAY = "DynamicThirdPartyGateway" - DYNAMIC_VENETIAN_BLIND = "DynamicVenetianBlind" - DYNAMIC_VENTILATION = "DynamicVentilation" - DYNAMIC_WASHING_MACHINE = "DynamicWashingMachine" - DYNAMIC_WEATHER_STATION = "DynamicWeatherStation" - DYNAMIC_WIND_SENSOR = "DynamicWindSensor" - DYNAMIC_WINDOW = "DynamicWindow" - ELECTRICAL_HEATER = "ElectricalHeater" - ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT = ( - "ElectricalHeaterWithAdjustableTemperatureSetpoint" - ) - EMPTY_REMOTE_CONTROLLER = "EmptyRemoteController" - EN_OCEAN_GENERIC = "EnOceanGeneric" - EN_OCEAN_GENERIC_ELECTRIC_COUNTER = "EnOceanGenericElectricCounter" - EN_OCEAN_TRANSCEIVER = "EnOceanTransceiver" - EVO_HOME_CONTROLLER = "EvoHomeController" - EWATTCH_TICCOUNTER = "EwattchTICCounter" - EXTERIOR_VENETIAN_BLIND = "ExteriorVenetianBlind" - FLOOR_HEATING = "FloorHeating" - GAS_DHW_CONSUMPTION_SENSOR = "GasDHWConsumptionSensor" - GAS_HEATER_CONSUMPTION_SENSOR = "GasHeaterConsumptionSensor" - GENERIC_16_CHANNELS_COUNTER = "Generic16ChannelsCounter" - GENERIC_1_CHANNEL_COUNTER = "Generic1ChannelCounter" - GENERIC_CAMERA = "GenericCamera" - GROUP_CONFIGURATION = "GroupConfiguration" - HEAT_DETECTION_SENSOR = "HeatDetectionSensor" - HEAT_PUMP = "HeatPump" - HEATING_SET_POINT = "HeatingSetPoint" - HEATING_TEMPERATURE_INTERFACE = "HeatingTemperatureInterface" - HITACHI_AIR_TO_AIR_HEAT_PUMP = "HitachiAirToAirHeatPump" - HITACHI_AIR_TO_WATER_HEATING_ZONE = "HitachiAirToWaterHeatingZone" - HITACHI_AIR_TO_WATER_MAIN_COMPONENT = "HitachiAirToWaterMainComponent" - HITACHI_DHW = "HitachiDHW" - HITACHI_SWIMMING_POOL = "HitachiSwimmingPool" - HITACHI_THERMOSTAT = "HitachiThermostat" - HOMEKIT_STACK = "HomekitStack" - HUE_BRIDGE = "HueBridge" - IOGENERIC = "IOGeneric" - IOSIREN = "IOSiren" - IOSTACK = "IOStack" - IRBLASTER = "IRBlaster" - IMHOTEP_HEATING_TEMPERATURE_INTERFACE = "ImhotepHeatingTemperatureInterface" - INSTANT_ELECTRIC_CURRENT_CONSUMPTION_SENSOR = ( - "InstantElectricCurrentConsumptionSensor" - ) - INSTANT_ELECTRIC_POWER_CONSUMPTION_SENSOR = "InstantElectricPowerConsumptionSensor" - INTRUSION_DETECTOR = "IntrusionDetector" - INTRUSION_EVENT_SENSOR = "IntrusionEventSensor" - INTRUSION_SENSOR = "IntrusionSensor" - INVALID = "Invalid" - JSWCAMERA = "JSWCamera" - KIZ_OTHERM_BRIDGE = "KizOThermBridge" - KIZ_OTHERM_V2_BRIDGE = "KizOThermV2Bridge" - LOCK_UNLOCK_DOOR_LOCK_WITH_UNKNOWN_POSITION = ( - "LockUnlockDoorLockWithUnknownPosition" - ) - LUMINANCE_SENSOR = "LuminanceSensor" - MEDIA_RENDERER = "MediaRenderer" - MOTION_SENSOR = "MotionSensor" - MULTI_METER_ELECTRIC_SENSOR = "MultiMeterElectricSensor" - MY_FOX_ALARM_CONTROLLER = "MyFoxAlarmController" - MY_FOX_CAMERA = "MyFoxCamera" - MY_FOX_SECURITY_CAMERA = "MyFoxSecurityCamera" - NODE = "Node" - OVPGENERIC = "OVPGeneric" - OCCUPANCY_SENSOR = "OccupancySensor" - ON_OFF_HEATING_SYSTEM = "OnOffHeatingSystem" - ON_OFF_LIGHT = "OnOffLight" - ON_OFF_REMOTECONTROLLER = "OnOffRemotecontroller" - OPEN_CLOSE_GATE = "OpenCloseGate" - OPEN_CLOSE_GATE_4T = "OpenCloseGate4T" - OPEN_CLOSE_GATE_WITH_PEDESTRIAN_POSITION = "OpenCloseGateWithPedestrianPosition" - OPEN_CLOSE_SLIDING_GARAGE_DOOR = "OpenCloseSlidingGarageDoor" - OPEN_CLOSE_SLIDING_GARAGE_DOOR_4T = "OpenCloseSlidingGarageDoor4T" - OPEN_CLOSE_SLIDING_GARAGE_DOOR_WITH_PEDESTRIAN_POSITION = ( - "OpenCloseSlidingGarageDoorWithPedestrianPosition" - ) - OPEN_CLOSE_SLIDING_GATE = "OpenCloseSlidingGate" - OPEN_CLOSE_SLIDING_GATE_4T = "OpenCloseSlidingGate4T" - OPEN_CLOSE_SLIDING_GATE_WITH_PEDESTRIAN_POSITION = ( - "OpenCloseSlidingGateWithPedestrianPosition" - ) - OPEN_THERM_DIAGNOSTIC = "OpenThermDiagnostic" - PERGOLA_HORIZONTAL_AWNING = "PergolaHorizontalAwning" - PERGOLA_HORIZONTAL_AWNING_UNO = "PergolaHorizontalAwningUno" - PERGOLA_SIDE_SCREEN = "PergolaSideScreen" - POD = "Pod" - POSITIONABLE_AND_LOCKABLE_SLIDING_WINDOW = "PositionableAndLockableSlidingWindow" - POSITIONABLE_AND_STRETCHABLE_PERGOLA_SCREEN = ( - "PositionableAndStretchablePergolaScreen" - ) - POSITIONABLE_CURTAIN = "PositionableCurtain" - POSITIONABLE_DUAL_ROLLER_SHUTTER = "PositionableDualRollerShutter" - POSITIONABLE_EXTERIOR_VENETIAN_BLIND = "PositionableExteriorVenetianBlind" - POSITIONABLE_EXTERIOR_VENETIAN_BLIND_UNO = "PositionableExteriorVenetianBlindUno" - POSITIONABLE_EXTERIOR_VENETIAN_BLIND_WITH_WP = ( - "PositionableExteriorVenetianBlindWithWP" - ) - POSITIONABLE_EXTERIOR_VENETIAN_BLIND_WITH_WP2 = ( - "PositionableExteriorVenetianBlindWithWP2" - ) - POSITIONABLE_GARAGE_DOOR = "PositionableGarageDoor" - POSITIONABLE_GARAGE_DOOR_WITH_PARTIAL_POSITION = ( - "PositionableGarageDoorWithPartialPosition" - ) - POSITIONABLE_GATE = "PositionableGate" - POSITIONABLE_GATE_WITH_PEDESTRIAN_POSITION = ( - "PositionableGateWithPedestrianPosition" - ) - POSITIONABLE_HORIZONTAL_AWNING = "PositionableHorizontalAwning" - POSITIONABLE_HORIZONTAL_AWNING_UNO = "PositionableHorizontalAwningUno" - POSITIONABLE_OR_ORIENTABLE_ROLLER_SHUTTER = "PositionableOrOrientableRollerShutter" - POSITIONABLE_OR_PROGRESSIVE_ORIENTABLE_ROLLER_SHUTTER = ( - "PositionableOrProgressiveOrientableRollerShutter" - ) - POSITIONABLE_PROJECTION_ROLLER_SHUTTER = "PositionableProjectionRollerShutter" - POSITIONABLE_ROLLER_SHUTTER = "PositionableRollerShutter" - POSITIONABLE_ROLLER_SHUTTER_UNO = "PositionableRollerShutterUno" - POSITIONABLE_ROLLER_SHUTTER_WITH_LOW_SPEED_MANAGEMENT = ( - "PositionableRollerShutterWithLowSpeedManagement" - ) - POSITIONABLE_SCREEN = "PositionableScreen" - POSITIONABLE_SCREEN_UNO = "PositionableScreenUno" - POSITIONABLE_SLIDING_WINDOW = "PositionableSlidingWindow" - POSITIONABLE_TILTED_ROLLER_SHUTTER = "PositionableTiltedRollerShutter" - POSITIONABLE_TILTED_SCREEN = "PositionableTiltedScreen" - POSITIONABLE_TILTED_WINDOW = "PositionableTiltedWindow" - POSITIONABLE_VENETIAN_BLIND = "PositionableVenetianBlind" - POSITIONABLE_WINDOW = "PositionableWindow" - POSITIONABLE_WINDOW_UNO = "PositionableWindowUno" - PROGRAMMABLE_AND_PROTECTABLE_THERMOSTAT_SET_POINT = ( - "ProgrammableAndProtectableThermostatSetPoint" - ) - RTD_INDOOR_SIREN = "RTDIndoorSiren" - RTD_OUTDOOR_SIREN = "RTDOutdoorSiren" - RTS_GENERIC = "RTSGeneric" - RTS_GENERIC_4T = "RTSGeneric4T" - RTS_THERMOSTAT = "RTSThermostat" - RAIN_SENSOR = "RainSensor" - RELATIVE_HUMIDITY_SENSOR = "RelativeHumiditySensor" - REMOTE_CONTROLLER_ONE_WAY = "RemoteControllerOneWay" - REPEATER = "Repeater" - ROCKER_SWITCH_AUTO_MANU_UP_DOWN_CONTROLLER = "RockerSwitchAutoManuUpDownController" - ROCKER_SWITCHX_1_CONTROLLER = "RockerSwitchx1Controller" - ROCKER_SWITCHX_2_CONTROLLER = "RockerSwitchx2Controller" - ROCKER_SWITCHX_4_CONTROLLER = "RockerSwitchx4Controller" - SCENE_CONTROLLER = "SceneController" - SCHNEIDER_SWITCH_CONFIGURATION = "SchneiderSwitchConfiguration" - SIREN_STATUS = "SirenStatus" - SLATS_ORIENTATION = "SlatsOrientation" - SLIDING_DISCRETE_GATE_WITH_PEDESTRIAN_POSITION = ( - "SlidingDiscreteGateWithPedestrianPosition" - ) - SMOKE_SENSOR = "SmokeSensor" - SOMFY_CONFIGURATION_TOOL = "SomfyConfigurationTool" - SOMFY_HEATING_TEMPERATURE_INTERFACE = "SomfyHeatingTemperatureInterface" - SOMFY_PILOT_WIRE_ELECTRICAL_HEATER = "SomfyPilotWireElectricalHeater" - SOMFY_PILOT_WIRE_HEATING_INTERFACE = "SomfyPilotWireHeatingInterface" - SOMFY_THERMOSTAT = "SomfyThermostat" - STATEFUL_ALARM_CONTROLLER = "StatefulAlarmController" - STATEFUL_ON_OFF = "StatefulOnOff" - STATEFUL_ON_OFF_LIGHT = "StatefulOnOffLight" - STATELESS_ALARM_CONTROLLER = "StatelessAlarmController" - STATELESS_EXTERIOR_HEATING = "StatelessExteriorHeating" - STATELESS_ON_OFF = "StatelessOnOff" - SUN_ENERGY_SENSOR = "SunEnergySensor" - SUN_INTENSITY_SENSOR = "SunIntensitySensor" - SWIMMING_POOL = "SwimmingPool" - SWINGING_SHUTTER = "SwingingShutter" - TSKALARM_CONTROLLER = "TSKAlarmController" - TEMPERATURE_SENSOR = "TemperatureSensor" - THERMOSTAT_HEATING_TEMPERATURE_INTERFACE = "ThermostatHeatingTemperatureInterface" - THERMOSTAT_SET_POINT = "ThermostatSetPoint" - THERMOSTAT_ZONES_CONTROLLER = "ThermostatZonesController" - THREE_WAY_WINDOW_HANDLE = "ThreeWayWindowHandle" - TILT_ONLY_VENETIAN_BLIND = "TiltOnlyVenetianBlind" - TIMED_ON_OFF = "TimedOnOff" - TIMED_ON_OFF_LIGHT = "TimedOnOffLight" - UNIVERSAL_SENSOR = "UniversalSensor" - UNTYPED = "Untyped" - UP_DOWN_BIOCLIMATIC_PERGOLA = "UpDownBioclimaticPergola" - UP_DOWN_CELLULAR_SCREEN = "UpDownCellularScreen" - UP_DOWN_CURTAIN = "UpDownCurtain" - UP_DOWN_DUAL_CURTAIN = "UpDownDualCurtain" - UP_DOWN_EXTERIOR_SCREEN = "UpDownExteriorScreen" - UP_DOWN_EXTERIOR_VENETIAN_BLIND = "UpDownExteriorVenetianBlind" - UP_DOWN_GARAGE_DOOR = "UpDownGarageDoor" - UP_DOWN_GARAGE_DOOR_4T = "UpDownGarageDoor4T" - UP_DOWN_GARAGE_DOOR_WITH_VENTILATION_POSITION = ( - "UpDownGarageDoorWithVentilationPosition" - ) - UP_DOWN_HORIZONTAL_AWNING = "UpDownHorizontalAwning" - UP_DOWN_ROLLER_SHUTTER = "UpDownRollerShutter" - UP_DOWN_SCREEN = "UpDownScreen" - UP_DOWN_SHEER_SCREEN = "UpDownSheerScreen" - UP_DOWN_SWINGING_SHUTTER = "UpDownSwingingShutter" - UP_DOWN_VENETIAN_BLIND = "UpDownVenetianBlind" - UP_DOWN_WINDOW = "UpDownWindow" - UP_DOWN_ZEBRA_SCREEN = "UpDownZebraScreen" - VOCSENSOR = "VOCSensor" - VALVE_HEATING_TEMPERATURE_INTERFACE = "ValveHeatingTemperatureInterface" - VENTILATION_INLET = "VentilationInlet" - VENTILATION_OUTLET = "VentilationOutlet" - VENTILATION_TRANSFER = "VentilationTransfer" - WATER_DETECTION_SENSOR = "WaterDetectionSensor" - WEATHER_FORECAST_SENSOR = "WeatherForecastSensor" - WIFI = "Wifi" - WIND_SPEED_SENSOR = "WindSpeedSensor" - WINDOW_LOCK = "WindowLock" - WINDOW_WITH_TILT_SENSOR = "WindowWithTiltSensor" - ZWAVE_AEOTEC_CONFIGURATION = "ZWaveAeotecConfiguration" - ZWAVE_CONFIGURATION = "ZWaveConfiguration" - ZWAVE_DANFOSS_RSLINK = "ZWaveDanfossRSLink" - ZWAVE_DOOR_LOCK_CONFIGURATION = "ZWaveDoorLockConfiguration" - ZWAVE_FIBARO_ROLLER_SHUTTER_CONFIGURATION = "ZWaveFibaroRollerShutterConfiguration" - ZWAVE_HEATIT_THERMOSTAT_CONFIGURATION = "ZWaveHeatitThermostatConfiguration" - ZWAVE_NODON_CONFIGURATION = "ZWaveNodonConfiguration" - ZWAVE_QUBINO_CONFIGURATION = "ZWaveQubinoConfiguration" - ZWAVE_SEDEVICE_CONFIGURATION = "ZWaveSEDeviceConfiguration" - ZWAVE_TRANSCEIVER = "ZWaveTransceiver" - ZIGBEE_NETWORK = "ZigbeeNetwork" - ZIGBEE_STACK = "ZigbeeStack" - - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value): # type: ignore - _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") - return cls.UNKNOWN diff --git a/pyoverkiz/exceptions.py b/pyoverkiz/exceptions.py deleted file mode 100644 index 4cf7091..0000000 --- a/pyoverkiz/exceptions.py +++ /dev/null @@ -1,109 +0,0 @@ -class BaseOverkizException(Exception): - pass - - -class OverkizException(BaseOverkizException): - pass - - -class BadCredentialsException(BaseOverkizException): - pass - - -class InvalidCommandException(BaseOverkizException): - pass - - -class NotAuthenticatedException(BaseOverkizException): - pass - - -class TooManyExecutionsException(BaseOverkizException): - pass - - -class ExecutionQueueFullException(BaseOverkizException): - pass - - -class TooManyRequestsException(BaseOverkizException): - pass - - -class TooManyConcurrentRequestsException(BaseOverkizException): - pass - - -class ServiceUnavailableException(BaseOverkizException): - pass - - -class MaintenanceException(ServiceUnavailableException): - pass - - -class MissingAuthorizationTokenException(BaseOverkizException): - pass - - -class InvalidEventListenerIdException(BaseOverkizException): - pass - - -class NoRegisteredEventListenerException(BaseOverkizException): - pass - - -class SessionAndBearerInSameRequestException(BaseOverkizException): - pass - - -class TooManyAttemptsBannedException(BaseOverkizException): - pass - - -class InvalidTokenException(BaseOverkizException): - pass - - -class NotSuchTokenException(BaseOverkizException): - pass - - -class UnknownUserException(BaseOverkizException): - pass - - -class UnknownObjectException(BaseOverkizException): - pass - - -class AccessDeniedToGatewayException(BaseOverkizException): - pass - - -# Nexity -class NexityBadCredentialsException(BadCredentialsException): - pass - - -class NexityServiceException(BaseOverkizException): - pass - - -# CozyTouch -class CozyTouchBadCredentialsException(BadCredentialsException): - pass - - -class CozyTouchServiceException(BaseOverkizException): - pass - - -# Somfy -class SomfyBadCredentialsException(BadCredentialsException): - pass - - -class SomfyServiceException(BaseOverkizException): - pass diff --git a/pyoverkiz/models.py b/pyoverkiz/models.py deleted file mode 100644 index ab68132..0000000 --- a/pyoverkiz/models.py +++ /dev/null @@ -1,831 +0,0 @@ -from __future__ import annotations - -import re -from typing import Any, Iterator, cast - -from attr import define, field - -from pyoverkiz.enums import ( - DataType, - EventName, - ExecutionState, - ExecutionSubType, - ExecutionType, - FailureType, - GatewaySubType, - GatewayType, - ProductType, - UIClass, - UIWidget, - UpdateBoxStatus, -) -from pyoverkiz.enums.protocol import Protocol -from pyoverkiz.obfuscate import obfuscate_email, obfuscate_id, obfuscate_string -from pyoverkiz.types import DATA_TYPE_TO_PYTHON, StateType - -# pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals - -# :///[#] -DEVICE_URL_RE = r"(?P.+)://(?P[^/]+)/(?P[^#]+)(#(?P\d+))?" - - -@define(init=False, kw_only=True) -class Setup: - creation_time: str | None = None - last_update_time: str | None = None - id: str = field(repr=obfuscate_id, default=None) - location: Location | None = None - gateways: list[Gateway] - devices: list[Device] - zones: list[Zone] | None = None - reseller_delegation_type: str | None = None - oid: str | None = None - root_place: Place | None = None - features: list[Feature] | None = None - - def __init__( - self, - *, - creation_time: str | None = None, - last_update_time: str | None = None, - id: str = field(repr=obfuscate_id, default=None), - location: dict[str, Any] | None = None, - gateways: list[dict[str, Any]], - devices: list[dict[str, Any]], - zones: list[dict[str, Any]] | None = None, - reseller_delegation_type: str | None = None, - oid: str | None = None, - root_place: dict[str, Any] | None = None, - features: list[dict[str, Any]] | None = None, - **_: Any, - ) -> None: - self.id = id - self.creation_time = creation_time - self.last_update_time = last_update_time - self.location = Location(**location) if location else None - self.gateways = [Gateway(**g) for g in gateways] - self.devices = [Device(**d) for d in devices] - self.zones = [Zone(**z) for z in zones] if zones else None - self.reseller_delegation_type = reseller_delegation_type - self.oid = oid - self.root_place = Place(**root_place) if root_place else None - self.features = [Feature(**f) for f in features] if features else None - - -@define(init=False, kw_only=True) -class Location: - creation_time: str - last_update_time: str | None = None - city: str = field(repr=obfuscate_string, default=None) - country: str = field(repr=obfuscate_string, default=None) - postal_code: str = field(repr=obfuscate_string, default=None) - address_line1: str = field(repr=obfuscate_string, default=None) - address_line2: str = field(repr=obfuscate_string, default=None) - timezone: str - longitude: str = field(repr=obfuscate_string, default=None) - latitude: str = field(repr=obfuscate_string, default=None) - twilight_mode: int - twilight_angle: str - twilight_city: str | None = None - summer_solstice_dusk_minutes: str - winter_solstice_dusk_minutes: str - twilight_offset_enabled: bool - dawn_offset: int - dusk_offset: int - - def __init__( - self, - *, - creation_time: str, - last_update_time: str | None = None, - city: str = field(repr=obfuscate_string, default=None), - country: str = field(repr=obfuscate_string, default=None), - postal_code: str = field(repr=obfuscate_string, default=None), - address_line1: str = field(repr=obfuscate_string, default=None), - address_line2: str = field(repr=obfuscate_string, default=None), - timezone: str, - longitude: str = field(repr=obfuscate_string, default=None), - latitude: str = field(repr=obfuscate_string, default=None), - twilight_mode: int, - twilight_angle: str, - twilight_city: str | None = None, - summer_solstice_dusk_minutes: str, - winter_solstice_dusk_minutes: str, - twilight_offset_enabled: bool, - dawn_offset: int, - dusk_offset: int, - **_: Any, - ) -> None: - self.creation_time = creation_time - self.last_update_time = last_update_time - self.city = city - self.country = country - self.postal_code = postal_code - self.address_line1 = address_line1 - self.address_line2 = address_line2 - self.timezone = timezone - self.longitude = longitude - self.latitude = latitude - self.twilight_mode = twilight_mode - self.twilight_angle = twilight_angle - self.twilight_city = twilight_city - self.summer_solstice_dusk_minutes = summer_solstice_dusk_minutes - self.winter_solstice_dusk_minutes = winter_solstice_dusk_minutes - self.twilight_offset_enabled = twilight_offset_enabled - self.dawn_offset = dawn_offset - self.dusk_offset = dusk_offset - - -@define(init=False, kw_only=True) -class Device: - id: str = field(repr=False) - attributes: States - available: bool - enabled: bool - label: str = field(repr=obfuscate_string) - device_url: str = field(repr=obfuscate_id) - gateway_id: str | None = field(repr=obfuscate_id) - device_address: str | None = field(repr=obfuscate_id) - subsystem_id: int | None = None - is_sub_device: bool = False - controllable_name: str - definition: Definition - data_properties: list[dict[str, Any]] | None = None - widget: UIWidget - ui_class: UIClass - states: States - type: ProductType - place_oid: str | None = None - protocol: Protocol | None = field(init=False, repr=False) - - def __init__( - self, - *, - attributes: list[dict[str, Any]] | None = None, - available: bool, - enabled: bool, - label: str, - device_url: str, - controllable_name: str, - definition: dict[str, Any], - data_properties: list[dict[str, Any]] | None = None, - widget: str | None = None, - widget_name: str | None = None, - ui_class: str | None = None, - states: list[dict[str, Any]] | None = None, - type: int, - place_oid: str | None = None, - **_: Any, - ) -> None: - self.id = device_url - self.attributes = States(attributes) - self.available = available - self.definition = Definition(**definition) - self.device_url = device_url - self.enabled = enabled - self.label = label - self.controllable_name = controllable_name - self.states = States(states) - self.data_properties = data_properties - self.type = ProductType(type) - self.place_oid = place_oid - - self.protocol = None - self.gateway_id = None - self.device_address = None - self.subsystem_id = None - self.is_sub_device = False - - # Split :///[#] into multiple variables - match = re.search(DEVICE_URL_RE, device_url) - - if match: - self.protocol = Protocol(match.group("protocol")) - self.gateway_id = match.group("gatewayId") - self.device_address = match.group("deviceAddress") - - if match.group("subsystemId"): - self.subsystem_id = int(match.group("subsystemId")) - self.is_sub_device = self.subsystem_id > 1 - - if ui_class: - self.ui_class = UIClass(ui_class) - elif self.definition.ui_class: - self.ui_class = UIClass(self.definition.ui_class) - - if widget: - self.widget = UIWidget(widget) - elif self.definition.widget_name: - self.widget = UIWidget(self.definition.widget_name) - - -@define(init=False, kw_only=True) -class StateDefinition: - qualified_name: str - type: str | None = None - values: list[str] | None = None - - def __init__( - self, - name: str | None = None, - qualified_name: str | None = None, - type: str | None = None, - values: list[str] | None = None, - **_: Any, - ) -> None: - self.type = type - self.values = values - - if qualified_name: - self.qualified_name = qualified_name - elif name: - self.qualified_name = name - - -@define(init=False, kw_only=True) -class Definition: - commands: CommandDefinitions - states: list[StateDefinition] - widget_name: str | None = None - ui_class: str | None = None - qualified_name: str | None = None - - def __init__( - self, - *, - commands: list[dict[str, Any]], - states: list[dict[str, Any]] | None = None, - widget_name: str | None = None, - ui_class: str | None = None, - qualified_name: str | None = None, - **_: Any, - ) -> None: - self.commands = CommandDefinitions(commands) - self.states = [StateDefinition(**sd) for sd in states] if states else [] - self.widget_name = widget_name - self.ui_class = ui_class - self.qualified_name = qualified_name - - -@define(init=False, kw_only=True) -class CommandDefinition: - command_name: str - nparams: int - - def __init__(self, command_name: str, nparams: int, **_: Any) -> None: - self.command_name = command_name - self.nparams = nparams - - -@define(init=False) -class CommandDefinitions: - _commands: list[CommandDefinition] - - def __init__(self, commands: list[dict[str, Any]]): - self._commands = [CommandDefinition(**command) for command in commands] - - def __iter__(self) -> Iterator[CommandDefinition]: - return self._commands.__iter__() - - def __contains__(self, name: str) -> bool: - return self.__getitem__(name) is not None - - def __getitem__(self, command: str) -> CommandDefinition | None: - return next((cd for cd in self._commands if cd.command_name == command), None) - - def __len__(self) -> int: - return len(self._commands) - - get = __getitem__ - - -@define(init=False, kw_only=True) -class State: - name: str - type: DataType - value: StateType - - def __init__( - self, - name: str, - type: int, - value: StateType = None, - **_: Any, - ): - self.name = name - self.value = value - self.type = DataType(type) - - @property - def value_as_int(self) -> int | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.INTEGER: - return cast(int, self.value) - raise TypeError(f"{self.name} is not an integer") - - @property - def value_as_float(self) -> float | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.FLOAT: - return cast(float, self.value) - raise TypeError(f"{self.name} is not a float") - - @property - def value_as_bool(self) -> bool | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.BOOLEAN: - return cast(bool, self.value) - raise TypeError(f"{self.name} is not a boolean") - - @property - def value_as_str(self) -> str | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.STRING: - return cast(str, self.value) - raise TypeError(f"{self.name} is not a string") - - @property - def value_as_dict(self) -> dict[str, Any] | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.JSON_OBJECT: - return cast(dict, self.value) - raise TypeError(f"{self.name} is not a JSON object") - - @property - def value_as_list(self) -> list[Any] | None: - if self.type == DataType.NONE: - return None - if self.type == DataType.JSON_ARRAY: - return cast(list, self.value) - raise TypeError(f"{self.name} is not an array") - - -@define(init=False, kw_only=True) -class EventState(State): - def __init__( - self, - name: str, - type: int, - value: str | None = None, - **_: Any, - ): - super().__init__(name, type, value, **_) - - # Overkiz (cloud) returns all state values as a string - # We cast them here based on the data type provided by Overkiz - # Overkiz (local) returns all state values in the right format - if isinstance(self.value, str) and self.type in DATA_TYPE_TO_PYTHON: - self.value = DATA_TYPE_TO_PYTHON[self.type](self.value) - - -@define(init=False) -class States: - _states: list[State] - - def __init__(self, states: list[dict[str, Any]] | None = None) -> None: - if states: - self._states = [State(**state) for state in states] - else: - self._states = [] - - def __iter__(self) -> Iterator[State]: - return self._states.__iter__() - - def __contains__(self, name: str) -> bool: - return self.__getitem__(name) is not None - - def __getitem__(self, name: str) -> State | None: - return next((state for state in self._states if state.name == name), None) - - def __setitem__(self, name: str, state: State) -> None: - found = self.__getitem__(name) - if found is None: - self._states.append(state) - else: - self._states[self._states.index(found)] = state - - def __len__(self) -> int: - return len(self._states) - - get = __getitem__ - - -class Command(dict): - """Represents an OverKiz Command.""" - - name: str - parameters: list[str | int | float] | None - - def __init__( - self, name: str, parameters: list[str | int | float] | None = None, **_: Any - ): - self.name = name - self.parameters = parameters - dict.__init__(self, name=name, parameters=parameters) - - -@define(init=False, kw_only=True) -class Event: - name: EventName - timestamp: int | None - setupoid: str | None = field(repr=obfuscate_id, default=None) - owner_key: str | None = field(repr=obfuscate_id, default=None) - type: int | None = None - sub_type: int | None = None - time_to_next_state: int | None = None - failed_commands: list[dict[str, Any]] | None = None - failure_type_code: FailureType | None = None - failure_type: str | None = None - condition_groupoid: str | None = None - place_oid: str | None = None - label: str | None = None - metadata: Any | None = None - camera_id: str | None = None - deleted_raw_devices_count: Any | None = None - protocol_type: Any | None = None - gateway_id: str | None = field(repr=obfuscate_id, default=None) - exec_id: str | None = None - device_url: str | None = field(repr=obfuscate_id, default=None) - device_states: list[EventState] - old_state: ExecutionState | None = None - new_state: ExecutionState | None = None - - def __init__( - self, - name: EventName, - timestamp: int | None = None, - setupoid: str | None = field(repr=obfuscate_id, default=None), - owner_key: str | None = None, - type: int | None = None, - sub_type: int | None = None, - time_to_next_state: int | None = None, - failed_commands: list[dict[str, Any]] | None = None, - failure_type_code: FailureType | None = None, - failure_type: str | None = None, - condition_groupoid: str | None = None, - place_oid: str | None = None, - label: str | None = None, - metadata: Any | None = None, - camera_id: str | None = None, - deleted_raw_devices_count: Any | None = None, - protocol_type: Any | None = None, - gateway_id: str | None = None, - exec_id: str | None = None, - device_url: str | None = None, - device_states: list[dict[str, Any]] | None = None, - old_state: ExecutionState | None = None, - new_state: ExecutionState | None = None, - **_: Any, - ): - self.timestamp = timestamp - self.gateway_id = gateway_id - self.exec_id = exec_id - self.device_url = device_url - self.device_states = ( - [EventState(**s) for s in device_states] if device_states else [] - ) - self.old_state = ExecutionState(old_state) if old_state else None - self.new_state = ExecutionState(new_state) if new_state else None - self.setupoid = setupoid - self.owner_key = owner_key - self.type = type - self.sub_type = sub_type - self.time_to_next_state = time_to_next_state - self.failed_commands = failed_commands - - self.failure_type = failure_type - self.condition_groupoid = condition_groupoid - self.place_oid = place_oid - self.label = label - self.metadata = metadata - self.camera_id = camera_id - self.deleted_raw_devices_count = deleted_raw_devices_count - self.protocol_type = protocol_type - self.name = EventName(name) - self.failure_type_code = ( - None if failure_type_code is None else FailureType(failure_type_code) - ) - - -@define(init=False, kw_only=True) -class Execution: - id: str - description: str - owner: str = field(repr=obfuscate_email) - state: str - action_group: list[dict[str, Any]] - - def __init__( - self, - id: str, - description: str, - owner: str, - state: str, - action_group: list[dict[str, Any]], - **_: Any, - ): - self.id = id - self.description = description - self.owner = owner - self.state = state - self.action_group = action_group - - -@define(init=False, kw_only=True) -class Scenario: - label: str = field(repr=obfuscate_string) - oid: str = field(repr=obfuscate_id) - - def __init__(self, label: str, oid: str, **_: Any): - self.label = label - self.oid = oid - - -@define(init=False, kw_only=True) -class Partner: - activated: bool - name: str - id: str = field(repr=obfuscate_id) - status: str - - def __init__(self, activated: bool, name: str, id: str, status: str, **_: Any): - self.activated = activated - self.name = name - self.id = id - self.status = status - - -@define(init=False, kw_only=True) -class Connectivity: - status: str - protocol_version: str - - def __init__(self, status: str, protocol_version: str, **_: Any): - self.status = status - self.protocol_version = protocol_version - - -@define(init=False, kw_only=True) -class Gateway: - partners: list[Partner] - functions: str | None = None - sub_type: GatewaySubType | None = None - id: str = field(repr=obfuscate_id) - gateway_id: str = field(repr=obfuscate_id) - alive: bool | None = None - mode: str | None = None - place_oid: str | None = None - time_reliable: bool | None = None - connectivity: Connectivity - up_to_date: bool | None = None - update_status: UpdateBoxStatus | None = None - sync_in_progress: bool | None = None - type: GatewayType | None = None - - def __init__( - self, - *, - partners: list[dict[str, Any]] | None = None, - functions: str | None = None, - sub_type: GatewaySubType | None = None, - gateway_id: str, - alive: bool | None = None, - mode: str | None = None, - place_oid: str | None = None, - time_reliable: bool | None = None, - connectivity: dict[str, Any], - up_to_date: bool | None = None, - update_status: UpdateBoxStatus | None = None, - sync_in_progress: bool | None = None, - type: GatewayType | None = None, - **_: Any, - ) -> None: - self.id = gateway_id - self.gateway_id = gateway_id - self.functions = functions - self.alive = alive - self.mode = mode - self.place_oid = place_oid - self.time_reliable = time_reliable - self.connectivity = Connectivity(**connectivity) - self.up_to_date = up_to_date - self.update_status = UpdateBoxStatus(update_status) if update_status else None - self.sync_in_progress = sync_in_progress - self.partners = [Partner(**p) for p in partners] if partners else [] - self.type = GatewayType(type) if type else None - self.sub_type = GatewaySubType(sub_type) if sub_type else None - - -@define(init=False, kw_only=True) -class HistoryExecutionCommand: - device_url: str = field(repr=obfuscate_id) - command: str - rank: int - dynamic: bool - state: ExecutionState - failure_type: str - parameters: list[Any] | None = None - - def __init__( - self, - device_url: str, - command: str, - rank: int, - dynamic: bool, - state: ExecutionState, - failure_type: str, - parameters: list[Any] | None = None, - **_: Any, - ) -> None: - self.device_url = device_url - self.command = command - self.parameters = parameters - self.rank = rank - self.dynamic = dynamic - self.state = ExecutionState(state) - self.failure_type = failure_type - - -@define(init=False, kw_only=True) -class HistoryExecution: - id: str - event_time: int - owner: str = field(repr=obfuscate_email) - source: str - end_time: int | None = None - effective_start_time: int | None = None - duration: int - label: str | None = None - type: str - state: ExecutionState - failure_type: str - commands: list[HistoryExecutionCommand] - execution_type: ExecutionType - execution_sub_type: ExecutionSubType - - def __init__( - self, - *, - id: str, - event_time: int, - owner: str, - source: str, - end_time: int | None = None, - effective_start_time: int | None = None, - duration: int, - label: str | None = None, - type: str, - state: ExecutionState, - failure_type: str, - commands: list[dict[str, Any]], - execution_type: ExecutionType, - execution_sub_type: ExecutionSubType, - **_: Any, - ) -> None: - self.id = id - self.event_time = event_time - self.owner = owner - self.source = source - self.end_time = end_time - self.effective_start_time = effective_start_time - self.duration = duration - self.label = label - self.type = type - self.state = ExecutionState(state) - self.failure_type = failure_type - self.commands = [HistoryExecutionCommand(**hec) for hec in commands] - self.execution_type = ExecutionType(execution_type) - self.execution_sub_type = ExecutionSubType(execution_sub_type) - - -@define(init=False, kw_only=True) -class Place: - creation_time: int - last_update_time: int | None = None - label: str - type: int - id: str - oid: str - sub_places: list[Place] - - def __init__( - self, - *, - creation_time: int, - last_update_time: int | None = None, - label: str, - type: int, - oid: str, - sub_places: list[Any] | None, - **_: Any, - ) -> None: - self.id = oid - self.creation_time = creation_time - self.last_update_time = last_update_time - self.label = label - self.type = type - self.oid = oid - self.sub_places = [Place(**p) for p in sub_places] if sub_places else [] - - -@define(kw_only=True) -class Feature: - name: str - source: str - - -@define(kw_only=True) -class ZoneItem: - item_type: str - device_oid: str - device_url: str - - -@define(init=False, kw_only=True) -class Zone: - creation_time: str - last_update_time: str - label: str - type: int - items: list[ZoneItem] | None - external_oid: str | None - metadata: str | None - oid: str - - def __init__( - self, - *, - last_update_time: str, - label: str, - type: int, - items: list[dict[str, Any]] | None, - external_oid: str | None = None, - metadata: str | None = None, - oid: str, - **_: Any, - ) -> None: - self.last_update_time = last_update_time - self.label = label - self.type = type - self.items = [ZoneItem(**z) for z in items] if items else [] - self.external_oid = external_oid - self.metadata = metadata - self.oid = oid - - -@define(kw_only=True) -class OverkizServer: - """Class to describe an Overkiz server.""" - - name: str - endpoint: str - manufacturer: str - configuration_url: str | None - - -@define(kw_only=True) -class LocalToken: - label: str - gateway_id: str = field(repr=obfuscate_id, default=None) - expiration_time: int - gateway_creation_time: int - uuid: str - scope: str - - -@define(kw_only=True) -class OptionParameter: - name: str - value: str - - -@define(init=False, kw_only=True) -class Option: - creation_time: int - last_update_time: int - option_id: str - start_date: int - parameters: list[OptionParameter] | None - - def __init__( - self, - *, - creation_time: int, - last_update_time: int, - option_id: str, - start_date: int, - parameters: list[dict[str, Any]] | None, - **_: Any, - ) -> None: - self.creation_time = creation_time - self.last_update_time = last_update_time - self.option_id = option_id - self.start_date = start_date - self.parameters = ( - [OptionParameter(**p) for p in parameters] if parameters else [] - ) diff --git a/pyoverkiz/obfuscate.py b/pyoverkiz/obfuscate.py deleted file mode 100644 index c9deff3..0000000 --- a/pyoverkiz/obfuscate.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Utils for Overkiz client""" -from __future__ import annotations - -import re -from typing import Any - -from pyoverkiz.types import JSON - - -def obfuscate_id(id: str | None) -> str: - """Mask id""" - return re.sub(r"(SETUP)?\d+-", "****-", str(id)) - - -def obfuscate_email(email: str | None) -> str: - """Mask email""" - email = str(email).replace("_-_", "@") # Replace @ for _-_ (Nexity) - return re.sub(r"(.).*@.*(.\..*)", r"\1****@****\2", email) - - -def obfuscate_string(input: str) -> str: - """Mask string""" - return re.sub(r"[a-zA-Z0-9_.-]*", "*", str(input)) - - -def obfuscate_sensitive_data(data: dict[str, Any]) -> JSON: - """Mask Overkiz JSON data to remove sensitive data""" - mask_next_value = False - - for key, value in data.items(): - if key in ["gatewayId", "id", "deviceURL"]: - data[key] = obfuscate_id(value) - - if key in [ - "label", - "city", - "country", - "postalCode", - "addressLine1", - "addressLine2", - "longitude", - "latitude", - ]: - data[key] = obfuscate_string(value) - - if value in [ - "core:NameState", - "homekit:SetupCode", - "homekit:SetupPayload", - "core:SSIDState", - ]: - mask_next_value = True - - if mask_next_value and key == "value": - data[key] = obfuscate_string(value) - mask_next_value = False - - # Mask homekit:SetupCode and homekit:SetupPayload - if isinstance(value, dict): - obfuscate_sensitive_data(value) - elif isinstance(value, list): - for val in value: - if isinstance(val, str): - continue - if isinstance(val, int): - continue - if isinstance(val, float): - continue - if isinstance(val, list): - continue - - obfuscate_sensitive_data(val) - - return data diff --git a/pyoverkiz/overkiz-root-ca-2048.crt b/pyoverkiz/overkiz-root-ca-2048.crt deleted file mode 100644 index 9d9adef..0000000 --- a/pyoverkiz/overkiz-root-ca-2048.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPDCCAiSgAwIBAgIJAOvswclbF4QnMA0GCSqGSIb3DQEBCwUAMEsxEDAOBgNV -BAoTB092ZXJraXoxEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD092ZXJraXog -Um9vdCBDQTELMAkGA1UEBhMCRlIwHhcNMTYwNDI3MTI1ODE1WhcNMzYwNDI3MTI1 -ODE1WjBLMRAwDgYDVQQKEwdPdmVya2l6MRAwDgYDVQQLEwdSb290IENBMRgwFgYD -VQQDEw9PdmVya2l6IFJvb3QgQ0ExCzAJBgNVBAYTAkZSMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAsszRfbcNCEoD9ZfzTbXfuMK8CrXGqBR/ZOCk4guN -aRKqZ/rLQm3V6Q+dlJ/8qle5J3KN5bZmqT4qXKHwJsaOiLfPyAptSM6vuIlls2N+ -UsKkv3m5+gTyLaSGMS4wh2GoOCa21V9t5wYUQnoFaQByVNyl+kkrWLpKw5gQasU0 -xkVsjAVKgkkb3puBl7sZgiSoz97I9U9JUkg3spH0I84CZRI+JejioDHvkZEyf83j -+QFxSTV/hZkUwUY/X0zt2dTZuliCTePeCdANryo6+9TbBp98j/SB1s59FcO8NSSK -sV07rTFlM9/soko2/J0aTtXHE86wFq7vfFVZzZxsQpIBbwIDAQABoyMwITAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEA -GU7xUWlZZEVnEK0k3Z1FoRl9xA7cLOiDVCQ5qQDFfQgGpMtXv1PKsQNZ6T6tZN3d -bdzsqcQXtLhXknz6aGBZNR4g6liQhVuCaiyURaI+LM2KuSZnbixs3+1SPBvxHrJh -/gOsxctxq+0DALnOK9qbGl6N5DtjM/EC5Qve71c+UVTEcJjJ3L2S1Ne+PxDOJuUC -JsOLUk96G+uLn6CQB5Wu8fYrkWAjF3yrxkCoZCqOvVrnbL77vXmz2mlqNHSJt3Ur -ndWJLVvrRFKdSG6WiNCh/Q+ARQAorN60JD9x8+IyXRGvlZl7KVeRduE2rjZuom7h -QLMnmaF+oFW5mnhh9gu6Gg== ------END CERTIFICATE----- diff --git a/pyoverkiz/py.typed b/pyoverkiz/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/pyoverkiz/types.py b/pyoverkiz/types.py deleted file mode 100644 index 5b3feed..0000000 --- a/pyoverkiz/types.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -import json -from typing import Any, Callable, Dict, List, Union - -from pyoverkiz.enums import DataType - -StateType = Union[str, int, float, bool, Dict[str, Any], List[Any], None] - - -DATA_TYPE_TO_PYTHON: dict[DataType, Callable[[Any], StateType]] = { - DataType.INTEGER: int, - DataType.DATE: int, - DataType.FLOAT: float, - DataType.BOOLEAN: bool, - DataType.JSON_ARRAY: json.loads, - DataType.JSON_OBJECT: json.loads, -} - -JSON = Union[Dict[str, Any], List[Dict[str, Any]]] # pylint: disable=invalid-name diff --git a/pyoverkiz/utils.py b/pyoverkiz/utils.py deleted file mode 100644 index a5ce52d..0000000 --- a/pyoverkiz/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import re - -from pyoverkiz.const import LOCAL_API_PATH -from pyoverkiz.models import OverkizServer - - -def generate_local_server( - host: str, - name: str = "Somfy Developer Mode", - manufacturer: str = "Somfy", - configuration_url: str | None = None, -) -> OverkizServer: - """Generate OverkizServer class for connection with a local API (Somfy Developer mode).""" - return OverkizServer( - name=name, - endpoint=f"https://{host}{LOCAL_API_PATH}", - manufacturer=manufacturer, - configuration_url=configuration_url, - ) - - -def is_overkiz_gateway(gateway_id: str) -> bool: - "Return if gateway is Overkiz gateway. Can be used to distinguish between the main gateway and an additional gateway." - return bool(re.match(r"\d{4}-\d{4}-\d{4}", gateway_id)) diff --git a/requirements.txt b/requirements.txt index 7e5c9b5..db1e72d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ jmespath>=1.0.1 multidict>=6.0.4 pyasn1>=0.4.8 pyhumps>=3.8.0 -pyoverkiz>=1.7.4 +pyoverkiz==2.1.0 python-dateutil>=2.8.2 python-jose>=3.3.0 requests>=2.28.2 diff --git a/requirements_tahoma-gpt.txt b/requirements_tahoma-gpt.txt index d9fa4b1..4462e41 100644 --- a/requirements_tahoma-gpt.txt +++ b/requirements_tahoma-gpt.txt @@ -38,7 +38,7 @@ protobuf==4.23.3 psutil==5.9.5 pyasn1==0.5.0 pyhumps==3.8.0 -pyoverkiz==1.9.0 +pyoverkiz==2.1.0 pyparsing==3.1.0 python-dateutil==2.8.2 python-jose==3.3.0 diff --git a/tahoma.py b/tahoma.py index e2bab42..c3a0b48 100644 --- a/tahoma.py +++ b/tahoma.py @@ -22,9 +22,8 @@ import datetime from pyoverkiz.client import OverkizClient, Command from pyoverkiz.enums import OverkizCommand -from pyoverkiz.models import Command -from pyoverkiz.models import Scenario -from pyoverkiz.exceptions import NotAuthenticatedException +from pyoverkiz.models import Command, Action +from pyoverkiz.exceptions import NotAuthenticatedError from aiohttp.client_exceptions import ClientConnectorError import requests import base64 @@ -622,21 +621,21 @@ def remove_accent(old): exit() success = 1 if remove_accent(action).upper() == "OPEN" or remove_accent(action).upper() == "OUVRIR" : - fonction = Command(OverkizCommand.OPEN, [0]) + fonction = Command(name=OverkizCommand.OPEN, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'CLOSE' or remove_accent(action).upper() == "FERMER" : - fonction = Command(OverkizCommand.CLOSE, [0]) + fonction = Command(name=OverkizCommand.CLOSE, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' ACTION is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, []) + fonction = Command(name=OverkizCommand.STOP, parameters=[]) success = 0 elif remove_accent(action).upper() == 'MY' : - fonction = Command(OverkizCommand.MY, [0]) + fonction = Command(name=OverkizCommand.MY, parameters=[0]) success = 0 elif str(action).isnumeric() == True : if 0 <= int(action) <= 100 : - fonction = Command(OverkizCommand.SET_CLOSURE, [int(action)]) + fonction = Command(name=OverkizCommand.SET_CLOSURE, parameters=[int(action)]) success = 0 print('Will close to '+str(action)+' %') print("Be careful! This function is only available for IO protocols. It doesn't work with RTS devices...") @@ -691,21 +690,21 @@ def remove_accent(old): exit() success = 1 if remove_accent(action).upper() == "OPEN" or remove_accent(action).upper() == "OUVRIR" : - fonction = Command(OverkizCommand.OPEN, [0]) + fonction = Command(name=OverkizCommand.OPEN, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'CLOSE' or remove_accent(action).upper() == "FERMER" : - fonction = Command(OverkizCommand.CLOSE, [0]) + fonction = Command(name=OverkizCommand.CLOSE, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' function is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, []) + fonction = Command(name=OverkizCommand.STOP, parameters=[]) success = 0 elif remove_accent(action).upper() == 'MY' : - fonction = Command(OverkizCommand.MY, [0]) + fonction = Command(name=OverkizCommand.MY, parameters=[0]) success = 0 elif str(action).isnumeric() == True : if 0 <= int(action) <= 100 : - fonction = Command(OverkizCommand.SET_CLOSURE, [int(action)]) + fonction = Command(name=OverkizCommand.SET_CLOSURE, parameters=[int(action)]) success = 0 print('Will close to '+str(action)+' %') print("Be careful! This function is only available for IO protocols. It doesn't work with RTS devices...") @@ -766,25 +765,25 @@ def remove_accent(old): is_bioclimatic = 'BioclimaticPergola' in widget_of[0] success = 1 if remove_accent(action).upper() == "OPEN" or remove_accent(action).upper() == "OUVRIR" : - fonction = Command(OverkizCommand.OPEN_SLATS, []) if is_bioclimatic else Command(OverkizCommand.OPEN, [0]) + fonction = Command(name=OverkizCommand.OPEN_SLATS, parameters=[]) if is_bioclimatic else Command(name=OverkizCommand.OPEN, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'CLOSE' or remove_accent(action).upper() == "FERMER" : - fonction = Command(OverkizCommand.CLOSE_SLATS, []) if is_bioclimatic else Command(OverkizCommand.CLOSE, [0]) + fonction = Command(name=OverkizCommand.CLOSE_SLATS, parameters=[]) if is_bioclimatic else Command(name=OverkizCommand.CLOSE, parameters=[0]) success = 0 elif remove_accent(action).upper() == 'STOP' : print("Please note that the 'stop' function is only compatible with IO protocols and will not work with RTS devices. If you are using an RTS device, please use the command 'tahoma CANCEL LAST ACTION' instead.") - fonction = Command(OverkizCommand.STOP, []) + fonction = Command(name=OverkizCommand.STOP, parameters=[]) success = 0 elif remove_accent(action).upper() == 'MY' : - fonction = Command(OverkizCommand.MY, [0]) + fonction = Command(name=OverkizCommand.MY, parameters=[0]) success = 0 elif str(action).isnumeric() == True : if 0 <= int(action) <= 100 : if is_bioclimatic : - fonction = Command(OverkizCommand.SET_ORIENTATION, [int(action)]) + fonction = Command(name=OverkizCommand.SET_ORIENTATION, parameters=[int(action)]) print('Will set orientation to '+str(action)+' %') else : - fonction = Command(OverkizCommand.SET_CLOSURE, [int(action)]) + fonction = Command(name=OverkizCommand.SET_CLOSURE, parameters=[int(action)]) print('Will close to '+str(action)+' %') print("Be careful! This function is only available for IO protocols. It doesn't work with RTS devices...") success = 0 @@ -857,10 +856,10 @@ def remove_accent(old): success = 1 if remove_accent(action).upper() == "ON" or remove_accent(action).upper() == "ALLUMER" : - fonction = Command(OverkizCommand.ON) + fonction = Command(name=OverkizCommand.ON) success = 0 elif remove_accent(action).upper() == 'OFF' or remove_accent(action).upper() == "ETEINDRE" : - fonction = Command(OverkizCommand.OFF) + fonction = Command(name=OverkizCommand.OFF) success = 0 elif remove_accent(action).upper() == 'TOGGLE' or remove_accent(action).upper() == "BASCULER" : f = open(list_of_tahoma_states, 'r') @@ -874,10 +873,10 @@ async def state() -> None: get_state = await asyncio.wait_for( client.get_state(str(url[0])), timeout=10.0) # print(str(get_state).upper()) if "'OFF'" in str(get_state).upper(): - fonction = Command(OverkizCommand.ON) + fonction = Command(name=OverkizCommand.ON) return fonction else: - fonction = Command(OverkizCommand.OFF) + fonction = Command(name=OverkizCommand.OFF) return fonction overkiz_fonction = asyncio.run(state()) fonction = overkiz_fonction @@ -932,19 +931,19 @@ async def state() -> None: exit() success = 1 if remove_accent(action).upper() == "ARM" or remove_accent(action).upper() == "ACTIVER" or remove_accent(action).upper() == "ON": - fonction = Command(OverkizCommand.ARM) + fonction = Command(name=OverkizCommand.ARM) success = 0 elif remove_accent(action).upper() == 'DISARM' or remove_accent(action).upper() == "DESACTIVER" or remove_accent(action).upper() == "OFF" : - fonction = Command(OverkizCommand.DISARM) + fonction = Command(name=OverkizCommand.DISARM) success = 0 elif remove_accent(action).upper() == 'PARTIAL' or remove_accent(action).upper() == "PARTIEL" : - fonction = Command(OverkizCommand.PARTIAL) + fonction = Command(name=OverkizCommand.PARTIAL) success = 0 elif remove_accent(action).upper() == 'ARM_NIGHT' or remove_accent(action).upper() == "ACTIVER_NUIT" : - fonction = Command(OverkizCommand.ARM_NIGHT) + fonction = Command(name=OverkizCommand.ARM_NIGHT) success = 0 elif remove_accent(action).upper() == 'ARM_AWAY' or remove_accent(action).upper() == "ACTIVER_PARTI" : - fonction = Command(OverkizCommand.ARM_AWAY) + fonction = Command(name=OverkizCommand.ARM_AWAY) success = 0 else : print( "\n'"+action+"'"+" is not a valide action.\n") @@ -997,27 +996,27 @@ async def state() -> None: exit() success = 1 if remove_accent(action).lower() == "comfort" or remove_accent(action).lower() == "confort" : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['comfort']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['comfort']) success = 0 elif remove_accent(action).lower() == 'frostprotection' or remove_accent(action).lower() == "horsgel" : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['frostprotection']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['frostprotection']) success = 0 elif remove_accent(action).lower() == 'comfort-1' or remove_accent(action).lower() == "confort-1" : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['comfort-1']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['comfort-1']) success = 0 elif remove_accent(action).lower() == 'comfort-2' or remove_accent(action).lower() == "confort-2" : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['comfort-2']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['comfort-2']) success = 0 elif remove_accent(action).lower() == 'eco' : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['eco']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['eco']) success = 0 elif remove_accent(action).lower() == 'off' or remove_accent(action).lower() == "eteindre" : - fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['off']) + fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['off']) success = 0 elif remove_accent(action).lower() == 'standby' or remove_accent(action).lower() == "veille" : if "adjustable" in widget_heater.lower() or "setpoint" in widget_heater.lower() : # if widget_heater == "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" : - fonction = Command(OverkizCommand.SET_OPERATING_MODE, ['standby']) + fonction = Command(name=OverkizCommand.SET_OPERATING_MODE, parameters=['standby']) success = 0 else: str1 = " " @@ -1035,7 +1034,7 @@ async def state() -> None: elif remove_accent(action).lower() == 'manual' or remove_accent(action).lower() == "manuel" : if "adjustable" in widget_heater.lower() or "setpoint" in widget_heater.lower() : # if widget_heater == "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" : - fonction = Command(OverkizCommand.SET_OPERATING_MODE, ['basic']) + fonction = Command(name=OverkizCommand.SET_OPERATING_MODE, parameters=['basic']) success = 0 else: str1 = " " @@ -1053,7 +1052,7 @@ async def state() -> None: elif remove_accent(action).lower() == 'prog' : if "adjustable" in widget_heater.lower() or "setpoint" in widget_heater.lower() : # if widget_heater == "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" : - fonction = Command(OverkizCommand.SET_OPERATING_MODE, ['internal']) + fonction = Command(name=OverkizCommand.SET_OPERATING_MODE, parameters=['internal']) success = 0 else: str1 = " " @@ -1071,7 +1070,7 @@ async def state() -> None: elif remove_accent(action).lower() == 'auto' : if "adjustable" in widget_heater.lower() or "setpoint" in widget_heater.lower() : # if widget_heater == "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" : - fonction = Command(OverkizCommand.SET_OPERATING_MODE, ['auto']) + fonction = Command(name=OverkizCommand.SET_OPERATING_MODE, parameters=['auto']) success = 0 else: str1 = " " @@ -1089,7 +1088,7 @@ async def state() -> None: elif str(action).replace(".","").isnumeric(): if "adjustable" in widget_heater.lower() or "setpoint" in widget_heater.lower() : # if widget_heater == "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint" : - fonction = Command(OverkizCommand.SET_TARGET_TEMPERATURE, [float(str(action))]) + fonction = Command(name=OverkizCommand.SET_TARGET_TEMPERATURE, parameters=[float(str(action))]) success = 0 else: str1 = " " @@ -1155,7 +1154,7 @@ async def state() -> None: exit() - #fonction = Command(OverkizCommand.SET_HEATING_LEVEL,['comfort']) + #fonction = Command(name=OverkizCommand.SET_HEATING_LEVEL, parameters=['comfort']) #exec_id = await client.execute_scenario(device_url) str1 = " " @@ -1254,10 +1253,10 @@ async def get_token_or_gateway_id() -> None: gateway_id = "" if not gateway_id: for gateway in gateways: - token2 = await client.generate_local_token(gateway.id) - await client.activate_local_token(gateway_id=gateway.id, token=token2, label="tahoma by @pzim-devdata") - if all(char.isdigit() or char == '-' for char in gateway.id) and gateway.id : - gateway_id_list.append(gateway.id) + token2 = await client.generate_local_token(gateway.gateway_id) + await client.activate_local_token(gateway_id=gateway.gateway_id, token=token2, label="tahoma by @pzim-devdata") + if all(char.isdigit() or char == '-' for char in gateway.gateway_id) and gateway.gateway_id : + gateway_id_list.append(gateway.gateway_id) #If many tahoma gateways, choosing the first one: if len(gateway_id_list) > 1: print("You have more than one tahoma gateway, the process will be executed with the first one :") @@ -1366,10 +1365,10 @@ async def main() -> None: async with tahoma_config.build_client('remote', app_config) as client: # async with client_factory() as client: await client.login() - exec_id = await client.execute_scenario(device_url) + exec_id = await client.execute_persisted_action_group(device_url) # error = 0 # return error -# except (NotAuthenticatedException,ClientConnectorError) as e: +# except (NotAuthenticatedError,ClientConnectorError) as e: # print(e) # error = 1 # return error @@ -1409,8 +1408,8 @@ async def main() -> None: for execution in executions: execution_id=str(execution.id) try: - await client.cancel_command(str(execution_id)) - message="The last command: '"+execution.action_group['actions'][0]['commands'][0]['name']+"' has been successfully cancelled." + await client.cancel_execution(str(execution_id)) + message="The last command: '"+str(execution.action_group.actions[0].commands[0].name)+"' has been successfully cancelled." if logs == 'Y': try: with open(log_place, "a") as f: @@ -1430,7 +1429,7 @@ async def main() -> None: # print("Local API connection succesfull!") # print(str(fonction)) # print(device_url) - exec_id = await client.execute_command( device_url, fonction ) + exec_id = await client.execute_action_group( actions=[Action(device_url=device_url, commands=[fonction])] ) error = 0 # print("execution") try: @@ -1443,7 +1442,7 @@ async def main() -> None: await session.close() except: pass return error - except (NotAuthenticatedException,ClientConnectorError) as e: + except (NotAuthenticatedError,ClientConnectorError) as e: print(e) error = 1 try: diff --git a/tahoma_config.py b/tahoma_config.py index 9375c0f..5398167 100644 --- a/tahoma_config.py +++ b/tahoma_config.py @@ -9,8 +9,11 @@ from dataclasses import dataclass from hashlib import sha256 +from pyoverkiz.auth.credentials import LocalTokenCredentials, UsernamePasswordCredentials from pyoverkiz.client import OverkizClient -from pyoverkiz.const import SUPPORTED_SERVERS, OverkizServer +from pyoverkiz.const import LOCAL_API_PATH, SUPPORTED_SERVERS +from pyoverkiz.enums.server import APIType +from pyoverkiz.models import ServerConfig @dataclass @@ -138,16 +141,18 @@ def build_client(mode, config, session=None, verify_ssl=False): """Construct a real OverkizClient directly - no eval().""" if mode == 'local': return OverkizClient( - username="", - password="", - token=config.token, + credentials=LocalTokenCredentials(token=config.token), verify_ssl=verify_ssl, session=session, - server=OverkizServer( + server=ServerConfig( name="Somfy TaHoma (local)", - endpoint=f"https://gateway-{config.gateway_id}.local:8443/enduser-mobile-web/1/enduserAPI/", + endpoint=f"https://gateway-{config.gateway_id}.local:8443{LOCAL_API_PATH}", manufacturer="Somfy", + api_type=APIType.LOCAL, configuration_url=None, ), ) - return OverkizClient(config.username, config.password, SUPPORTED_SERVERS[config.server]) + return OverkizClient( + credentials=UsernamePasswordCredentials(username=config.username, password=config.password), + server=SUPPORTED_SERVERS[config.server], + ) From 63a311b92b3b7915189749e5759d023bc6a40d20 Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 11:17:43 +0200 Subject: [PATCH 6/8] refactor with tests --- .github/workflows/ci.yml | 29 ++++++ get_devices_url.py | 17 +--- ruff.toml | 7 ++ tahoma.py | 14 +-- test/__init__.py | 5 - test/test.py | 5 - test/test_tahoma_config.py | 189 +++++++++++++++++++++++++++++++++++++ 7 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 ruff.toml delete mode 100644 test/__init__.py delete mode 100644 test/test.py create mode 100644 test/test_tahoma_config.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7691428 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - run: pip install -r requirements.txt + - run: pip install pytest + + - name: Lint (ruff) + uses: astral-sh/ruff-action@v3 + + - name: Unit tests (pytest) + run: python -m pytest -v diff --git a/get_devices_url.py b/get_devices_url.py index 0cdaa01..78d04d1 100644 --- a/get_devices_url.py +++ b/get_devices_url.py @@ -5,27 +5,18 @@ #MIT Licence import asyncio -import sys import argparse import os import requests import aiohttp -from datetime import datetime, timedelta +from datetime import datetime import json from collections import defaultdict -import re -from getpass import getpass -import time -from pyoverkiz.enums import OverkizCommand -from pyoverkiz.models import Command -from pyoverkiz.models import PersistedActionGroup import base64 import tahoma_config try: - import __version__ str_newrelic='Github' except: - from tahoma import __version__ str_newrelic='Pypi' @@ -172,7 +163,7 @@ def update_file(url, content, sha, message): # Mise à jour du fichier stats stats_content = json.dumps(current_content, indent=2) - put_response = update_file(stats_url, stats_content, sha, f"Update stats ({machine_id[:6]})") + update_file(stats_url, stats_content, sha, f"Update stats ({machine_id[:6]})") # Création des statistiques mensuelles monthly_stats = defaultdict(lambda: { @@ -321,7 +312,7 @@ def update_file(url, content, sha, message): devices = await client.get_devices() scenarios = await client.get_action_groups() try : - f2.write(f"Devices :\n") + f2.write("Devices :\n") for device in devices: print("\n"+device.label+","+device.device_url+","+device.widget+","+device.ui_class+","+device.controllable_name+"") print(f"{device.label},{device.device_url},{device.widget},{device.ui_class},{device.controllable_name}") @@ -417,7 +408,7 @@ def update_file(url, content, sha, message): await client.close() print("\nScenes :\n") try : - f2.write(f"\nScenes :\n") + f2.write("\nScenes :\n") for scenario in scenarios: f2.write(f"{scenario.label},{scenario.oid}\n") f9.write(f"{scenario.label},{scenario.oid}\n") diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..0aec41a --- /dev/null +++ b/ruff.toml @@ -0,0 +1,7 @@ +extend-exclude = [ + "argparse.py", "os.py", "sys.py", "getpass.py", + "tahoma-gpt.py", "tahoma-gpt_french.py", +] + +[lint] +select = ["F"] diff --git a/tahoma.py b/tahoma.py index c3a0b48..934839b 100644 --- a/tahoma.py +++ b/tahoma.py @@ -20,7 +20,6 @@ from getpass import getpass import time import datetime -from pyoverkiz.client import OverkizClient, Command from pyoverkiz.enums import OverkizCommand from pyoverkiz.models import Command, Action from pyoverkiz.exceptions import NotAuthenticatedError @@ -98,7 +97,6 @@ def countdown(duration): def main(): icon_app = os.path.dirname(os.path.abspath(__file__))+'/icons/connected_house.png' - icon_chauffe_eau=os.path.dirname(os.path.abspath(__file__))+'/icons/water heater.png' passwd_file = os.path.dirname(os.path.abspath(__file__))+'/temp/identifier_file.txt' @@ -110,7 +108,6 @@ def main(): list_of_tahoma_plugs = os.path.dirname(os.path.abspath(__file__))+'/temp/plugs.txt' list_of_tahoma_sunscreens = os.path.dirname(os.path.abspath(__file__))+'/temp/sunscreens.txt' list_of_tahoma_scenes = os.path.dirname(os.path.abspath(__file__))+'/temp/scenarios.txt' - list_of_tahoma_sensors = os.path.dirname(os.path.abspath(__file__))+'/temp/sensors.txt' list_of_tahoma_states = os.path.dirname(os.path.abspath(__file__))+'/temp/states.txt' list_of_tahoma_lights = os.path.dirname(os.path.abspath(__file__))+'/temp/lights.txt' list_of_tahoma_pergolas = os.path.dirname(os.path.abspath(__file__))+'/temp/pergolas.txt' @@ -158,7 +155,7 @@ def main(): try: f = open(init_file, 'r') - init = f.read() + f.read() f.close() init_str=sha256(b"init").hexdigest() except: @@ -465,7 +462,7 @@ def info(): for i in master_list : bad_name.append(i.split(",")[0]) print("\nHere is the list of the installed devices for the "+category.upper()+" category :\n"+str(bad_name)) - except Exception as e: + except Exception: print("\nCan't obtain any device from the "+category.upper()+" category\nDid you downloaded the list of Tahoma's devices ?\nIf not, execute tahoma --getlist \nFor more info execute tahoma -h") print( '\nYou must provide a part of the NAME as argument \n The name must be a single and unique word, not taken by another device of the same category !\n For example if you have two devices called and you will need to choose <2> as device [NAME] for and not ).\n You can also use the full NAME with [""].\n For example ["Alarm 2"]\n See tahoma --list or tahoma --help for info.') exit() @@ -568,7 +565,6 @@ def remove_accent(old): if (remove_accent(str(args.action)).lower() == 'cancel' or remove_accent(str(args.action)).lower() == 'annuler' or ('cancel' in [remove_accent(str(args.suite[i])).lower() for i in range(0, len(args.suite), 3)]) or ('annuler' in [remove_accent(str(args.suite[i])).lower() for i in range(0, len(args.suite), 3)]) )and local_remote == 'local': print("Be careful !!!\n\nCan't perform a CANCEL action when using the local API of tahoma: \nRun tahoma with the '--remote' argument.\n\nThe program will start with the 'remote' parameter...\n") local_remote = "remote" - new_local_remote = "remote" ##########################PARAMETERING FUNCTION @@ -1365,7 +1361,7 @@ async def main() -> None: async with tahoma_config.build_client('remote', app_config) as client: # async with client_factory() as client: await client.login() - exec_id = await client.execute_persisted_action_group(device_url) + await client.execute_persisted_action_group(device_url) # error = 0 # return error # except (NotAuthenticatedError,ClientConnectorError) as e: @@ -1387,7 +1383,7 @@ async def main() -> None: async with tahoma_config.build_client('remote', app_config) as client: await client.login() # get_state = await client.get_state(device_url) - get_state = await asyncio.wait_for( client.get_state( device_url ), timeout=10.0) + get_state = await asyncio.wait_for( client.get_state( device_url ), timeout=10.0) # noqa: F841 - read via eval(state_function) below state_function=str(command_state[j]).replace("['","").replace("']","") message=str(good_name[j])+':'+str(eval(state_function)) if logs == 'Y': @@ -1429,7 +1425,7 @@ async def main() -> None: # print("Local API connection succesfull!") # print(str(fonction)) # print(device_url) - exec_id = await client.execute_action_group( actions=[Action(device_url=device_url, commands=[fonction])] ) + await client.execute_action_group( actions=[Action(device_url=device_url, commands=[fonction])] ) error = 0 # print("execution") try: diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index 3f2ff2d..0000000 --- a/test/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/test.py b/test/test.py deleted file mode 100644 index 6df5e96..0000000 --- a/test/test.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/python3 - - - - diff --git a/test/test_tahoma_config.py b/test/test_tahoma_config.py new file mode 100644 index 0000000..fd505c1 --- /dev/null +++ b/test/test_tahoma_config.py @@ -0,0 +1,189 @@ +"""Unit tests for tahoma_config.py - the shared config/dispatch module used by +tahoma.py and get_devices_url.py. See CLAUDE.md for background. + +These are network-free by design (no real gateway/cloud account needed) so +they can run in CI. Live device tests stay manual, per CLAUDE.md's testing +section. +""" + +import argparse +import asyncio +import importlib + +import pytest +from pyoverkiz.client import OverkizClient +from pyoverkiz.enums.server import APIType + +import tahoma_config + + +def make_args(**overrides): + defaults = dict(username=None, password=None, server=None, token=None, + pin=None, local=False, remote=False) + defaults.update(overrides) + return argparse.Namespace(**defaults) + + +# --- load_config ----------------------------------------------------------- + +def test_load_config_defaults_when_temp_missing(tmp_path): + config = tahoma_config.load_config(str(tmp_path)) + assert config.token == "" + assert config.gateway_id == "" + assert config.local_remote == "remote" + assert config.server == "somfy_europe" + assert config.username == "" + assert config.password == "" + + +def test_load_config_reads_temp_files(tmp_path): + temp = tmp_path / "temp" + temp.mkdir() + (temp / "token.txt").write_text("mytoken\n") + (temp / "gateway_id.txt").write_text("1234-5678-9012\n") + (temp / "local_remote.txt").write_text("local\n") + (temp / "server_choosen.txt").write_text("somfy_america\n") + + config = tahoma_config.load_config(str(tmp_path)) + assert config.token == "mytoken" + assert config.gateway_id == "1234-5678-9012" + assert config.local_remote == "local" + assert config.server == "somfy_america" + + +# --- apply_cli_overrides ----------------------------------------------------- + +def test_apply_cli_overrides_username_password_server(): + config = tahoma_config.Config() + tahoma_config.apply_cli_overrides( + config, make_args(username="bob", password="secret", server="somfy_america") + ) + assert config.username == "bob" + assert config.password == "secret" + assert config.server == "somfy_america" + + +def test_apply_cli_overrides_token_and_pin(): + config = tahoma_config.Config() + tahoma_config.apply_cli_overrides(config, make_args(token="tok", pin="0001-0002-0003")) + assert config.token == "tok" + assert config.gateway_id == "0001-0002-0003" + + +def test_apply_cli_overrides_local_and_remote_flags(): + config = tahoma_config.Config(local_remote="remote") + tahoma_config.apply_cli_overrides(config, make_args(local=True)) + assert config.local_remote == "local" + + tahoma_config.apply_cli_overrides(config, make_args(remote=True)) + assert config.local_remote == "remote" + + +def test_apply_cli_overrides_leaves_unset_fields_untouched(): + config = tahoma_config.Config(username="existing") + tahoma_config.apply_cli_overrides(config, make_args()) + assert config.username == "existing" + + +# --- resolve_local_capability ------------------------------------------------ + +def test_resolve_local_capability_remote_configured_stays_remote(): + config = tahoma_config.Config(local_remote="remote") + assert tahoma_config.resolve_local_capability(config) == "remote" + + +def test_resolve_local_capability_local_with_capable_category(): + config = tahoma_config.Config(local_remote="local") + mode = tahoma_config.resolve_local_capability( + config, category="pergola", action="stop", + local_capable_categories={"pergola", "shutter"}, + ) + assert mode == "local" + + +def test_resolve_local_capability_local_with_incapable_category_falls_back(capsys): + config = tahoma_config.Config(local_remote="local") + mode = tahoma_config.resolve_local_capability( + config, category="scene", action="run", + local_capable_categories={"pergola", "shutter"}, + ) + assert mode == "remote" + assert "not yet supported for local use" in capsys.readouterr().out + + +def test_resolve_local_capability_no_category_check_stays_local(): + config = tahoma_config.Config(local_remote="local") + assert tahoma_config.resolve_local_capability(config) == "local" + + +# --- check_local_credentials ------------------------------------------------- + +def test_check_local_credentials_ok_when_both_present(): + config = tahoma_config.Config(token="tok", gateway_id="0001-0002-0003") + ok, message = tahoma_config.check_local_credentials(config) + assert ok is True + assert message == "" + + +@pytest.mark.parametrize( + "token,gateway_id,expected_substrings", + [ + ("", "", ["token", "gateway_id"]), + ("tok", "", ["gateway_id"]), + ("", "0001-0002-0003", ["token"]), + ], +) +def test_check_local_credentials_reports_whats_missing(token, gateway_id, expected_substrings): + config = tahoma_config.Config(token=token, gateway_id=gateway_id) + ok, message = tahoma_config.check_local_credentials(config) + assert ok is False + for substring in expected_substrings: + assert substring in message + + +# --- build_client ------------------------------------------------------------ +# This is the construction path that broke across the pyoverkiz 2.1.0 +# migration (positional args -> keyword-only credentials=/server=, +# OverkizServer -> ServerConfig). Exercising it here is the main regression +# guard against the *next* pyoverkiz upgrade. + +async def _build_and_inspect(mode, config, **kwargs): + """OverkizClient() opens an aiohttp.ClientSession when none is passed, + which requires a running event loop - hence the asyncio.run() wrapper + around every build_client() call in these tests. + """ + client = tahoma_config.build_client(mode, config, **kwargs) + try: + return type(client), client.server_config.api_type, client.server_config.endpoint + finally: + await client.close() + + +def test_build_client_local_mode(): + config = tahoma_config.Config(token="tok", gateway_id="1234-5678-9012") + client_type, api_type, endpoint = asyncio.run( + _build_and_inspect("local", config, verify_ssl=False) + ) + assert client_type is OverkizClient + assert api_type == APIType.LOCAL + assert "1234-5678-9012" in endpoint + + +def test_build_client_remote_mode(): + config = tahoma_config.Config(username="bob", password="secret", server="somfy_europe") + client_type, api_type, _ = asyncio.run(_build_and_inspect("remote", config)) + assert client_type is OverkizClient + assert api_type == APIType.CLOUD + + +# --- import surface ------------------------------------------------------ + +@pytest.mark.parametrize("module_name", ["tahoma_config", "get_devices_url", "tahoma"]) +def test_first_party_modules_import_cleanly(module_name): + """Would have caught today's pyoverkiz 2.1.0 breakage: renamed + OverkizServer/NotAuthenticatedException, removed Device.id/Gateway.id, + Command becoming keyword-only, execute_command/get_scenarios/ + cancel_command being renamed - all of these surface as import- or + module-level errors, not just runtime ones. + """ + importlib.import_module(module_name) From c5b97ad7e7dcd4d6ca840746cb2d6b6e4ca0a12c Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 11:49:15 +0200 Subject: [PATCH 7/8] Drop Python 3.11 from CI matrix pyoverkiz>=2.0 requires Python >=3.12, so 3.11 can never install the real dependency. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7691428..e0c2a48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.12", "3.13"] steps: - uses: actions/checkout@v4 From db2f5c09c9696f17dfceebff3c2ae89fc7849610 Mon Sep 17 00:00:00 2001 From: Michael Kopp Date: Sun, 2 Aug 2026 12:30:24 +0200 Subject: [PATCH 8/8] fix readme to point to relative path of the file instead of absolute path --- README.md | 2 +- README_FR.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dfe4b74..26a3d34 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ There is a ChatGPT functionality. See this [documentation](https://github.com/pz #### 1. Download Download the zip file and extract it to a choosen folder: -[Download :inbox_tray:](https://github.com/pzim-devdata/tahoma/releases/latest/download/tahoma.zip) +[Download :inbox_tray:](../../releases/latest/download/tahoma.zip) #### 2. Install dependencies Run the following commands in your imported folder: diff --git a/README_FR.md b/README_FR.md index 071a6e2..a7504df 100644 --- a/README_FR.md +++ b/README_FR.md @@ -125,7 +125,7 @@ Si vous aimez ce programme, veuillez lui attribuer une étoile sur GitHub pour a #### 1. Télécharger Télécharger le fichier zip et l'extraire dans un dossier choisi : -[Download :inbox_tray:](https://github.com/pzim-devdata/tahoma/releases/latest/download/tahoma.zip) +[Download :inbox_tray:](../../releases/latest/download/tahoma.zip) #### 2. Installer les dépendances Exécutez les commandes suivantes dans votre dossier importé :