From ffc60062f6df816a206d0dee17af9d96f9299048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Zi=C3=B3=C5=82kowski?= Date: Sun, 15 Sep 2024 20:32:15 +0200 Subject: [PATCH] Refactor (#278) --- tdmgr/GUI/console.py | 5 +- tdmgr/GUI/devices.py | 4 +- tdmgr/GUI/dialogs/buttons.py | 4 +- tdmgr/GUI/dialogs/main.py | 20 ++-- tdmgr/GUI/dialogs/patterns.py | 2 +- tdmgr/GUI/dialogs/power.py | 4 +- tdmgr/GUI/dialogs/setoptions.py | 2 +- tdmgr/GUI/dialogs/switches.py | 4 +- tdmgr/GUI/dialogs/templates.py | 11 ++- tdmgr/GUI/dialogs/timers.py | 3 +- tdmgr/GUI/rules.py | 3 +- tdmgr/GUI/widgets.py | 2 +- tdmgr/models/devices.py | 4 +- tdmgr/{util => }/mqtt.py | 23 +++++ tdmgr/tasmota/__init__.py | 0 tdmgr/{util => tasmota}/commands.py | 10 ++ tdmgr/tasmota/common.py | 1 + tdmgr/{util/__init__.py => tasmota/device.py} | 92 +++---------------- tdmgr/{util => tasmota}/discovery.py | 4 +- tdmgr/tasmota/environment.py | 22 +++++ tdmgr/{util => tasmota}/setoptions.py | 0 tests/conftest.py | 3 +- tests/test_device.py | 3 +- tests/test_environment.py | 4 +- tests/test_message.py | 2 +- tests/test_mqtt.py | 2 +- 26 files changed, 126 insertions(+), 108 deletions(-) rename tdmgr/{util => }/mqtt.py (94%) create mode 100644 tdmgr/tasmota/__init__.py rename tdmgr/{util => tasmota}/commands.py (89%) create mode 100644 tdmgr/tasmota/common.py rename tdmgr/{util/__init__.py => tasmota/device.py} (85%) rename tdmgr/{util => tasmota}/discovery.py (86%) create mode 100644 tdmgr/tasmota/environment.py rename tdmgr/{util => tasmota}/setoptions.py (100%) diff --git a/tdmgr/GUI/console.py b/tdmgr/GUI/console.py index 9e0a818..48db278 100644 --- a/tdmgr/GUI/console.py +++ b/tdmgr/GUI/console.py @@ -18,8 +18,9 @@ ) from tdmgr.GUI.widgets import GroupBoxV, HLayout, VLayout, console_font -from tdmgr.util import Message, TasmotaDevice -from tdmgr.util.commands import commands +from tdmgr.mqtt import Message +from tdmgr.tasmota.commands import commands +from tdmgr.tasmota.device import TasmotaDevice class ConsoleWidget(QDockWidget): diff --git a/tdmgr/GUI/devices.py b/tdmgr/GUI/devices.py index 0939547..676d5b0 100644 --- a/tdmgr/GUI/devices.py +++ b/tdmgr/GUI/devices.py @@ -44,7 +44,9 @@ base_view, default_views, ) -from tdmgr.util import TasmotaDevice, initial_commands, resets +from tdmgr.mqtt import initial_commands +from tdmgr.tasmota.commands import resets +from tdmgr.tasmota.device import TasmotaDevice class DevicesListWidget(QWidget): diff --git a/tdmgr/GUI/dialogs/buttons.py b/tdmgr/GUI/dialogs/buttons.py index 671d179..8201e58 100644 --- a/tdmgr/GUI/dialogs/buttons.py +++ b/tdmgr/GUI/dialogs/buttons.py @@ -2,8 +2,8 @@ from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QTabWidget, QWidget from tdmgr.GUI.widgets import Command, HTMLLabel, VLayout, docs_url -from tdmgr.util.commands import commands -from tdmgr.util.setoptions import setoptions +from tdmgr.tasmota.commands import commands +from tdmgr.tasmota.setoptions import setoptions class ButtonsDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/main.py b/tdmgr/GUI/dialogs/main.py index 5f56ec8..0f450a3 100644 --- a/tdmgr/GUI/dialogs/main.py +++ b/tdmgr/GUI/dialogs/main.py @@ -29,9 +29,17 @@ from tdmgr.GUI.telemetry import TelemetryWidget from tdmgr.GUI.widgets import Toolbar from tdmgr.models.devices import TasmotaDevicesModel -from tdmgr.util import MQTT_PATH_REGEX, TasmotaDevice, TasmotaEnvironment, initial_commands -from tdmgr.util.discovery import lwt_discovery_stage2 -from tdmgr.util.mqtt import DEFAULT_PATTERNS, Message, MqttClient, expand_fulltopic +from tdmgr.mqtt import ( + DEFAULT_PATTERNS, + MQTT_PATH_REGEX, + Message, + MqttClient, + expand_fulltopic, + initial_commands, +) +from tdmgr.tasmota.device import TasmotaDevice +from tdmgr.tasmota.discovery import lwt_discovery_stage2 +from tdmgr.tasmota.environment import TasmotaEnvironment log = logging.getLogger(__name__) @@ -366,15 +374,15 @@ def mqtt_message(self, msg: Message): if device := self.env.find_device(msg): if msg.is_lwt: log.debug("MQTT: LWT message for %s: %s", device.p["Topic"], msg.payload) - device.update_property("LWT", msg.payload) + device.online = msg.payload - if msg.payload == device.p["Online"]: + if device.online: # known device came online, query initial state self.initial_query(device, True) else: # forward the message for processing - device.update_property("LWT", device.p["Online"]) + device.online = True device.process_message(msg) # TODO: ditto diff --git a/tdmgr/GUI/dialogs/patterns.py b/tdmgr/GUI/dialogs/patterns.py index 2e994a8..248259b 100644 --- a/tdmgr/GUI/dialogs/patterns.py +++ b/tdmgr/GUI/dialogs/patterns.py @@ -1,7 +1,7 @@ from PyQt5.QtWidgets import QDialog, QInputDialog, QLabel, QListWidget, QMessageBox, QPushButton from tdmgr.GUI.widgets import HLayout, VLayout -from tdmgr.util import DEFAULT_PATTERNS +from tdmgr.mqtt import DEFAULT_PATTERNS class PatternsDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/power.py b/tdmgr/GUI/dialogs/power.py index 401a391..e18ff51 100644 --- a/tdmgr/GUI/dialogs/power.py +++ b/tdmgr/GUI/dialogs/power.py @@ -2,8 +2,8 @@ from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QTabWidget, QWidget from tdmgr.GUI.widgets import Command, HTMLLabel, Interlock, PulseTime, VLayout, docs_url -from tdmgr.util.commands import commands -from tdmgr.util.setoptions import setoptions +from tdmgr.tasmota.commands import commands +from tdmgr.tasmota.setoptions import setoptions class PowerDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/setoptions.py b/tdmgr/GUI/dialogs/setoptions.py index 46e3434..821d682 100644 --- a/tdmgr/GUI/dialogs/setoptions.py +++ b/tdmgr/GUI/dialogs/setoptions.py @@ -2,7 +2,7 @@ from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QLabel from tdmgr.GUI.widgets import DictComboBox, GroupBoxV, VLayout -from tdmgr.util.setoptions import setoptions +from tdmgr.tasmota.setoptions import setoptions class SetOptionsDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/switches.py b/tdmgr/GUI/dialogs/switches.py index 3bd1f00..ba7be55 100644 --- a/tdmgr/GUI/dialogs/switches.py +++ b/tdmgr/GUI/dialogs/switches.py @@ -2,8 +2,8 @@ from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QTabWidget, QWidget from tdmgr.GUI.widgets import Command, CommandMultiSelect, HTMLLabel, VLayout, docs_url -from tdmgr.util.commands import commands -from tdmgr.util.setoptions import setoptions +from tdmgr.tasmota.commands import commands +from tdmgr.tasmota.setoptions import setoptions class SwitchesDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/templates.py b/tdmgr/GUI/dialogs/templates.py index 4a0eaad..76e47a7 100644 --- a/tdmgr/GUI/dialogs/templates.py +++ b/tdmgr/GUI/dialogs/templates.py @@ -12,7 +12,16 @@ ) from tdmgr.GUI.widgets import DictComboBox, VLayout -from tdmgr.util import template_adc + +template_adc = { + "0": "None", + "15": "User", + "1": "Analog", + "2": "Temperature", + "3": "Light", + "4": "Button", + "5": "Buttoni", +} class TemplateDialog(QDialog): diff --git a/tdmgr/GUI/dialogs/timers.py b/tdmgr/GUI/dialogs/timers.py index 64f1404..f937119 100644 --- a/tdmgr/GUI/dialogs/timers.py +++ b/tdmgr/GUI/dialogs/timers.py @@ -14,7 +14,8 @@ ) from tdmgr.GUI.widgets import GroupBoxH, GroupBoxV, HLayout, VLayout -from tdmgr.util import Message, TasmotaDevice +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice class TimersDialog(QDialog): diff --git a/tdmgr/GUI/rules.py b/tdmgr/GUI/rules.py index a34c8a5..760633e 100644 --- a/tdmgr/GUI/rules.py +++ b/tdmgr/GUI/rules.py @@ -15,7 +15,8 @@ ) from tdmgr.GUI.widgets import CheckableAction, GroupBoxH, GroupBoxV, HLayout, Toolbar, VLayout -from tdmgr.util import Message, TasmotaDevice +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice RE_RULE = re.compile(r"^RULE\d", re.IGNORECASE) diff --git a/tdmgr/GUI/widgets.py b/tdmgr/GUI/widgets.py index 46a1c5d..6dfbdd5 100644 --- a/tdmgr/GUI/widgets.py +++ b/tdmgr/GUI/widgets.py @@ -24,7 +24,7 @@ ) from tdmgr.schemas.result import PulseTimeLegacyResultSchema -from tdmgr.util import TasmotaDevice +from tdmgr.tasmota.device import TasmotaDevice base_view = ["Device"] default_views = { diff --git a/tdmgr/models/devices.py b/tdmgr/models/devices.py index 7bf5c5f..8c6deea 100644 --- a/tdmgr/models/devices.py +++ b/tdmgr/models/devices.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt from tdmgr.models.roles import DeviceRoles -from tdmgr.util import TasmotaDevice +from tdmgr.tasmota.device import TasmotaDevice class TasmotaDevicesModel(QAbstractTableModel): @@ -128,7 +128,7 @@ def data(self, idx, role=Qt.DisplayRole): return val if role == DeviceRoles.LWTRole: - return d.is_online + return d.online if role == DeviceRoles.RestartReasonRole: return d.p.get("RestartReason") diff --git a/tdmgr/util/mqtt.py b/tdmgr/mqtt.py similarity index 94% rename from tdmgr/util/mqtt.py rename to tdmgr/mqtt.py index 115a594..31cfbfa 100644 --- a/tdmgr/util/mqtt.py +++ b/tdmgr/mqtt.py @@ -24,6 +24,29 @@ ] +def initial_commands(): + commands = [ + "template", + "modules", + "gpio", + "buttondebounce", + "switchdebounce", + "interlock", + "blinktime", + "blinkcount", + "pulsetime", + ] + + commands = [(command, "") for command in commands] + commands += [("status", "0"), ("gpios", "255")] + + for sht in range(8): + commands.append([f"shutterrelay{sht + 1}", ""]) + commands.append([f"shutterposition{sht + 1}", ""]) + + return commands + + def expand_fulltopic(fulltopic): if fulltopic[-1] != '/': fulltopic = f"{fulltopic}/" diff --git a/tdmgr/tasmota/__init__.py b/tdmgr/tasmota/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tdmgr/util/commands.py b/tdmgr/tasmota/commands.py similarity index 89% rename from tdmgr/util/commands.py rename to tdmgr/tasmota/commands.py index 9e862bf..a83998c 100644 --- a/tdmgr/util/commands.py +++ b/tdmgr/tasmota/commands.py @@ -1,3 +1,13 @@ +resets = [ + "1: reset device settings to firmware defaults", + "2: erase flash, reset device settings to firmware defaults", + "3: erase flash SDK parameters", + "4: reset device settings to firmware defaults, keep Wi-Fi credentials", + "5: erase flash, reset parameters to firmware defaults, keep Wi-Fi settings", + "6: erase flash, reset parameters to firmware defaults, keep Wi-Fi and MQTT settings", + "99: reset device bootcount to zero", +] + commands = { "BlinkCount": { "description": "Number of relay toggles (blinks)", diff --git a/tdmgr/tasmota/common.py b/tdmgr/tasmota/common.py new file mode 100644 index 0000000..b9dbbce --- /dev/null +++ b/tdmgr/tasmota/common.py @@ -0,0 +1 @@ +COMMAND_UNKNOWN = {"Command": "Unknown"} diff --git a/tdmgr/util/__init__.py b/tdmgr/tasmota/device.py similarity index 85% rename from tdmgr/util/__init__.py rename to tdmgr/tasmota/device.py index 1a207f4..2990eac 100644 --- a/tdmgr/util/__init__.py +++ b/tdmgr/tasmota/device.py @@ -1,7 +1,6 @@ import json import logging import re -from enum import Enum from functools import lru_cache from typing import Callable, Optional, Union @@ -9,6 +8,7 @@ from pydantic import BaseModel, ValidationError from PyQt5.QtCore import QObject, pyqtSignal +from tdmgr.mqtt import DEFAULT_PATTERNS, MQTT_PATH_REGEX, Message from tdmgr.schemas.discovery import DiscoverySchema, TopicPrefixes from tdmgr.schemas.result import ( PulseTimeLegacyResultSchema, @@ -16,82 +16,9 @@ TemplateResultSchema, ) from tdmgr.schemas.status import STATUS_SCHEMA_MAP -from tdmgr.util.mqtt import DEFAULT_PATTERNS, MQTT_PATH_REGEX, Message +from tdmgr.tasmota.common import COMMAND_UNKNOWN -# TODO: extract from __init__ - -# TODO: extract to common -resets = [ - "1: reset device settings to firmware defaults", - "2: erase flash, reset device settings to firmware defaults", - "3: erase flash SDK parameters", - "4: reset device settings to firmware defaults, keep Wi-Fi credentials", - "5: erase flash, reset parameters to firmware defaults, keep Wi-Fi settings", - "6: erase flash, reset parameters to firmware defaults, keep Wi-Fi and MQTT settings", - "99: reset device bootcount to zero", -] - -template_adc = { - "0": "None", - "15": "User", - "1": "Analog", - "2": "Temperature", - "3": "Light", - "4": "Button", - "5": "Buttoni", -} - - -COMMAND_UNKNOWN = {"Command": "Unknown"} - - -# TODO: extract to mqtt -def initial_commands(): - commands = [ - "template", - "modules", - "gpio", - "buttondebounce", - "switchdebounce", - "interlock", - "blinktime", - "blinkcount", - "pulsetime", - ] - - commands = [(command, "") for command in commands] - commands += [("status", "0"), ("gpios", "255")] - - for sht in range(8): - commands.append([f"shutterrelay{sht + 1}", ""]) - commands.append([f"shutterposition{sht + 1}", ""]) - - return commands - - -def parse_payload(payload): - match = re.match(r"(\d+) \((.*)\)", payload) - if match: - return dict([match.groups()]) - return {} - - -class DiscoveryMode(int, Enum): - BOTH = 0 - NATIVE = 1 - LEGACY = 2 - - -class TasmotaEnvironment: - def __init__(self): - self.devices: list[TasmotaDevice] = [] - self.lwts = dict() - self.retained = set() - - def find_device(self, msg: Message) -> 'TasmotaDevice': - for d in self.devices: - if d.message_topic_matches_fulltopic(msg): - return d +log = logging.getLogger(__name__) class TasmotaDevice(QObject): @@ -294,7 +221,7 @@ def process_status(self, schema: BaseModel, payload: dict): self.update_property(k, v) except ValidationError as e: - logging.critical("MQTT: Cannot parse %s", e) + log.critical("MQTT: Cannot parse %s", e) def process_sensor(self, payload: str): sensor_data = json.loads(payload) @@ -305,7 +232,7 @@ def process_sensor(self, payload: str): def process_message(self, msg: Message): if self.debug: - logging.debug("MQTT: %s %s", msg.topic, msg.payload) + log.debug("MQTT: %s %s", msg.topic, msg.payload) if msg.prefix in (self.topic_prefixes.stat, self.topic_prefixes.tele): # /STATE or /STATUS response @@ -408,9 +335,16 @@ def name(self): return self.p["Topic"] @property - def is_online(self): + def online(self): return self.p.get("LWT", self.p["Offline"]) == self.p["Online"] + @online.setter + def online(self, val: Union[bool, dict]): + if isinstance(val, bool): + self.update_property("LWT", self.p["Online"]) + else: + self.update_property("LWT", val) + @property def url(self) -> Optional[str]: if self.ip_address != "0.0.0.0": diff --git a/tdmgr/util/discovery.py b/tdmgr/tasmota/discovery.py similarity index 86% rename from tdmgr/util/discovery.py rename to tdmgr/tasmota/discovery.py index d6a9cba..bc7fc14 100644 --- a/tdmgr/util/discovery.py +++ b/tdmgr/tasmota/discovery.py @@ -1,6 +1,8 @@ import logging -from tdmgr.util import Message, TasmotaDevice, TasmotaEnvironment +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice +from tdmgr.tasmota.environment import TasmotaEnvironment def native_discovery(): diff --git a/tdmgr/tasmota/environment.py b/tdmgr/tasmota/environment.py new file mode 100644 index 0000000..98e47a5 --- /dev/null +++ b/tdmgr/tasmota/environment.py @@ -0,0 +1,22 @@ +from enum import Enum + +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice + + +class DiscoveryMode(int, Enum): + BOTH = 0 + NATIVE = 1 + LEGACY = 2 + + +class TasmotaEnvironment: + def __init__(self): + self.devices: list[TasmotaDevice] = [] + self.lwts = dict() + self.retained = set() + + def find_device(self, msg: Message) -> TasmotaDevice: + for d in self.devices: + if d.message_topic_matches_fulltopic(msg): + return d diff --git a/tdmgr/util/setoptions.py b/tdmgr/tasmota/setoptions.py similarity index 100% rename from tdmgr/util/setoptions.py rename to tdmgr/tasmota/setoptions.py diff --git a/tests/conftest.py b/tests/conftest.py index 3ba0720..143668c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,8 @@ import pytest -from tdmgr.util import Message, TasmotaDevice +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice def get_payload(version: str, filename: str) -> bytes: diff --git a/tests/test_device.py b/tests/test_device.py index 0f4bd1f..18d24a4 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,6 +1,7 @@ import pytest -from tdmgr.util import Message, TasmotaDevice +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice from tests.conftest import get_payload diff --git a/tests/test_environment.py b/tests/test_environment.py index a50416a..42b7127 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,6 +1,8 @@ import pytest -from tdmgr.util import Message, TasmotaDevice, TasmotaEnvironment +from tdmgr.mqtt import Message +from tdmgr.tasmota.device import TasmotaDevice +from tdmgr.tasmota.environment import TasmotaEnvironment @pytest.mark.parametrize( diff --git a/tests/test_message.py b/tests/test_message.py index 4e6876d..d947079 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,6 +1,6 @@ import pytest -from tdmgr.util import Message +from tdmgr.mqtt import Message def test_message(): diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py index 8bbf5bc..9633f73 100644 --- a/tests/test_mqtt.py +++ b/tests/test_mqtt.py @@ -1,6 +1,6 @@ import pytest -from tdmgr.util.mqtt import expand_fulltopic +from tdmgr.mqtt import expand_fulltopic @pytest.mark.parametrize(