summaryrefslogtreecommitdiff
path: root/src/home/mqtt/payload
diff options
context:
space:
mode:
Diffstat (limited to 'src/home/mqtt/payload')
-rw-r--r--src/home/mqtt/payload/base_payload.py129
-rw-r--r--src/home/mqtt/payload/inverter.py73
-rw-r--r--src/home/mqtt/payload/relay.py54
-rw-r--r--src/home/mqtt/payload/sensors.py20
4 files changed, 276 insertions, 0 deletions
diff --git a/src/home/mqtt/payload/base_payload.py b/src/home/mqtt/payload/base_payload.py
new file mode 100644
index 0000000..c9ec907
--- /dev/null
+++ b/src/home/mqtt/payload/base_payload.py
@@ -0,0 +1,129 @@
+import abc
+import struct
+
+from typing import Generic, TypeVar
+
+
+class MQTTPayload(abc.ABC):
+ FORMAT = ''
+ PACKER = {}
+ UNPACKER = {}
+
+ def __init__(self, **kwargs):
+ for field in self.__class__.__annotations__:
+ setattr(self, field, kwargs[field])
+
+ def pack(self):
+ args = []
+ bf_number = -1
+ bf_arg = 0
+ bf_progress = 0
+
+ for field, field_type in self.__class__.__annotations__.items():
+ field_type_origin = None
+ if hasattr(field_type, '__extra__') or hasattr(field_type, '__origin__'):
+ try:
+ field_type_origin = field_type.__extra__
+ except AttributeError:
+ field_type_origin = field_type.__origin__
+
+ if field_type_origin is not None and issubclass(field_type_origin, MQTTPayloadBitField):
+ n, s, b = field_type.__args__
+ if n != bf_number:
+ if bf_number != -1:
+ args.append(bf_arg)
+ bf_number = n
+ bf_progress = 0
+ bf_arg = 0
+ bf_arg |= (getattr(self, field) & (2 ** b - 1)) << bf_progress
+ bf_progress += b
+
+ else:
+ if bf_number != -1:
+ args.append(bf_arg)
+ bf_number = -1
+ bf_progress = 0
+ bf_arg = 0
+
+ args.append(self._pack_field(field))
+
+ if bf_number != -1:
+ args.append(bf_arg)
+
+ return struct.pack(self.FORMAT, *args)
+
+ @classmethod
+ def unpack(cls, buf: bytes):
+ data = struct.unpack(cls.FORMAT, buf)
+ kwargs = {}
+ i = 0
+ bf_number = -1
+ bf_progress = 0
+
+ for field, field_type in cls.__annotations__.items():
+ field_type_origin = None
+ if hasattr(field_type, '__extra__') or hasattr(field_type, '__origin__'):
+ try:
+ field_type_origin = field_type.__extra__
+ except AttributeError:
+ field_type_origin = field_type.__origin__
+
+ if field_type_origin is not None and issubclass(field_type_origin, MQTTPayloadBitField):
+ n, s, b = field_type.__args__
+ if n != bf_number:
+ bf_number = n
+ bf_progress = 0
+ kwargs[field] = (data[i] >> bf_progress) & (2 ** b - 1)
+ bf_progress += b
+ continue # don't increment i
+
+ if bf_number != -1:
+ bf_number = -1
+ i += 1
+
+ if issubclass(field_type, MQTTPayloadCustomField):
+ kwargs[field] = field_type.unpack(data[i])
+ else:
+ kwargs[field] = cls._unpack_field(field, data[i])
+ i += 1
+ return cls(**kwargs)
+
+ def _pack_field(self, name):
+ val = getattr(self, name)
+ if self.PACKER and name in self.PACKER:
+ return self.PACKER[name](val)
+ else:
+ return val
+
+ @classmethod
+ def _unpack_field(cls, name, val):
+ if isinstance(val, MQTTPayloadCustomField):
+ return
+ if cls.UNPACKER and name in cls.UNPACKER:
+ return cls.UNPACKER[name](val)
+ else:
+ return val
+
+
+class MQTTPayloadCustomField(abc.ABC):
+ def __init__(self, **kwargs):
+ for field in self.__class__.__annotations__:
+ setattr(self, field, kwargs[field])
+
+ @abc.abstractmethod
+ def __index__(self):
+ pass
+
+ @classmethod
+ @abc.abstractmethod
+ def unpack(cls, *args, **kwargs):
+ pass
+
+
+NT = TypeVar('NT') # number of bit field
+ST = TypeVar('ST') # size in bytes
+BT = TypeVar('BT') # size in bits of particular value
+
+
+class MQTTPayloadBitField(int, Generic[NT, ST, BT]):
+ pass
diff --git a/src/home/mqtt/payload/inverter.py b/src/home/mqtt/payload/inverter.py
new file mode 100644
index 0000000..b3f4edd
--- /dev/null
+++ b/src/home/mqtt/payload/inverter.py
@@ -0,0 +1,73 @@
+import struct
+
+from .base_payload import MQTTPayload, MQTTPayloadBitField
+from typing import Tuple
+
+_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: MQTTPayloadBitField[0, 16, 2]
+ mppt2_charger_status: MQTTPayloadBitField[0, 16, 2]
+ battery_power_direction: MQTTPayloadBitField[0, 16, 2]
+ dc_ac_power_direction: MQTTPayloadBitField[0, 16, 2]
+ line_power_direction: MQTTPayloadBitField[0, 16, 2]
+ load_connected: MQTTPayloadBitField[0, 16, 1]
+
+
+class Generation(MQTTPayload):
+ # 8 bytes
+ FORMAT = 'II'
+
+ time: int
+ wh: int
diff --git a/src/home/mqtt/payload/relay.py b/src/home/mqtt/payload/relay.py
new file mode 100644
index 0000000..2a327ba
--- /dev/null
+++ b/src/home/mqtt/payload/relay.py
@@ -0,0 +1,54 @@
+from .base_payload import MQTTPayload, MQTTPayloadCustomField
+
+
+class StatFlags(MQTTPayloadCustomField):
+ state: bool
+ config_changed_value_present: bool
+ config_changed: bool
+
+ @staticmethod
+ def unpack(flags: int):
+ state = flags & 0x1
+ ccvp = (flags >> 1) & 0x1
+ cc = (flags >> 2) & 0x1
+ return StatFlags(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 InitialStatPayload(MQTTPayload):
+ FORMAT = 'IBbIB'
+
+ ip: int
+ fw_version: int
+ rssi: int
+ free_heap: int
+ flags: StatFlags
+
+
+class StatPayload(MQTTPayload):
+ FORMAT = 'bIB'
+
+ rssi: int
+ free_heap: int
+ flags: StatFlags
+
+
+class PowerPayload(MQTTPayload):
+ FORMAT = '12sB'
+ PACKER = {
+ 'state': lambda n: int(n)
+ }
+ UNPACKER = {
+ 'state': lambda n: bool(n)
+ }
+
+ secret: str
+ state: bool
diff --git a/src/home/mqtt/payload/sensors.py b/src/home/mqtt/payload/sensors.py
new file mode 100644
index 0000000..3ecc243
--- /dev/null
+++ b/src/home/mqtt/payload/sensors.py
@@ -0,0 +1,20 @@
+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