diff options
Diffstat (limited to 'src/home')
-rw-r--r-- | src/home/media/__init__.py | 1 | ||||
-rw-r--r-- | src/home/mqtt/__init__.py | 13 | ||||
-rw-r--r-- | src/home/mqtt/_module.py | 34 | ||||
-rw-r--r-- | src/home/mqtt/_node.py | 95 | ||||
-rw-r--r-- | src/home/mqtt/_payload.py (renamed from src/home/mqtt/payload/base_payload.py) | 4 | ||||
-rw-r--r-- | src/home/mqtt/esp.py | 106 | ||||
-rw-r--r-- | src/home/mqtt/module/diagnostics.py (renamed from src/home/mqtt/payload/esp.py) | 53 | ||||
-rw-r--r-- | src/home/mqtt/module/inverter.py (renamed from src/home/mqtt/payload/inverter.py) | 8 | ||||
-rw-r--r-- | src/home/mqtt/module/ota.py | 67 | ||||
-rw-r--r-- | src/home/mqtt/module/relay.py | 82 | ||||
-rw-r--r-- | src/home/mqtt/module/temphum.py | 57 | ||||
-rw-r--r-- | src/home/mqtt/mqtt.py | 24 | ||||
-rw-r--r-- | src/home/mqtt/payload/__init__.py | 1 | ||||
-rw-r--r-- | src/home/mqtt/payload/relay.py | 22 | ||||
-rw-r--r-- | src/home/mqtt/payload/sensors.py | 20 | ||||
-rw-r--r-- | src/home/mqtt/payload/temphum.py | 15 | ||||
-rw-r--r-- | src/home/mqtt/relay.py | 86 | ||||
-rw-r--r-- | src/home/mqtt/temphum.py | 54 | ||||
-rw-r--r-- | src/home/mqtt/util.py | 30 | ||||
-rw-r--r-- | src/home/pio/products.py | 4 |
20 files changed, 459 insertions, 317 deletions
diff --git a/src/home/media/__init__.py b/src/home/media/__init__.py index 976c990..6923105 100644 --- a/src/home/media/__init__.py +++ b/src/home/media/__init__.py @@ -12,6 +12,7 @@ __map__ = { __all__ = list(itertools.chain(*__map__.values())) + def __getattr__(name): if name in __all__: for file, names in __map__.items(): diff --git a/src/home/mqtt/__init__.py b/src/home/mqtt/__init__.py index 982e2b6..c95061f 100644 --- a/src/home/mqtt/__init__.py +++ b/src/home/mqtt/__init__.py @@ -1,4 +1,9 @@ -from .mqtt import MqttBase -from .util import poll_tick -from .relay import MqttRelay, MqttRelayState -from .temphum import MqttTempHum
\ No newline at end of file +from .mqtt import MqttBase, MqttPayload, MqttPayloadCustomField +from ._node import MqttNode +from ._module import MqttModule +from .util import ( + poll_tick, + get_modules as get_mqtt_modules, + import_module as import_mqtt_module, + add_module as add_mqtt_module +)
\ No newline at end of file diff --git a/src/home/mqtt/_module.py b/src/home/mqtt/_module.py new file mode 100644 index 0000000..ef50e70 --- /dev/null +++ b/src/home/mqtt/_module.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import abc +import logging + +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from ._node import MqttNode + from ._payload import MqttPayload + + +class MqttModule(abc.ABC): + tick_interval: int + _initialized: bool + + def __init__(self, tick_interval=0): + self.tick_interval = tick_interval + self._initialized = False + self._logger = logging.getLogger(self.__class__.__name__) + + def init(self, mqtt: MqttNode): + pass + + def is_initialized(self): + return self._initialized + + def set_initialized(self): + self._initialized = True + + def tick(self, mqtt: MqttNode): + pass + + def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: + pass diff --git a/src/home/mqtt/_node.py b/src/home/mqtt/_node.py new file mode 100644 index 0000000..688b30b --- /dev/null +++ b/src/home/mqtt/_node.py @@ -0,0 +1,95 @@ +import paho.mqtt.client as mqtt + +from .mqtt import MqttBase +from typing import List +from ._module import MqttModule +from ._payload import MqttPayload + + +class MqttNode(MqttBase): + _modules: List[MqttModule] + _module_subscriptions: dict[str, MqttModule] + _node_id: str + _payload_callbacks: list[callable] + # _devices: list[MqttEspDevice] + # _message_callback: Optional[callable] + # _ota_publish_callback: Optional[callable] + + def __init__(self, + node_id: str, + # devices: Union[MqttEspDevice, list[MqttEspDevice]] + ): + super().__init__(clean_session=True) + self._modules = [] + self._module_subscriptions = {} + self._node_id = node_id + self._payload_callbacks = [] + # if not isinstance(devices, list): + # devices = [devices] + # self._devices = devices + # self._message_callback = None + # self._ota_publish_callback = None + # self._ota_mid = None + + def on_connect(self, client: mqtt.Client, userdata, flags, rc): + super().on_connect(client, userdata, flags, rc) + for module in self._modules: + if not module.is_initialized(): + module.init(self) + module.set_initialized() + + def on_publish(self, client: mqtt.Client, userdata, mid): + pass # FIXME + # if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback: + # self._ota_publish_callback() + + def on_message(self, client: mqtt.Client, userdata, msg): + try: + topic = msg.topic + actual_topic = topic[len(f'hk/{self._node_id}/'):] + + if actual_topic in self._module_subscriptions: + payload = self._module_subscriptions[actual_topic].handle_payload(self, actual_topic, msg.payload) + if isinstance(payload, MqttPayload): + for f in self._payload_callbacks: + f(payload) + + except Exception as e: + self._logger.exception(str(e)) + + # def push_ota(self, + # device_id, + # filename: str, + # publish_callback: callable, + # qos: int): + # device = next(d for d in self._devices if d.id == device_id) + # assert device.secret is not None, 'device secret not specified' + # + # self._ota_publish_callback = publish_callback + # payload = OtaPayload(secret=device.secret, filename=filename) + # publish_result = self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/admin/ota', + # payload=payload.pack(), + # qos=qos) + # self._ota_mid = publish_result.mid + # self._client.loop_write() + # + # @classmethod + # def get_mqtt_topics(cls, additional_topics: Optional[list[str]] = None): + # return rf'^hk/(.*?)/{cls.TOPIC_LEAF}/(stat|stat1|otares'+('|'+('|'.join(additional_topics)) if additional_topics else '')+')$' + + def add_module(self, module: MqttModule): + self._modules.append(module) + if self._connected: + module.init(self) + module.set_initialized() + + def subscribe_module(self, topic: str, module: MqttModule, qos: int = 1): + self._module_subscriptions[topic] = module + self._client.subscribe(f'hk/{self._node_id}/{topic}', qos) + + def publish(self, topic: str, payload: bytes, qos: int = 1): + self._client.publish(f'hk/{self._node_id}/{topic}', payload, qos) + self._client.loop_write() + + def add_payload_callback(self, callback: callable): + self._payload_callbacks.append(callback)
\ No newline at end of file diff --git a/src/home/mqtt/payload/base_payload.py b/src/home/mqtt/_payload.py index 1abd898..58eeae3 100644 --- a/src/home/mqtt/payload/base_payload.py +++ b/src/home/mqtt/_payload.py @@ -1,5 +1,5 @@ -import abc import struct +import abc import re from typing import Optional, Tuple @@ -142,4 +142,4 @@ def _bit_field_params(cl) -> Optional[Tuple[int, ...]]: match = re.match(r'MQTTPayloadBitField_(\d+)_(\d+)_(\d)$', cl.__name__) if match is not None: return tuple([int(match.group(i)) for i in range(1, 4)]) - return None + return None
\ No newline at end of file diff --git a/src/home/mqtt/esp.py b/src/home/mqtt/esp.py deleted file mode 100644 index 56ced83..0000000 --- a/src/home/mqtt/esp.py +++ /dev/null @@ -1,106 +0,0 @@ -import re -import paho.mqtt.client as mqtt - -from .mqtt import MqttBase -from typing import Optional, Union -from .payload.esp import ( - OTAPayload, - OTAResultPayload, - DiagnosticsPayload, - InitialDiagnosticsPayload -) - - -class MqttEspDevice: - id: str - secret: Optional[str] - - def __init__(self, id: str, secret: Optional[str] = None): - self.id = id - self.secret = secret - - -class MqttEspBase(MqttBase): - _devices: list[MqttEspDevice] - _message_callback: Optional[callable] - _ota_publish_callback: Optional[callable] - - TOPIC_LEAF = 'esp' - - def __init__(self, - devices: Union[MqttEspDevice, list[MqttEspDevice]], - subscribe_to_updates=True): - super().__init__(clean_session=True) - if not isinstance(devices, list): - devices = [devices] - self._devices = devices - self._message_callback = None - self._ota_publish_callback = None - self._subscribe_to_updates = subscribe_to_updates - self._ota_mid = None - - def on_connect(self, client: mqtt.Client, userdata, flags, rc): - super().on_connect(client, userdata, flags, rc) - - if self._subscribe_to_updates: - for device in self._devices: - topic = f'hk/{device.id}/{self.TOPIC_LEAF}/#' - self._logger.debug(f"subscribing to {topic}") - client.subscribe(topic, qos=1) - - def on_publish(self, client: mqtt.Client, userdata, mid): - if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback: - self._ota_publish_callback() - - def set_message_callback(self, callback: callable): - self._message_callback = callback - - def on_message(self, client: mqtt.Client, userdata, msg): - try: - match = re.match(self.get_mqtt_topics(), msg.topic) - self._logger.debug(f'topic: {msg.topic}') - if not match: - return - - device_id = match.group(1) - subtopic = match.group(2) - - # try: - next(d for d in self._devices if d.id == device_id) - # except StopIteration:h - # return - - message = None - if subtopic == 'stat': - message = DiagnosticsPayload.unpack(msg.payload) - elif subtopic == 'stat1': - message = InitialDiagnosticsPayload.unpack(msg.payload) - elif subtopic == 'otares': - message = OTAResultPayload.unpack(msg.payload) - - if message and self._message_callback: - self._message_callback(device_id, message) - return True - - except Exception as e: - self._logger.exception(str(e)) - - def push_ota(self, - device_id, - filename: str, - publish_callback: callable, - qos: int): - device = next(d for d in self._devices if d.id == device_id) - assert device.secret is not None, 'device secret not specified' - - self._ota_publish_callback = publish_callback - payload = OTAPayload(secret=device.secret, filename=filename) - publish_result = self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/admin/ota', - payload=payload.pack(), - qos=qos) - self._ota_mid = publish_result.mid - self._client.loop_write() - - @classmethod - def get_mqtt_topics(cls, additional_topics: Optional[list[str]] = None): - return rf'^hk/(.*?)/{cls.TOPIC_LEAF}/(stat|stat1|otares'+('|'+('|'.join(additional_topics)) if additional_topics else '')+')$'
\ No newline at end of file diff --git a/src/home/mqtt/payload/esp.py b/src/home/mqtt/module/diagnostics.py index 171cdb9..c31cce2 100644 --- a/src/home/mqtt/payload/esp.py +++ b/src/home/mqtt/module/diagnostics.py @@ -1,39 +1,8 @@ -import hashlib +from ..mqtt import MqttPayload, MqttPayloadCustomField +from .._node import MqttNode, MqttModule +from typing import Optional -from .base_payload import MqttPayload, MqttPayloadCustomField - - -class OTAResultPayload(MqttPayload): - FORMAT = '=BB' - result: int - error_code: int - - -class OTAPayload(MqttPayload): - secret: str - filename: str - - # structure of returned data: - # - # uint8_t[len(secret)] secret; - # uint8_t[16] md5; - # *uint8_t data - - def pack(self): - buf = bytearray(self.secret.encode()) - m = hashlib.md5() - with open(self.filename, 'rb') as fd: - content = fd.read() - m.update(content) - buf.extend(m.digest()) - buf.extend(content) - return buf - - def unpack(cls, buf: bytes): - raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented') - # secret = buf[:12].decode() - # filename = buf[12:].decode() - # return OTAPayload(secret=secret, filename=filename) +MODULE_NAME = 'MqttDiagnosticsModule' class DiagnosticsFlags(MqttPayloadCustomField): @@ -76,3 +45,17 @@ class DiagnosticsPayload(MqttPayload): rssi: int free_heap: int flags: DiagnosticsFlags + + +class MqttDiagnosticsModule(MqttModule): + def init(self, mqtt: MqttNode): + for topic in ('diag', 'd1ag', 'stat', 'stat1'): + mqtt.subscribe_module(topic, self) + + def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: + if topic in ('stat', 'diag'): + message = DiagnosticsPayload.unpack(payload) + elif topic in ('stat1', 'd1ag'): + message = InitialDiagnosticsPayload.unpack(payload) + self._logger.debug(message) + return message diff --git a/src/home/mqtt/payload/inverter.py b/src/home/mqtt/module/inverter.py index 09388df..9cf2978 100644 --- a/src/home/mqtt/payload/inverter.py +++ b/src/home/mqtt/module/inverter.py @@ -1,7 +1,7 @@ import struct -from .base_payload import MqttPayload, bit_field -from typing import Tuple +from .._node import MqttNode +from .._payload import MqttPayload, bit_field _mult_10 = lambda n: int(n*10) _div_10 = lambda n: n/10 @@ -71,3 +71,7 @@ class Generation(MqttPayload): time: int wh: int + + +class MqttInverterModule(MqttNode): + pass diff --git a/src/home/mqtt/module/ota.py b/src/home/mqtt/module/ota.py new file mode 100644 index 0000000..86d6839 --- /dev/null +++ b/src/home/mqtt/module/ota.py @@ -0,0 +1,67 @@ +import hashlib + +from typing import Optional +from ..mqtt import MqttPayload +from .._node import MqttModule, MqttNode + +MODULE_NAME = 'MqttOtaModule' + + +class OtaResultPayload(MqttPayload): + FORMAT = '=BB' + result: int + error_code: int + + +class OtaPayload(MqttPayload): + secret: str + filename: str + + # structure of returned data: + # + # uint8_t[len(secret)] secret; + # uint8_t[16] md5; + # *uint8_t data + + def pack(self): + buf = bytearray(self.secret.encode()) + m = hashlib.md5() + with open(self.filename, 'rb') as fd: + content = fd.read() + m.update(content) + buf.extend(m.digest()) + buf.extend(content) + return buf + + def unpack(cls, buf: bytes): + raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented') + # secret = buf[:12].decode() + # filename = buf[12:].decode() + # return OTAPayload(secret=secret, filename=filename) + + +class MqttOtaModule(MqttModule): + def init(self, mqtt: MqttNode): + mqtt.subscribe_module("otares", self) + + def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: + if topic == 'otares': + message = OtaResultPayload.unpack(payload) + self._logger.debug(message) + return message + + # def push_ota(self, + # node_id, + # filename: str, + # publish_callback: callable, + # qos: int): + # device = next(d for d in self._devices if d.id == device_id) + # assert device.secret is not None, 'device secret not specified' + # + # self._ota_publish_callback = publish_callback + # payload = OtaPayload(secret=device.secret, filename=filename) + # publish_result = self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/admin/ota', + # payload=payload.pack(), + # qos=qos) + # self._ota_mid = publish_result.mid + # self._client.loop_write()
\ No newline at end of file diff --git a/src/home/mqtt/module/relay.py b/src/home/mqtt/module/relay.py new file mode 100644 index 0000000..bf22bfe --- /dev/null +++ b/src/home/mqtt/module/relay.py @@ -0,0 +1,82 @@ +import datetime + +from typing import Optional +from .. import MqttModule, MqttPayload, MqttNode + +MODULE_NAME = 'MqttRelayModule' + + +class MqttPowerSwitchPayload(MqttPayload): + FORMAT = '=12sB' + PACKER = { + 'state': lambda n: int(n), + 'secret': lambda s: s.encode('utf-8') + } + UNPACKER = { + 'state': lambda n: bool(n), + 'secret': lambda s: s.decode('utf-8') + } + + secret: str + state: bool + + +class MqttPowerStatusPayload(MqttPayload): + FORMAT = '=B' + PACKER = { + 'opened': lambda n: int(n), + } + UNPACKER = { + 'opened': lambda n: bool(n), + } + + opened: bool + + +class MqttRelayState: + enabled: bool + update_time: datetime.datetime + rssi: int + fw_version: int + ever_updated: bool + + def __init__(self): + self.ever_updated = False + self.enabled = False + self.rssi = 0 + + def update(self, + enabled: bool, + rssi: int, + fw_version=None): + self.ever_updated = True + self.enabled = enabled + self.rssi = rssi + self.update_time = datetime.datetime.now() + if fw_version: + self.fw_version = fw_version + + +class MqttRelayModule(MqttModule): + def init(self, mqtt: MqttNode): + mqtt.subscribe_module('relay/switch', self) + mqtt.subscribe_module('relay/status', self) + + @staticmethod + def switchpower(mqtt: MqttNode, + enable: bool, + secret: str): + payload = MqttPowerSwitchPayload(secret=secret, state=enable) + mqtt.publish('relay/switch', payload=payload.pack()) + + def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: + message = None + + if topic == 'relay/switch': + message = MqttPowerSwitchPayload.unpack(payload) + elif topic == 'relay/status': + message = MqttPowerStatusPayload.unpack(payload) + + if message is not None: + self._logger.debug(message) + return message diff --git a/src/home/mqtt/module/temphum.py b/src/home/mqtt/module/temphum.py new file mode 100644 index 0000000..0e43f1b --- /dev/null +++ b/src/home/mqtt/module/temphum.py @@ -0,0 +1,57 @@ +from enum import auto +from .._node import MqttNode +from .._module import MqttModule +from .._payload import MqttPayload +from ...util import HashableEnum +from typing import Optional + +two_digits_precision = lambda x: round(x, 2) + +MODULE_NAME = 'MqttTempHumModule' + + +class MqttTemphumDataPayload(MqttPayload): + FORMAT = '=ddb' + UNPACKER = { + 'temp': two_digits_precision, + 'rh': two_digits_precision + } + + temp: float + rh: float + error: int + + +class MqttTempHumNodes(HashableEnum): + KBN_SH_HALL = auto() + KBN_SH_BATHROOM = auto() + KBN_SH_LIVINGROOM = auto() + KBN_SH_BEDROOM = auto() + + KBN_BH_2FL = auto() + KBN_BH_2FL_STREET = auto() + KBN_BH_1FL_LIVINGROOM = auto() + KBN_BH_1FL_BEDROOM = auto() + KBN_BH_1FL_BATHROOM = auto() + + KBN_NH_1FL_INV = auto() + KBN_NH_1FL_CENTER = auto() + KBN_NH_1LF_KT = auto() + KBN_NH_1FL_DS = auto() + KBN_NH_1FS_EZ = auto() + + SPB_FLAT120_CABINET = auto() + + +class MqttTempHumModule(MqttModule): + def init(self, mqtt: MqttNode): + mqtt.subscribe_module('temphum/data', self) + + def handle_payload(self, + mqtt: MqttNode, + topic: str, + payload: bytes) -> Optional[MqttPayload]: + if topic == 'temphum/data': + message = MqttTemphumDataPayload.unpack(payload) + self._logger.debug(message) + return message
\ No newline at end of file diff --git a/src/home/mqtt/mqtt.py b/src/home/mqtt/mqtt.py index 4acd4f6..fad5d26 100644 --- a/src/home/mqtt/mqtt.py +++ b/src/home/mqtt/mqtt.py @@ -3,8 +3,8 @@ import paho.mqtt.client as mqtt import ssl import logging -from typing import Tuple from ..config import config +from ._payload import * def username_and_password() -> Tuple[str, str]: @@ -14,6 +14,8 @@ def username_and_password() -> Tuple[str, str]: class MqttBase: + _connected: bool + def __init__(self, clean_session=True): self._client = mqtt.Client(client_id=config['mqtt']['client_id'], protocol=mqtt.MQTTv311, @@ -24,6 +26,7 @@ class MqttBase: self._client.on_log = self.on_log self._client.on_publish = self.on_publish self._loop_started = False + self._connected = False self._logger = logging.getLogger(self.__class__.__name__) @@ -41,7 +44,9 @@ class MqttBase: 'assets', 'mqtt_ca.crt' )) - self._client.tls_set(ca_certs=ca_certs, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2) + self._client.tls_set(ca_certs=ca_certs, + cert_reqs=ssl.CERT_REQUIRED, + tls_version=ssl.PROTOCOL_TLSv1_2) def connect_and_loop(self, loop_forever=True): host = config['mqtt']['host'] @@ -61,9 +66,11 @@ class MqttBase: def on_connect(self, client: mqtt.Client, userdata, flags, rc): self._logger.info("Connected with result code " + str(rc)) + self._connected = True def on_disconnect(self, client: mqtt.Client, userdata, rc): self._logger.info("Disconnected with result code " + str(rc)) + self._connected = False def on_log(self, client: mqtt.Client, userdata, level, buf): level = mqtt.LOGGING_LEVEL[level] if level in mqtt.LOGGING_LEVEL else logging.INFO @@ -73,4 +80,15 @@ class MqttBase: self._logger.debug(msg.topic + ": " + str(msg.payload)) def on_publish(self, client: mqtt.Client, userdata, mid): - self._logger.debug(f'publish done, mid={mid}')
\ No newline at end of file + self._logger.debug(f'publish done, mid={mid}') + + +class MqttEspDevice: + id: str + secret: Optional[str] + + def __init__(self, + node_id: str, + secret: Optional[str] = None): + self.id = node_id + self.secret = secret diff --git a/src/home/mqtt/payload/__init__.py b/src/home/mqtt/payload/__init__.py deleted file mode 100644 index eee6709..0000000 --- a/src/home/mqtt/payload/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .base_payload import MqttPayload
\ No newline at end of file diff --git a/src/home/mqtt/payload/relay.py b/src/home/mqtt/payload/relay.py deleted file mode 100644 index 4902991..0000000 --- a/src/home/mqtt/payload/relay.py +++ /dev/null @@ -1,22 +0,0 @@ -from .base_payload import MqttPayload -from .esp import ( - OTAResultPayload, - OTAPayload, - InitialDiagnosticsPayload, - DiagnosticsPayload -) - - -class PowerPayload(MqttPayload): - FORMAT = '=12sB' - PACKER = { - 'state': lambda n: int(n), - 'secret': lambda s: s.encode('utf-8') - } - UNPACKER = { - 'state': lambda n: bool(n), - 'secret': lambda s: s.decode('utf-8') - } - - secret: str - state: bool diff --git a/src/home/mqtt/payload/sensors.py b/src/home/mqtt/payload/sensors.py deleted file mode 100644 index f99b307..0000000 --- a/src/home/mqtt/payload/sensors.py +++ /dev/null @@ -1,20 +0,0 @@ -from .base_payload import MqttPayload - -_mult_100 = lambda n: int(n*100) -_div_100 = lambda n: n/100 - - -class Temperature(MqttPayload): - FORMAT = 'IhH' - PACKER = { - 'temp': _mult_100, - 'rh': _mult_100, - } - UNPACKER = { - 'temp': _div_100, - 'rh': _div_100, - } - - time: int - temp: float - rh: float diff --git a/src/home/mqtt/payload/temphum.py b/src/home/mqtt/payload/temphum.py deleted file mode 100644 index c0b744e..0000000 --- a/src/home/mqtt/payload/temphum.py +++ /dev/null @@ -1,15 +0,0 @@ -from .base_payload import MqttPayload - -two_digits_precision = lambda x: round(x, 2) - - -class TempHumDataPayload(MqttPayload): - FORMAT = '=ddb' - UNPACKER = { - 'temp': two_digits_precision, - 'rh': two_digits_precision - } - - temp: float - rh: float - error: int diff --git a/src/home/mqtt/relay.py b/src/home/mqtt/relay.py index a90f19c..cf657f7 100644 --- a/src/home/mqtt/relay.py +++ b/src/home/mqtt/relay.py @@ -1,71 +1,59 @@ +#!/usr/bin/env python3 import paho.mqtt.client as mqtt import re -import datetime +import logging -from .payload.relay import ( - PowerPayload, -) -from .esp import MqttEspBase +from .mqtt import MQTTBase -class MqttRelay(MqttEspBase): - TOPIC_LEAF = 'relay' +class MQTTRelayClient(MQTTBase): + _home_id: str - def set_power(self, device_id, enable: bool, secret=None): - device = next(d for d in self._devices if d.id == device_id) - secret = secret if secret else device.secret + def __init__(self, home_id: str): + super().__init__(clean_session=True) + self._home_id = home_id - assert secret is not None, 'device secret not specified' + def on_connect(self, client: mqtt.Client, userdata, flags, rc): + super().on_connect(client, userdata, flags, rc) - payload = PowerPayload(secret=secret, - state=enable) - self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/power', - payload=payload.pack(), - qos=1) - self._client.loop_write() + topic = f'home/{self._home_id}/#' + self._logger.info(f"subscribing to {topic}") - def on_message(self, client: mqtt.Client, userdata, msg): - if super().on_message(client, userdata, msg): - return + client.subscribe(topic, qos=1) + def on_message(self, client: mqtt.Client, userdata, msg): try: - match = re.match(self.get_mqtt_topics(['power']), msg.topic) + match = re.match(r'^home/(.*?)/relay/(stat|power)(?:/(.+))?$', msg.topic) + self._logger.info(f'topic: {msg.topic}') if not match: return - device_id = match.group(1) + name = match.group(1) subtopic = match.group(2) - message = None - if subtopic == 'power': - message = PowerPayload.unpack(msg.payload) + if name != self._home_id: + return - if message and self._message_callback: - self._message_callback(device_id, message) + if subtopic == 'stat': + stat_name, stat_value = match.group(3).split('/') + self._logger.info(f'stat: {stat_name} = {stat_value}') except Exception as e: self._logger.exception(str(e)) -class MqttRelayState: - enabled: bool - update_time: datetime.datetime - rssi: int - fw_version: int - ever_updated: bool - - def __init__(self): - self.ever_updated = False - self.enabled = False - self.rssi = 0 - - def update(self, - enabled: bool, - rssi: int, - fw_version=None): - self.ever_updated = True - self.enabled = enabled - self.rssi = rssi - self.update_time = datetime.datetime.now() - if fw_version: - self.fw_version = fw_version +class MQTTRelayController(MQTTBase): + _home_id: str + + def __init__(self, home_id: str): + super().__init__(clean_session=True) + self._home_id = home_id + + def set_power(self, enable: bool): + self._client.publish(f'home/{self._home_id}/relay/power', + payload=int(enable), + qos=1) + self._client.loop_write() + + def send_stat(self, stat: dict): + pass diff --git a/src/home/mqtt/temphum.py b/src/home/mqtt/temphum.py deleted file mode 100644 index 44810ef..0000000 --- a/src/home/mqtt/temphum.py +++ /dev/null @@ -1,54 +0,0 @@ -import paho.mqtt.client as mqtt -import re - -from enum import auto -from .payload.temphum import TempHumDataPayload -from .esp import MqttEspBase -from ..util import HashableEnum - - -class MqttTempHumNodes(HashableEnum): - KBN_SH_HALL = auto() - KBN_SH_BATHROOM = auto() - KBN_SH_LIVINGROOM = auto() - KBN_SH_BEDROOM = auto() - - KBN_BH_2FL = auto() - KBN_BH_2FL_STREET = auto() - KBN_BH_1FL_LIVINGROOM = auto() - KBN_BH_1FL_BEDROOM = auto() - KBN_BH_1FL_BATHROOM = auto() - - KBN_NH_1FL_INV = auto() - KBN_NH_1FL_CENTER = auto() - KBN_NH_1LF_KT = auto() - KBN_NH_1FL_DS = auto() - KBN_NH_1FS_EZ = auto() - - SPB_FLAT120_CABINET = auto() - - -class MqttTempHum(MqttEspBase): - TOPIC_LEAF = 'temphum' - - def on_message(self, client: mqtt.Client, userdata, msg): - if super().on_message(client, userdata, msg): - return - - try: - match = re.match(self.get_mqtt_topics(['data']), msg.topic) - if not match: - return - - device_id = match.group(1) - subtopic = match.group(2) - - message = None - if subtopic == 'data': - message = TempHumDataPayload.unpack(msg.payload) - - if message and self._message_callback: - self._message_callback(device_id, message) - - except Exception as e: - self._logger.exception(str(e)) diff --git a/src/home/mqtt/util.py b/src/home/mqtt/util.py index f71ffd8..91b6baf 100644 --- a/src/home/mqtt/util.py +++ b/src/home/mqtt/util.py @@ -1,4 +1,11 @@ import time +import os +import re +import importlib + +from ._node import MqttNode +from . import MqttModule +from typing import List def poll_tick(freq): @@ -6,3 +13,26 @@ def poll_tick(freq): while True: t += freq yield max(t - time.time(), 0) + + +def get_modules() -> List[str]: + modules = [] + for name in os.listdir(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'module')): + name = re.sub(r'\.py$', '', name) + modules.append(name) + return modules + + +def import_module(module: str): + return importlib.import_module( + f'..module.{module}', __name__) + + +def add_module(mqtt_node: MqttNode, module: str) -> MqttModule: + module = import_module(module) + if not hasattr(module, 'MODULE_NAME'): + raise RuntimeError(f'MODULE_NAME not found in module {module}') + cl = getattr(module, getattr(module, 'MODULE_NAME')) + instance = cl() + mqtt_node.add_module(instance) + return instance
\ No newline at end of file diff --git a/src/home/pio/products.py b/src/home/pio/products.py index 7649078..388da03 100644 --- a/src/home/pio/products.py +++ b/src/home/pio/products.py @@ -16,10 +16,6 @@ _products_dir = os.path.join( def get_products(): products = [] for f in os.listdir(_products_dir): - # temp hack - if f.endswith('-esp01'): - continue - # skip the common dir if f in ('common',): continue |