summaryrefslogtreecommitdiff
path: root/src/home/mqtt/module
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-05-31 09:22:00 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-06-03 01:01:27 +0300
commit0f0a5fd44867b684bcbafd102b6b769ae61229b4 (patch)
treed4845bd3295159c06041b25753af4eb2d7b59461 /src/home/mqtt/module
parent3e3753d726f8a02d98368f20f77dd9fa739e3d80 (diff)
wip
Diffstat (limited to 'src/home/mqtt/module')
-rw-r--r--src/home/mqtt/module/diagnostics.py61
-rw-r--r--src/home/mqtt/module/inverter.py77
-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
5 files changed, 344 insertions, 0 deletions
diff --git a/src/home/mqtt/module/diagnostics.py b/src/home/mqtt/module/diagnostics.py
new file mode 100644
index 0000000..c31cce2
--- /dev/null
+++ b/src/home/mqtt/module/diagnostics.py
@@ -0,0 +1,61 @@
+from ..mqtt import MqttPayload, MqttPayloadCustomField
+from .._node import MqttNode, MqttModule
+from typing import Optional
+
+MODULE_NAME = 'MqttDiagnosticsModule'
+
+
+class DiagnosticsFlags(MqttPayloadCustomField):
+ state: bool
+ config_changed_value_present: bool
+ config_changed: bool
+
+ @staticmethod
+ def unpack(flags: int):
+ # _logger.debug(f'StatFlags.unpack: flags={flags}')
+ state = flags & 0x1
+ ccvp = (flags >> 1) & 0x1
+ cc = (flags >> 2) & 0x1
+ # _logger.debug(f'StatFlags.unpack: state={state}')
+ return DiagnosticsFlags(state=(state == 1),
+ config_changed_value_present=(ccvp == 1),
+ config_changed=(cc == 1))
+
+ def __index__(self):
+ bits = 0
+ bits |= (int(self.state) & 0x1)
+ bits |= (int(self.config_changed_value_present) & 0x1) << 1
+ bits |= (int(self.config_changed) & 0x1) << 2
+ return bits
+
+
+class InitialDiagnosticsPayload(MqttPayload):
+ FORMAT = '=IBbIB'
+
+ ip: int
+ fw_version: int
+ rssi: int
+ free_heap: int
+ flags: DiagnosticsFlags
+
+
+class DiagnosticsPayload(MqttPayload):
+ FORMAT = '=bIB'
+
+ 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/module/inverter.py b/src/home/mqtt/module/inverter.py
new file mode 100644
index 0000000..9cf2978
--- /dev/null
+++ b/src/home/mqtt/module/inverter.py
@@ -0,0 +1,77 @@
+import struct
+
+from .._node import MqttNode
+from .._payload import MqttPayload, bit_field
+
+_mult_10 = lambda n: int(n*10)
+_div_10 = lambda n: n/10
+
+
+class Status(MqttPayload):
+ # 46 bytes
+ FORMAT = 'IHHHHHHBHHHHHBHHHHHHHH'
+
+ PACKER = {
+ 'grid_voltage': _mult_10,
+ 'grid_freq': _mult_10,
+ 'ac_output_voltage': _mult_10,
+ 'ac_output_freq': _mult_10,
+ 'battery_voltage': _mult_10,
+ 'battery_voltage_scc': _mult_10,
+ 'battery_voltage_scc2': _mult_10,
+ 'pv1_input_voltage': _mult_10,
+ 'pv2_input_voltage': _mult_10
+ }
+ UNPACKER = {
+ 'grid_voltage': _div_10,
+ 'grid_freq': _div_10,
+ 'ac_output_voltage': _div_10,
+ 'ac_output_freq': _div_10,
+ 'battery_voltage': _div_10,
+ 'battery_voltage_scc': _div_10,
+ 'battery_voltage_scc2': _div_10,
+ 'pv1_input_voltage': _div_10,
+ 'pv2_input_voltage': _div_10
+ }
+
+ time: int
+ grid_voltage: float
+ grid_freq: float
+ ac_output_voltage: float
+ ac_output_freq: float
+ ac_output_apparent_power: int
+ ac_output_active_power: int
+ output_load_percent: int
+ battery_voltage: float
+ battery_voltage_scc: float
+ battery_voltage_scc2: float
+ battery_discharge_current: int
+ battery_charge_current: int
+ battery_capacity: int
+ inverter_heat_sink_temp: int
+ mppt1_charger_temp: int
+ mppt2_charger_temp: int
+ pv1_input_power: int
+ pv2_input_power: int
+ pv1_input_voltage: float
+ pv2_input_voltage: float
+
+ # H
+ mppt1_charger_status: bit_field(0, 16, 2)
+ mppt2_charger_status: bit_field(0, 16, 2)
+ battery_power_direction: bit_field(0, 16, 2)
+ dc_ac_power_direction: bit_field(0, 16, 2)
+ line_power_direction: bit_field(0, 16, 2)
+ load_connected: bit_field(0, 16, 1)
+
+
+class Generation(MqttPayload):
+ # 8 bytes
+ FORMAT = 'II'
+
+ 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