From c44a3669100f72d108404a8fdccd18f55018c18b Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 7 Jun 2023 02:34:50 +0300 Subject: wip --- src/home/mqtt/__init__.py | 10 +--- src/home/mqtt/_module.py | 43 ++++++++++++-- src/home/mqtt/_node.py | 111 ++++++++++++++---------------------- src/home/mqtt/_wrapper.py | 55 ++++++++++++++++++ src/home/mqtt/module/diagnostics.py | 7 ++- src/home/mqtt/module/ota.py | 15 ++--- src/home/mqtt/module/relay.py | 8 +-- src/home/mqtt/module/temphum.py | 13 ++++- src/home/mqtt/mqtt.py | 20 ++----- src/home/mqtt/relay.py | 59 ------------------- src/home/mqtt/util.py | 31 ++-------- src/home/temphum/__init__.py | 19 +----- src/home/temphum/base.py | 20 +++---- src/home/temphum/dht12.py | 22 ------- src/home/temphum/i2c.py | 52 +++++++++++++++++ src/home/temphum/si7021.py | 13 ----- src/home/util.py | 9 ++- src/inverter_mqtt_receiver.py | 4 +- src/inverter_mqtt_sender.py | 4 +- src/mqtt_node_util.py | 34 +++++------ src/polaris_kettle_bot.py | 6 +- src/polaris_kettle_util.py | 4 +- src/pump_bot.py | 25 ++++---- src/sensors_mqtt_receiver.py | 4 +- src/sensors_mqtt_sender.py | 4 +- src/temphum_mqtt_node.py | 5 +- src/temphum_smbus_util.py | 3 +- src/temphumd.py | 5 +- tools/mcuota.py | 98 ------------------------------- tools/mcuota.sh | 14 ----- 30 files changed, 292 insertions(+), 425 deletions(-) create mode 100644 src/home/mqtt/_wrapper.py delete mode 100644 src/home/mqtt/relay.py delete mode 100644 src/home/temphum/dht12.py create mode 100644 src/home/temphum/i2c.py delete mode 100644 src/home/temphum/si7021.py delete mode 100755 tools/mcuota.py delete mode 100755 tools/mcuota.sh diff --git a/src/home/mqtt/__init__.py b/src/home/mqtt/__init__.py index c95061f..8633437 100644 --- a/src/home/mqtt/__init__.py +++ b/src/home/mqtt/__init__.py @@ -1,9 +1,5 @@ -from .mqtt import MqttBase, MqttPayload, MqttPayloadCustomField +from .mqtt import Mqtt, 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 +from ._wrapper import MqttWrapper +from .util import get_modules as get_mqtt_modules \ No newline at end of file diff --git a/src/home/mqtt/_module.py b/src/home/mqtt/_module.py index 840534e..80f27bb 100644 --- a/src/home/mqtt/_module.py +++ b/src/home/mqtt/_module.py @@ -2,6 +2,10 @@ from __future__ import annotations import abc import logging +import threading + +from time import sleep +from ..util import next_tick_gen from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: @@ -10,16 +14,29 @@ if TYPE_CHECKING: class MqttModule(abc.ABC): - tick_interval: int + _tick_interval: int _initialized: bool + _connected: bool + _ticker: Optional[threading.Thread] + _mqtt_node_ref: Optional[MqttNode] def __init__(self, tick_interval=0): - self.tick_interval = tick_interval + self._tick_interval = tick_interval self._initialized = False + self._ticker = None self._logger = logging.getLogger(self.__class__.__name__) + self._connected = False + self._mqtt_node_ref = None - def init(self, mqtt: MqttNode): - pass + def on_connect(self, mqtt: MqttNode): + self._connected = True + self._mqtt_node_ref = mqtt + if self._tick_interval: + self._start_ticker() + + def on_disconnect(self, mqtt: MqttNode): + self._connected = False + self._mqtt_node_ref = None def is_initialized(self): return self._initialized @@ -30,8 +47,24 @@ class MqttModule(abc.ABC): def unset_initialized(self): self._initialized = False - def tick(self, mqtt: MqttNode): + def tick(self): pass + def _tick(self): + g = next_tick_gen(self._tick_interval) + while self._connected: + sleep(next(g)) + if not self._connected: + break + self.tick() + + def _start_ticker(self): + if not self._ticker or not self._ticker.is_alive(): + name_part = f'{self._mqtt_node_ref.id}/' if self._mqtt_node_ref else '' + self._ticker = None + self._ticker = threading.Thread(target=self._tick, + name=f'mqtt:{self.__class__.__name__}/{name_part}ticker') + self._ticker.start() + 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 index f34da0c..ddf5ba2 100644 --- a/src/home/mqtt/_node.py +++ b/src/home/mqtt/_node.py @@ -1,103 +1,80 @@ -import paho.mqtt.client as mqtt +import logging +import importlib -from .mqtt import MqttBase -from typing import List, Optional -from ._module import MqttModule +from typing import List, TYPE_CHECKING, Optional from ._payload import MqttPayload +from ._module import MqttModule +if TYPE_CHECKING: + from ._wrapper import MqttWrapper +else: + MqttWrapper = None -class MqttNode(MqttBase): +class MqttNode: _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] + _wrapper: Optional[MqttWrapper] - def __init__(self, - node_id: str, - # devices: Union[MqttEspDevice, list[MqttEspDevice]] - ): - super().__init__(clean_session=True) + def __init__(self, node_id: str): 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 + self._logger = logging.getLogger(self.__class__.__name__) + self._wrapper = None - def on_connect(self, client: mqtt.Client, userdata, flags, rc): - super().on_connect(client, userdata, flags, rc) + def on_connect(self, wrapper: MqttWrapper): + self._wrapper = wrapper for module in self._modules: if not module.is_initialized(): - module.init(self) + module.on_connect(self) module.set_initialized() - def on_disconnect(self, client: mqtt.Client, userdata, rc): - super().on_disconnect(client, userdata, rc) + def on_disconnect(self): + self._wrapper = None for module in self._modules: module.unset_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) + def on_message(self, topic, payload): + if topic in self._module_subscriptions: + payload = self._module_subscriptions[topic].handle_payload(self, topic, 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 load_module(self, module_name: str, *args, **kwargs) -> MqttModule: + module = importlib.import_module(f'..module.{module_name}', __name__) + 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(*args, **kwargs) + self.add_module(instance) + return instance def add_module(self, module: MqttModule): self._modules.append(module) - if self._connected: - module.init(self) + if self._wrapper and self._wrapper._connected: + module.on_connect(self) module.set_initialized() def subscribe_module(self, topic: str, module: MqttModule, qos: int = 1): + if not self._wrapper or not self._wrapper._connected: + raise RuntimeError('not connected') + self._module_subscriptions[topic] = module - self._client.subscribe(f'hk/{self._node_id}/{topic}', qos) + self._wrapper.subscribe(self.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() + self._wrapper.publish(self.id, topic, payload, qos) def add_payload_callback(self, callback: callable): - self._payload_callbacks.append(callback) \ No newline at end of file + self._payload_callbacks.append(callback) + + @property + def id(self) -> str: + return self._node_id diff --git a/src/home/mqtt/_wrapper.py b/src/home/mqtt/_wrapper.py new file mode 100644 index 0000000..41f9d89 --- /dev/null +++ b/src/home/mqtt/_wrapper.py @@ -0,0 +1,55 @@ +import paho.mqtt.client as mqtt + +from .mqtt import Mqtt +from ._node import MqttNode +from ..config import config +from ..util import strgen + + +class MqttWrapper(Mqtt): + _nodes: list[MqttNode] + + def __init__(self, topic_prefix='hk', randomize_client_id=False): + client_id = config['mqtt']['client_id'] + if randomize_client_id: + client_id += '_'+strgen(6) + super().__init__(clean_session=True, client_id=client_id) + self._nodes = [] + self._topic_prefix = topic_prefix + + def on_connect(self, client: mqtt.Client, userdata, flags, rc): + super().on_connect(client, userdata, flags, rc) + for node in self._nodes: + node.on_connect(self) + + def on_disconnect(self, client: mqtt.Client, userdata, rc): + super().on_disconnect(client, userdata, rc) + for node in self._nodes: + node.on_disconnect() + + def on_message(self, client: mqtt.Client, userdata, msg): + try: + topic = msg.topic + for node in self._nodes: + node.on_message(topic[len(f'{self._topic_prefix}/{node.id}/'):], msg.payload) + except Exception as e: + self._logger.exception(str(e)) + + def add_node(self, node: MqttNode): + self._nodes.append(node) + if self._connected: + node.on_connect(self) + + def subscribe(self, + node_id: str, + topic: str, + qos: int): + self._client.subscribe(f'{self._topic_prefix}/{node_id}/{topic}', qos) + + def publish(self, + node_id: str, + topic: str, + payload: bytes, + qos: int): + self._client.publish(f'{self._topic_prefix}/{node_id}/{topic}', payload, qos) + self._client.loop_write() diff --git a/src/home/mqtt/module/diagnostics.py b/src/home/mqtt/module/diagnostics.py index c31cce2..fa6cc8e 100644 --- a/src/home/mqtt/module/diagnostics.py +++ b/src/home/mqtt/module/diagnostics.py @@ -48,14 +48,17 @@ class DiagnosticsPayload(MqttPayload): class MqttDiagnosticsModule(MqttModule): - def init(self, mqtt: MqttNode): + def on_connect(self, mqtt: MqttNode): + super().on_connect(mqtt) for topic in ('diag', 'd1ag', 'stat', 'stat1'): mqtt.subscribe_module(topic, self) def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: + message = None if topic in ('stat', 'diag'): message = DiagnosticsPayload.unpack(payload) elif topic in ('stat1', 'd1ag'): message = InitialDiagnosticsPayload.unpack(payload) - self._logger.debug(message) + if message: + self._logger.debug(message) return message diff --git a/src/home/mqtt/module/ota.py b/src/home/mqtt/module/ota.py index 5a1a309..e71cccc 100644 --- a/src/home/mqtt/module/ota.py +++ b/src/home/mqtt/module/ota.py @@ -42,18 +42,15 @@ class OtaPayload(MqttPayload): class MqttOtaModule(MqttModule): _ota_request: Optional[tuple[str, str, int]] - _mqtt_ref: Optional[MqttNode] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._ota_request = None - self._mqtt_ref = None - def init(self, mqtt: MqttNode): + def on_connect(self, mqtt: MqttNode): + super().on_connect(mqtt) mqtt.subscribe_module("otares", self) - self._mqtt_ref = mqtt - if self._ota_request is not None: secret, filename, qos = self._ota_request self._ota_request = None @@ -67,9 +64,9 @@ class MqttOtaModule(MqttModule): def do_push_ota(self, secret: str, filename: str, qos: int): payload = OtaPayload(secret=secret, filename=filename) - self._mqtt_ref.publish('ota', - payload=payload.pack(), - qos=qos) + self._mqtt_node_ref.publish('ota', + payload=payload.pack(), + qos=qos) def push_ota(self, secret: str, @@ -78,4 +75,4 @@ class MqttOtaModule(MqttModule): if not self._initialized: self._ota_request = (secret, filename, qos) else: - self.do_push_ota(secret, filename, qos) \ No newline at end of file + self.do_push_ota(secret, filename, qos) diff --git a/src/home/mqtt/module/relay.py b/src/home/mqtt/module/relay.py index bf22bfe..ae88ddb 100644 --- a/src/home/mqtt/module/relay.py +++ b/src/home/mqtt/module/relay.py @@ -58,16 +58,16 @@ class MqttRelayState: class MqttRelayModule(MqttModule): - def init(self, mqtt: MqttNode): + def on_connect(self, mqtt: MqttNode): + super().on_connect(mqtt) mqtt.subscribe_module('relay/switch', self) mqtt.subscribe_module('relay/status', self) - @staticmethod - def switchpower(mqtt: MqttNode, + def switchpower(self, enable: bool, secret: str): payload = MqttPowerSwitchPayload(secret=secret, state=enable) - mqtt.publish('relay/switch', payload=payload.pack()) + self._mqtt_node_ref.publish('relay/switch', payload=payload.pack()) def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: message = None diff --git a/src/home/mqtt/module/temphum.py b/src/home/mqtt/module/temphum.py index 0e43f1b..9cdfedb 100644 --- a/src/home/mqtt/module/temphum.py +++ b/src/home/mqtt/module/temphum.py @@ -4,6 +4,7 @@ from .._module import MqttModule from .._payload import MqttPayload from ...util import HashableEnum from typing import Optional +from ...temphum import BaseSensor two_digits_precision = lambda x: round(x, 2) @@ -44,9 +45,17 @@ class MqttTempHumNodes(HashableEnum): class MqttTempHumModule(MqttModule): - def init(self, mqtt: MqttNode): + def __init__(self, sensor: Optional[BaseSensor] = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self._sensor = sensor + + def on_connect(self, mqtt: MqttNode): + super().on_connect(mqtt) mqtt.subscribe_module('temphum/data', self) + def tick(self): + pass + def handle_payload(self, mqtt: MqttNode, topic: str, @@ -54,4 +63,4 @@ class MqttTempHumModule(MqttModule): if topic == 'temphum/data': message = MqttTemphumDataPayload.unpack(payload) self._logger.debug(message) - return message \ No newline at end of file + return message diff --git a/src/home/mqtt/mqtt.py b/src/home/mqtt/mqtt.py index fad5d26..ba32889 100644 --- a/src/home/mqtt/mqtt.py +++ b/src/home/mqtt/mqtt.py @@ -5,6 +5,7 @@ import logging from ..config import config from ._payload import * +from typing import Optional def username_and_password() -> Tuple[str, str]: @@ -13,11 +14,13 @@ def username_and_password() -> Tuple[str, str]: return username, password -class MqttBase: +class Mqtt: _connected: bool - def __init__(self, clean_session=True): - self._client = mqtt.Client(client_id=config['mqtt']['client_id'], + def __init__(self, + clean_session=True, + client_id: Optional[str] = None): + self._client = mqtt.Client(client_id=config['mqtt']['client_id'] if not client_id else client_id, protocol=mqtt.MQTTv311, clean_session=clean_session) self._client.on_connect = self.on_connect @@ -81,14 +84,3 @@ class MqttBase: def on_publish(self, client: mqtt.Client, userdata, mid): 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/relay.py b/src/home/mqtt/relay.py deleted file mode 100644 index cf657f7..0000000 --- a/src/home/mqtt/relay.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -import paho.mqtt.client as mqtt -import re -import logging - -from .mqtt import MQTTBase - - -class MQTTRelayClient(MQTTBase): - _home_id: str - - def __init__(self, home_id: str): - super().__init__(clean_session=True) - self._home_id = home_id - - def on_connect(self, client: mqtt.Client, userdata, flags, rc): - super().on_connect(client, userdata, flags, rc) - - topic = f'home/{self._home_id}/#' - self._logger.info(f"subscribing to {topic}") - - client.subscribe(topic, qos=1) - - def on_message(self, client: mqtt.Client, userdata, msg): - try: - match = re.match(r'^home/(.*?)/relay/(stat|power)(?:/(.+))?$', msg.topic) - self._logger.info(f'topic: {msg.topic}') - if not match: - return - - name = match.group(1) - subtopic = match.group(2) - - if name != self._home_id: - return - - 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 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/util.py b/src/home/mqtt/util.py index 91b6baf..390d463 100644 --- a/src/home/mqtt/util.py +++ b/src/home/mqtt/util.py @@ -1,38 +1,15 @@ -import time import os import re -import importlib -from ._node import MqttNode -from . import MqttModule from typing import List -def poll_tick(freq): - t = time.time() - 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')): + modules_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'module') + for name in os.listdir(modules_dir): + if os.path.isdir(os.path.join(modules_dir, name)): + continue 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/temphum/__init__.py b/src/home/temphum/__init__.py index 55a7e1f..46d14e6 100644 --- a/src/home/temphum/__init__.py +++ b/src/home/temphum/__init__.py @@ -1,18 +1 @@ -from .base import SensorType, TempHumSensor -from .si7021 import Si7021 -from .dht12 import DHT12 - -__all__ = [ - 'SensorType', - 'TempHumSensor', - 'create_sensor' -] - - -def create_sensor(type: SensorType, bus: int) -> TempHumSensor: - if type == SensorType.Si7021: - return Si7021(bus) - elif type == SensorType.DHT12: - return DHT12(bus) - else: - raise ValueError('unexpected sensor type') +from .base import SensorType, BaseSensor diff --git a/src/home/temphum/base.py b/src/home/temphum/base.py index e774433..602cab7 100644 --- a/src/home/temphum/base.py +++ b/src/home/temphum/base.py @@ -1,25 +1,19 @@ -import smbus - -from abc import abstractmethod, ABC +from abc import ABC from enum import Enum -class TempHumSensor: - @abstractmethod +class BaseSensor(ABC): + def __init__(self, bus: int): + super().__init__() + self.bus = smbus.SMBus(bus) + def humidity(self) -> float: pass - @abstractmethod def temperature(self) -> float: pass -class I2CTempHumSensor(TempHumSensor, ABC): - def __init__(self, bus: int): - super().__init__() - self.bus = smbus.SMBus(bus) - - class SensorType(Enum): Si7021 = 'si7021' - DHT12 = 'dht12' + DHT12 = 'dht12' \ No newline at end of file diff --git a/src/home/temphum/dht12.py b/src/home/temphum/dht12.py deleted file mode 100644 index d495766..0000000 --- a/src/home/temphum/dht12.py +++ /dev/null @@ -1,22 +0,0 @@ -from .base import I2CTempHumSensor - - -class DHT12(I2CTempHumSensor): - i2c_addr = 0x5C - - def _measure(self): - raw = self.bus.read_i2c_block_data(self.i2c_addr, 0, 5) - if (raw[0] + raw[1] + raw[2] + raw[3]) & 0xff != raw[4]: - raise ValueError("checksum error") - return raw - - def temperature(self) -> float: - raw = self._measure() - temp = raw[2] + (raw[3] & 0x7f) * 0.1 - if raw[3] & 0x80: - temp *= -1 - return temp - - def humidity(self) -> float: - raw = self._measure() - return raw[0] + raw[1] * 0.1 diff --git a/src/home/temphum/i2c.py b/src/home/temphum/i2c.py new file mode 100644 index 0000000..7d8e2e3 --- /dev/null +++ b/src/home/temphum/i2c.py @@ -0,0 +1,52 @@ +import abc +import smbus + +from .base import BaseSensor, SensorType + + +class I2CSensor(BaseSensor, abc.ABC): + def __init__(self, bus: int): + super().__init__() + self.bus = smbus.SMBus(bus) + + +class DHT12(I2CSensor): + i2c_addr = 0x5C + + def _measure(self): + raw = self.bus.read_i2c_block_data(self.i2c_addr, 0, 5) + if (raw[0] + raw[1] + raw[2] + raw[3]) & 0xff != raw[4]: + raise ValueError("checksum error") + return raw + + def temperature(self) -> float: + raw = self._measure() + temp = raw[2] + (raw[3] & 0x7f) * 0.1 + if raw[3] & 0x80: + temp *= -1 + return temp + + def humidity(self) -> float: + raw = self._measure() + return raw[0] + raw[1] * 0.1 + + +class Si7021(I2CSensor): + i2c_addr = 0x40 + + def temperature(self) -> float: + raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE3, 2) + return 175.72 * (raw[0] << 8 | raw[1]) / 65536.0 - 46.85 + + def humidity(self) -> float: + raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE5, 2) + return 125.0 * (raw[0] << 8 | raw[1]) / 65536.0 - 6.0 + + +def create_sensor(type: SensorType, bus: int) -> BaseSensor: + if type == SensorType.Si7021: + return Si7021(bus) + elif type == SensorType.DHT12: + return DHT12(bus) + else: + raise ValueError('unexpected sensor type') diff --git a/src/home/temphum/si7021.py b/src/home/temphum/si7021.py deleted file mode 100644 index 6289e15..0000000 --- a/src/home/temphum/si7021.py +++ /dev/null @@ -1,13 +0,0 @@ -from .base import I2CTempHumSensor - - -class Si7021(I2CTempHumSensor): - i2c_addr = 0x40 - - def temperature(self) -> float: - raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE3, 2) - return 175.72 * (raw[0] << 8 | raw[1]) / 65536.0 - 46.85 - - def humidity(self) -> float: - raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE5, 2) - return 125.0 * (raw[0] << 8 | raw[1]) / 65536.0 - 6.0 diff --git a/src/home/util.py b/src/home/util.py index 93a9d8f..a721682 100644 --- a/src/home/util.py +++ b/src/home/util.py @@ -193,4 +193,11 @@ def filesize_fmt(num, suffix="B") -> str: class HashableEnum(Enum): def hash(self) -> int: - return adler32(self.name.encode()) \ No newline at end of file + return adler32(self.name.encode()) + + +def next_tick_gen(freq): + t = time.time() + while True: + t += freq + yield max(t - time.time(), 0) \ No newline at end of file diff --git a/src/inverter_mqtt_receiver.py b/src/inverter_mqtt_receiver.py index d40647e..cc0f1ff 100755 --- a/src/inverter_mqtt_receiver.py +++ b/src/inverter_mqtt_receiver.py @@ -2,13 +2,13 @@ import paho.mqtt.client as mqtt import re -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.mqtt.payload.inverter import Status, Generation from home.database import InverterDatabase from home.config import config -class MqttReceiver(MqttBase): +class MqttReceiver(Mqtt): def __init__(self): super().__init__(clean_session=False) self.database = InverterDatabase() diff --git a/src/inverter_mqtt_sender.py b/src/inverter_mqtt_sender.py index fb2a2d8..3215d18 100755 --- a/src/inverter_mqtt_sender.py +++ b/src/inverter_mqtt_sender.py @@ -5,11 +5,11 @@ import json import inverterd from home.config import config -from home.mqtt import MqttBase, poll_tick +from home.mqtt import Mqtt, poll_tick from home.mqtt.payload.inverter import Status, Generation -class MqttClient(MqttBase): +class MqttClient(Mqtt): def __init__(self): super().__init__() diff --git a/src/mqtt_node_util.py b/src/mqtt_node_util.py index 0352c8f..d7ee127 100755 --- a/src/mqtt_node_util.py +++ b/src/mqtt_node_util.py @@ -5,19 +5,10 @@ from typing import Optional from argparse import ArgumentParser, ArgumentError from home.config import config -from home.mqtt import MqttNode, get_mqtt_modules, import_mqtt_module, MqttModule +from home.mqtt import MqttNode, MqttModule, MqttWrapper, get_mqtt_modules -mqtt: Optional[MqttNode] = None - - -def add_module(module: str) -> MqttModule: - module = import_mqtt_module(module) - if not hasattr(module, 'MODULE_NAME'): - raise RuntimeError(f'MODULE_NAME not found in module {m}') - cl = getattr(module, getattr(module, 'MODULE_NAME')) - instance = cl() - mqtt.add_module(instance) - return instance +mqtt_node: Optional[MqttNode] = None +mqtt: Optional[MqttWrapper] = None if __name__ == '__main__': @@ -38,19 +29,22 @@ if __name__ == '__main__': if (arg.switch_relay is not None or arg.node_secret is not None) and 'relay' not in arg.modules: raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules') - mqtt = MqttNode(node_id=arg.node_id) + mqtt = MqttWrapper(randomize_client_id=True) + mqtt_node = MqttNode(node_id=arg.node_id) + + mqtt.add_node(mqtt_node) # must-have modules - ota_module = add_module('ota') - add_module('diagnostics') + ota_module = mqtt_node.load_module('ota') + mqtt_node.load_module('diagnostics') if arg.modules: for m in arg.modules: - module_instance = add_module(m) + module_instance = mqtt_node.load_module(m) if m == 'relay' and arg.switch_relay is not None: if not arg.node_secret: raise ArgumentError(None, '--switch-relay requires --node-secret') - module_instance.switchpower(mqtt, + module_instance.switchpower(mqtt_node, arg.switch_relay == 1, arg.node_secret) @@ -61,10 +55,10 @@ if __name__ == '__main__': if arg.push_ota: if not os.path.exists(arg.push_ota): raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') - if not arg.node_secret: - raise ArgumentError(None, 'pushing OTA requires --node-secret') + if not arg.node_secret: + raise ArgumentError(None, 'pushing OTA requires --node-secret') - ota_module.push_ota(arg.node_secret, arg.push_ota, 1) + ota_module.push_ota(arg.node_secret, arg.push_ota, 1) while True: sleep(0.1) diff --git a/src/polaris_kettle_bot.py b/src/polaris_kettle_bot.py index 088707d..8438ab3 100755 --- a/src/polaris_kettle_bot.py +++ b/src/polaris_kettle_bot.py @@ -10,7 +10,7 @@ import paho.mqtt.client as mqtt from home.telegram import bot from home.api.types import BotType -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.config import config from home.util import chunks from syncleo import ( @@ -204,7 +204,7 @@ class KettleInfo: class KettleController(threading.Thread, - MqttBase, + Mqtt, DeviceListener, IncomingMessageListener, KettleInfoListener, @@ -224,7 +224,7 @@ class KettleController(threading.Thread, def __init__(self): # basic setup - MqttBase.__init__(self, clean_session=False) + Mqtt.__init__(self, clean_session=False) threading.Thread.__init__(self) self._logger = logging.getLogger(self.__class__.__name__) diff --git a/src/polaris_kettle_util.py b/src/polaris_kettle_util.py index 81326dd..816cff7 100755 --- a/src/polaris_kettle_util.py +++ b/src/polaris_kettle_util.py @@ -8,7 +8,7 @@ import paho.mqtt.client as mqtt from typing import Optional from argparse import ArgumentParser from queue import SimpleQueue -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.config import config from syncleo import ( Kettle, @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) control_tasks = SimpleQueue() -class MqttServer(MqttBase): +class MqttServer(Mqtt): def __init__(self): super().__init__(clean_session=False) diff --git a/src/pump_bot.py b/src/pump_bot.py index 48efec4..ab73097 100755 --- a/src/pump_bot.py +++ b/src/pump_bot.py @@ -10,16 +10,17 @@ from home.telegram import bot from home.telegram._botutil import user_any_name from home.relay.sunxi_h3_client import RelayClient from home.api.types import BotType -from home.mqtt import MqttNode, MqttModule, MqttPayload, add_mqtt_module -from home.mqtt.module.relay import MqttPowerStatusPayload +from home.mqtt import MqttNode, MqttWrapper, MqttPayload +from home.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule from home.mqtt.module.temphum import MqttTemphumDataPayload from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload config.load('pump_bot') -mqtt: Optional[MqttNode] = None -mqtt_relay_module: Optional[MqttModule] = None +mqtt: Optional[MqttWrapper] = None +mqtt_node: Optional[MqttNode] = None +mqtt_relay_module: Optional[MqttRelayModule] = None time_format = '%d.%m.%Y, %H:%M:%S' watering_mcu_status = { @@ -117,13 +118,13 @@ def off(ctx: bot.Context, silent=False) -> None: def watering_on(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(mqtt, True, config.get('mqtt_water_relay.secret')) + mqtt_relay_module.switchpower(True, config.get('mqtt_water_relay.secret')) ctx.reply(ctx.lang('sent')) notify(ctx.user, UserAction.WATERING_ON) def watering_off(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(mqtt, False, config.get('mqtt_water_relay.secret')) + mqtt_relay_module.switchpower(False, config.get('mqtt_water_relay.secret')) ctx.reply(ctx.lang('sent')) notify(ctx.user, UserAction.WATERING_OFF) @@ -233,14 +234,15 @@ def mqtt_payload_callback(payload: MqttPayload): if __name__ == '__main__': - mqtt = MqttNode(node_id=config.get('mqtt_water_relay.node_id')) + mqtt = MqttWrapper() + mqtt_node = MqttNode(node_id=config.get('mqtt_water_relay.node_id')) if is_development_mode(): - add_mqtt_module(mqtt, 'diagnostics') + mqtt_node.load_module('diagnostics') - mqtt_relay_module = add_mqtt_module(mqtt, 'temphum') - mqtt_relay_module = add_mqtt_module(mqtt, 'relay') + mqtt_node.load_module('temphum') + mqtt_relay_module = mqtt_node.load_module('relay') - mqtt.add_payload_callback(mqtt_payload_callback) + mqtt_node.add_payload_callback(mqtt_payload_callback) mqtt.configure_tls() mqtt.connect_and_loop(loop_forever=False) @@ -252,4 +254,3 @@ if __name__ == '__main__': mqtt.disconnect() except: pass - diff --git a/src/sensors_mqtt_receiver.py b/src/sensors_mqtt_receiver.py index a377ddd..f7cb467 100755 --- a/src/sensors_mqtt_receiver.py +++ b/src/sensors_mqtt_receiver.py @@ -2,7 +2,7 @@ import paho.mqtt.client as mqtt import re -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.config import config from home.mqtt.payload.sensors import Temperature from home.api.types import TemperatureSensorLocation @@ -16,7 +16,7 @@ def get_sensor_type(sensor: str) -> TemperatureSensorLocation: raise ValueError(f'unexpected sensor value: {sensor}') -class MqttServer(MqttBase): +class MqttServer(Mqtt): def __init__(self): super().__init__(clean_session=False) self.database = SensorsDatabase() diff --git a/src/sensors_mqtt_sender.py b/src/sensors_mqtt_sender.py index 87a28ca..393962a 100755 --- a/src/sensors_mqtt_sender.py +++ b/src/sensors_mqtt_sender.py @@ -3,12 +3,12 @@ import time import json from home.util import parse_addr, MySimpleSocketClient -from home.mqtt import MqttBase, poll_tick +from home.mqtt import Mqtt, poll_tick from home.mqtt.payload.sensors import Temperature from home.config import config -class MqttClient(MqttBase): +class MqttClient(Mqtt): def __init__(self): super().__init__(self) self._home_id = config['mqtt']['home_id'] diff --git a/src/temphum_mqtt_node.py b/src/temphum_mqtt_node.py index f4d1fca..bcd72da 100755 --- a/src/temphum_mqtt_node.py +++ b/src/temphum_mqtt_node.py @@ -6,10 +6,11 @@ import logging from typing import Optional from home.config import config -from home.temphum import SensorType, create_sensor, TempHumSensor +from home.temphum import SensorType, BaseSensor +from home.temphum.i2c import create_sensor logger = logging.getLogger(__name__) -sensor: Optional[TempHumSensor] = None +sensor: Optional[BaseSensor] = None lock = asyncio.Lock() delay = 0.01 diff --git a/src/temphum_smbus_util.py b/src/temphum_smbus_util.py index 0f90835..c06bacd 100755 --- a/src/temphum_smbus_util.py +++ b/src/temphum_smbus_util.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from argparse import ArgumentParser -from home.temphum import SensorType, create_sensor +from home.temphum import SensorType +from home.temphum.i2c import create_sensor if __name__ == '__main__': diff --git a/src/temphumd.py b/src/temphumd.py index f4d1fca..bcd72da 100755 --- a/src/temphumd.py +++ b/src/temphumd.py @@ -6,10 +6,11 @@ import logging from typing import Optional from home.config import config -from home.temphum import SensorType, create_sensor, TempHumSensor +from home.temphum import SensorType, BaseSensor +from home.temphum.i2c import create_sensor logger = logging.getLogger(__name__) -sensor: Optional[TempHumSensor] = None +sensor: Optional[BaseSensor] = None lock = asyncio.Lock() delay = 0.01 diff --git a/tools/mcuota.py b/tools/mcuota.py deleted file mode 100755 index 46968a8..0000000 --- a/tools/mcuota.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) - -from time import sleep -from argparse import ArgumentParser -from src.home.config import config -from src.home.mqtt import MqttRelay -from src.home.mqtt.esp import MqttEspDevice - - -def guess_filename(product: str, build_target: str): - return os.path.join( - products_dir, - product, - '.pio', - 'build', - build_target, - 'firmware.bin' - ) - - -def relayctl_publish_ota(filename: str, - device_id: str, - home_secret: str, - qos: int): - global stop - - def published(): - global stop - stop = True - - mqtt_relay = MqttRelay(devices=MqttEspDevice(id=device_id, secret=home_secret)) - mqtt_relay.configure_tls() - mqtt_relay.connect_and_loop(loop_forever=False) - mqtt_relay.push_ota(device_id, filename, published, qos) - while not stop: - sleep(0.1) - mqtt_relay.disconnect() - - -stop = False -products = { - 'relayctl': { - 'build_target': 'esp12e', - 'callback': relayctl_publish_ota - } -} - -products_dir = os.path.join( - os.path.dirname(__file__), - '..', - 'platformio' -) - - -def main(): - parser = ArgumentParser() - parser.add_argument('--filename', type=str) - parser.add_argument('--device-id', type=str, required=True) - parser.add_argument('--product', type=str, required=True) - parser.add_argument('--qos', type=int, default=1) - - config.load('mcuota_push', parser=parser) - arg = parser.parse_args() - - if arg.product not in products: - raise ValueError(f'invalid product: \'{arg.product}\' not found') - - if arg.device_id not in config['mqtt']['home_secrets']: - raise ValueError(f'home_secret for home {arg.device_id} not found in config!') - - filename = arg.filename if arg.filename else guess_filename(arg.product, products[arg.product]['build_target']) - if not os.path.exists(filename): - raise OSError(f'file \'{filename}\' does not exists') - - print('Please confirm following OTA params.') - print('') - print(f' Device ID: {arg.device_id}') - print(f' Product: {arg.product}') - print(f'Firmware file: {filename}') - print('') - input('Press any key to continue or Ctrl+C to abort.') - - products[arg.product]['callback'](filename, arg.device_id, config['mqtt']['home_secrets'][arg.device_id], qos=arg.qos) - - -if __name__ == '__main__': - try: - main() - except Exception as e: - print(str(e), file=sys.stderr) - sys.exit(1) diff --git a/tools/mcuota.sh b/tools/mcuota.sh deleted file mode 100755 index b2e7910..0000000 --- a/tools/mcuota.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" - -. "$DIR/lib.bash" - -if [ -d "$DIR/../venv" ]; then - echoinfo "activating python venv" - . "$DIR/../venv/bin/activate" -else - echowarn "python venv not found" -fi - -"$DIR/mcuota.py" "$@" \ No newline at end of file -- cgit v1.2.3