diff options
Diffstat (limited to 'src/home/mqtt')
-rw-r--r-- | src/home/mqtt/message/__init__.py | 2 | ||||
-rw-r--r-- | src/home/mqtt/message/inverter.py | 86 | ||||
-rw-r--r-- | src/home/mqtt/message/sensors.py | 19 | ||||
-rw-r--r-- | src/home/mqtt/mqtt.py | 30 | ||||
-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 |
8 files changed, 291 insertions, 122 deletions
diff --git a/src/home/mqtt/message/__init__.py b/src/home/mqtt/message/__init__.py deleted file mode 100644 index 2a2221b..0000000 --- a/src/home/mqtt/message/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .inverter import Status, Generation -from .sensors import Temperature diff --git a/src/home/mqtt/message/inverter.py b/src/home/mqtt/message/inverter.py deleted file mode 100644 index d36aad5..0000000 --- a/src/home/mqtt/message/inverter.py +++ /dev/null @@ -1,86 +0,0 @@ -import struct - -from typing import Tuple - - -class Status: - # 46 bytes - format = 'IHHHHHHBHHHHHBHHHHHHHH' - - def pack(self, time: int, data: dict) -> bytes: - bits = 0 - bits |= (data['mppt1_charger_status'] & 0x3) - bits |= (data['mppt2_charger_status'] & 0x3) << 2 - bits |= (data['battery_power_direction'] & 0x3) << 4 - bits |= (data['dc_ac_power_direction'] & 0x3) << 6 - bits |= (data['line_power_direction'] & 0x3) << 8 - bits |= (data['load_connected'] & 0x1) << 10 - - return struct.pack( - self.format, - time, - int(data['grid_voltage'] * 10), - int(data['grid_freq'] * 10), - int(data['ac_output_voltage'] * 10), - int(data['ac_output_freq'] * 10), - data['ac_output_apparent_power'], - data['ac_output_active_power'], - data['output_load_percent'], - int(data['battery_voltage'] * 10), - int(data['battery_voltage_scc'] * 10), - int(data['battery_voltage_scc2'] * 10), - data['battery_discharge_current'], - data['battery_charge_current'], - data['battery_capacity'], - data['inverter_heat_sink_temp'], - data['mppt1_charger_temp'], - data['mppt2_charger_temp'], - data['pv1_input_power'], - data['pv2_input_power'], - int(data['pv1_input_voltage'] * 10), - int(data['pv2_input_voltage'] * 10), - bits - ) - - def unpack(self, buf: bytes) -> Tuple[int, dict]: - data = struct.unpack(self.format, buf) - return data[0], { - 'grid_voltage': data[1] / 10, - 'grid_freq': data[2] / 10, - 'ac_output_voltage': data[3] / 10, - 'ac_output_freq': data[4] / 10, - 'ac_output_apparent_power': data[5], - 'ac_output_active_power': data[6], - 'output_load_percent': data[7], - 'battery_voltage': data[8] / 10, - 'battery_voltage_scc': data[9] / 10, - 'battery_voltage_scc2': data[10] / 10, - 'battery_discharge_current': data[11], - 'battery_charge_current': data[12], - 'battery_capacity': data[13], - 'inverter_heat_sink_temp': data[14], - 'mppt1_charger_temp': data[15], - 'mppt2_charger_temp': data[16], - 'pv1_input_power': data[17], - 'pv2_input_power': data[18], - 'pv1_input_voltage': data[19] / 10, - 'pv2_input_voltage': data[20] / 10, - 'mppt1_charger_status': data[21] & 0x03, - 'mppt2_charger_status': (data[21] >> 2) & 0x03, - 'battery_power_direction': (data[21] >> 4) & 0x03, - 'dc_ac_power_direction': (data[21] >> 6) & 0x03, - 'line_power_direction': (data[21] >> 8) & 0x03, - 'load_connected': (data[21] >> 10) & 0x01, - } - - -class Generation: - # 8 bytes - format = 'II' - - def pack(self, time: int, wh: int) -> bytes: - return struct.pack(self.format, int(time), wh) - - def unpack(self, buf: bytes) -> tuple: - data = struct.unpack(self.format, buf) - return tuple(data) diff --git a/src/home/mqtt/message/sensors.py b/src/home/mqtt/message/sensors.py deleted file mode 100644 index ee522f0..0000000 --- a/src/home/mqtt/message/sensors.py +++ /dev/null @@ -1,19 +0,0 @@ -import struct - -from typing import Tuple - - -class Temperature: - format = 'IhH' - - def pack(self, time: int, temp: float, rh: float) -> bytes: - return struct.pack( - self.format, - time, - int(temp*100), - int(rh*100) - ) - - def unpack(self, buf: bytes) -> Tuple[int, float, float]: - data = struct.unpack(self.format, buf) - return data[0], data[1]/100, data[2]/100 diff --git a/src/home/mqtt/mqtt.py b/src/home/mqtt/mqtt.py index b360d22..b3334b5 100644 --- a/src/home/mqtt/mqtt.py +++ b/src/home/mqtt/mqtt.py @@ -17,18 +17,18 @@ def username_and_password() -> Tuple[str, str]: class MQTTBase: def __init__(self, clean_session=True): - self.client = mqtt.Client(client_id=config['mqtt']['client_id'], - protocol=mqtt.MQTTv311, - clean_session=clean_session) - self.client.on_connect = self.on_connect - self.client.on_disconnect = self.on_disconnect - self.client.on_message = self.on_message + self._client = mqtt.Client(client_id=config['mqtt']['client_id'], + protocol=mqtt.MQTTv311, + clean_session=clean_session) + self._client.on_connect = self.on_connect + self._client.on_disconnect = self.on_disconnect + self._client.on_message = self.on_message - self.home_id = 1 + self._logger = logging.getLogger(self.__class__.__name__) username, password = username_and_password() if username and password: - self.client.username_pw_set(username, password) + self._client.username_pw_set(username, password) def configure_tls(self): ca_certs = os.path.realpath(os.path.join( @@ -39,23 +39,23 @@ 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'] port = config['mqtt']['port'] - self.client.connect(host, port, 60) + self._client.connect(host, port, 60) if loop_forever: - self.client.loop_forever() + self._client.loop_forever() else: - self.client.loop_start() + self._client.loop_start() def on_connect(self, client: mqtt.Client, userdata, flags, rc): - logger.info("Connected with result code " + str(rc)) + self._logger.info("Connected with result code " + str(rc)) def on_disconnect(self, client: mqtt.Client, userdata, rc): - logger.info("Disconnected with result code " + str(rc)) + self._logger.info("Disconnected with result code " + str(rc)) def on_message(self, client: mqtt.Client, userdata, msg): - logger.info(msg.topic + ": " + str(msg.payload)) + self._logger.info(msg.topic + ": " + str(msg.payload)) 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 |