diff --git a/tools/rpi/README.md b/tools/rpi/README.md index f18f5d0f..e4c3bd40 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -44,6 +44,34 @@ Whenever it sees a reply, it will decoded and logged to the given log file. +Inject payloads via MQTT +------------------------ + +To enable mqtt payload injection, this must be configured per inverter +```yaml +... + inverters: +... + - serial: 1147112345 + mqtt: + send_raw_enabled: true +... +``` + +This can be used to inject debug payloads +The message must be in hexlified format + +Use of variables: + * tttttttt expands to current time like we know from our `80 0b` command + +Example injects exactly the same as we normally use to poll data + + $ mosquitto_pub -h broker -t inverter_topic/command -m 800b00tttttttt0000000500000000 + +This allows for even faster hacking during runtime + + + Analysing the Logs ------------------ @@ -68,6 +96,8 @@ Configuration Local settings are read from ahoy.yml An example is provided as ahoy.yml.example + + Todo ---- diff --git a/tools/rpi/ahoy.py b/tools/rpi/ahoy.py index 19aabdb2..19812808 100644 --- a/tools/rpi/ahoy.py +++ b/tools/rpi/ahoy.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- import sys +import struct +import re import time from datetime import datetime import argparse @@ -28,6 +30,7 @@ hmradio = hoymiles.HoymilesNRF(device=radio) mqtt_client = None command_queue = {} +mqtt_command_topic_subs = [] hoymiles.HOYMILES_TRANSACTION_LOGGING=True hoymiles.HOYMILES_DEBUG_LOGGING=True @@ -125,13 +128,46 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): broker.publish(f'{topic}/frequency', data['frequency']) broker.publish(f'{topic}/temperature', data['temperature']) -def mqtt_on_command(): +def mqtt_on_command(client, userdata, message): """ Handle commands to topic hoymiles/{inverter_ser}/command - frame it and put onto command_queue + frame a payload and put onto command_queue + + Inverters must have mqtt.send_raw_enabled: true configured + + This can be used to inject debug payloads + The message must be in hexlified format + + Use of variables: + tttttttt gets expanded to a current int(time) + + Example injects exactly the same as we normally use to poll data: + mosquitto -h broker -t inverter_topic/command -m 800b00tttttttt0000000500000000 + + This allows for even faster hacking during runtime """ - raise NotImplementedError('Receiving mqtt commands is yet to be implemented') + try: + inverter_ser = next( + item[0] for item in mqtt_command_topic_subs if item[1] == message.topic) + except StopIteration: + print('Unexpedtedly received mqtt message for {message.topic}') + + if inverter_ser: + p_message = message.payload.decode('utf-8').lower() + + # Expand tttttttt to current time for use in hexlified payload + expand_time = ''.join(f'{b:02x}' for b in struct.pack('>L', int(time.time()))) + p_message = p_message.replace('tttttttt', expand_time) + + if (len(p_message) < 2048 \ + and len(p_message) % 2 == 0 \ + and re.match(r'^[a-f0-9]+$', p_message)): + payload = bytes.fromhex(p_message) + # commands must start with \x80 + if payload[0] == 0x80: + command_queue[str(inverter_ser)].append( + hoymiles.frame_payload(payload[1:])) if __name__ == '__main__': ahoy_config = dict(cfg.get('ahoy', {})) @@ -142,21 +178,32 @@ if __name__ == '__main__': mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None)) mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883)) mqtt_client.loop_start() + mqtt_client.on_message = mqtt_on_command if not radio.begin(): raise RuntimeError('Can\'t open radio') - #command_queue.append(hoymiles.compose_02_payload()) - #command_queue.append(hoymiles.compose_11_payload()) - inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])] - for inverter_ser in inverters: + for inverter in ahoy_config.get('inverters', []): + inverter_ser = inverter.get('serial') command_queue[str(inverter_ser)] = [] + # + # Enables and subscribe inverter to mqtt /command-Topic + # + if inverter.get('mqtt', {}).get('send_raw_enabled', False): + topic_item = ( + str(inverter_ser), + inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command' + ) + mqtt_client.subscribe(topic_item[1]) + mqtt_command_topic_subs.append(topic_item) + loop_interval = ahoy_config.get('interval', 1) try: while True: main_loop() + if loop_interval: time.sleep(time.time() % loop_interval) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index bcab9693..4e2cb586 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -17,4 +17,5 @@ ahoy: - name: 'balkon' serial: 114172220003 mqtt: + send_raw_enabled: false # allow inject debug data via mqtt topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}' diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index d1b0ae13..4c0a7c4a 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -104,8 +104,12 @@ class ResponseDecoder(ResponseDecoderFactory): model = self.inverter_model command = self.request_command - model_decoder = __import__(f'hoymiles.decoders') - device = getattr(model_decoder, f'{model}_Decode{command.upper()}') + model_decoders = __import__(f'hoymiles.decoders') + if hasattr(model_decoders, f'{model}_Decode{command.upper()}'): + device = getattr(model_decoders, f'{model}_Decode{command.upper()}') + else: + if HOYMILES_DEBUG_LOGGING: + device = getattr(model_decoders, f'DEBUG_DecodeAny') return device(self.response) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 3cef0946..0353d07d 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -57,33 +57,75 @@ class UnknownResponse: @property def hex_ascii(self): return ' '.join([f'{b:02x}' for b in self.response]) + @property def dump_longs(self): - n = len(self.response)/4 - vals = struct.unpack(f'>{int(n)}L', self.response) + res = self.response + n = len(res)/4 + + vals = None + if n % 4 == 0: + vals = struct.unpack(f'>{int(n)}L', res) + + return vals + + @property + def dump_longs_pad1(self): + res = self.response[1:] + n = len(res)/4 + + vals = None + if n % 4 == 0: + vals = struct.unpack(f'>{int(n)}L', res) + return vals @property def dump_shorts(self): n = len(self.response)/2 - vals = struct.unpack(f'>{int(n)}H', self.response) + + vals = None + if n % 2 == 0: + vals = struct.unpack(f'>{int(n)}H', self.response) return vals -class HM600_Decode02(UnknownResponse): - def __init__(self, response): - self.response = response + @property + def dump_shorts_pad1(self): + res = self.response[1:] + n = len(res)/2 -class HM600_Decode11(UnknownResponse): - def __init__(self, response): - self.response = response + vals = None + if n % 2 == 0: + vals = struct.unpack(f'>{int(n)}H', res) + return vals -class HM600_Decode12(UnknownResponse): +class DEBUG_DecodeAny(UnknownResponse): def __init__(self, response): self.response = response -class HM600_Decode0A(UnknownResponse): - def __init__(self, response): - self.response = response + longs = self.dump_longs + if not longs: + print(' type long : unable to decode (len or not mod 4)') + else: + print(' type long : ' + str(longs)) + + longs = self.dump_longs_pad1 + if not longs: + print(' type long pad1 : unable to decode (len or not mod 4)') + else: + print(' type long pad1 : ' + str(longs)) + + shorts = self.dump_shorts + if not shorts: + print(' type short : unable to decode (len or not mod 2)') + else: + print(' type short : ' + str(shorts)) + + shorts = self.dump_shorts_pad1 + if not shorts: + print(' type short pad1: unable to decode (len or not mod 2)') + else: + print(' type short pad1: ' + str(shorts)) class HM600_Decode0B(StatusResponse): def __init__(self, response):