diff options
Diffstat (limited to 'src/home/mqtt/payload')
-rw-r--r-- | src/home/mqtt/payload/base_payload.py | 129 | ||||
-rw-r--r-- | src/home/mqtt/payload/inverter.py | 73 | ||||
-rw-r--r-- | src/home/mqtt/payload/relay.py | 54 | ||||
-rw-r--r-- | src/home/mqtt/payload/sensors.py | 20 |
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 |