summaryrefslogtreecommitdiff
path: root/src/home
diff options
context:
space:
mode:
Diffstat (limited to 'src/home')
-rw-r--r--src/home/media/__init__.py1
-rw-r--r--src/home/mqtt/__init__.py13
-rw-r--r--src/home/mqtt/_module.py34
-rw-r--r--src/home/mqtt/_node.py95
-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.py106
-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.py67
-rw-r--r--src/home/mqtt/module/relay.py82
-rw-r--r--src/home/mqtt/module/temphum.py57
-rw-r--r--src/home/mqtt/mqtt.py24
-rw-r--r--src/home/mqtt/payload/__init__.py1
-rw-r--r--src/home/mqtt/payload/relay.py22
-rw-r--r--src/home/mqtt/payload/sensors.py20
-rw-r--r--src/home/mqtt/payload/temphum.py15
-rw-r--r--src/home/mqtt/relay.py86
-rw-r--r--src/home/mqtt/temphum.py54
-rw-r--r--src/home/mqtt/util.py30
-rw-r--r--src/home/pio/products.py4
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