Browse Source

MQTT payload injection and default unknown decoder

Adds the ability to directly inject payloads to be sent to the inverter.
Fixes application crash at missing decoder by adding default decoding.

All unknown payloads are now printed as long- and short-lists for faster
protocol analysis
pull/25/head
Jan-Jonas Sämann 3 years ago
parent
commit
0ee867993c
  1. 30
      tools/rpi/README.md
  2. 61
      tools/rpi/ahoy.py
  3. 1
      tools/rpi/ahoy.yml.example
  4. 8
      tools/rpi/hoymiles/__init__.py
  5. 68
      tools/rpi/hoymiles/decoders/__init__.py

30
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 Analysing the Logs
------------------ ------------------
@ -68,6 +96,8 @@ Configuration
Local settings are read from ahoy.yml Local settings are read from ahoy.yml
An example is provided as ahoy.yml.example An example is provided as ahoy.yml.example
Todo Todo
---- ----

61
tools/rpi/ahoy.py

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys import sys
import struct
import re
import time import time
from datetime import datetime from datetime import datetime
import argparse import argparse
@ -28,6 +30,7 @@ hmradio = hoymiles.HoymilesNRF(device=radio)
mqtt_client = None mqtt_client = None
command_queue = {} command_queue = {}
mqtt_command_topic_subs = []
hoymiles.HOYMILES_TRANSACTION_LOGGING=True hoymiles.HOYMILES_TRANSACTION_LOGGING=True
hoymiles.HOYMILES_DEBUG_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}/frequency', data['frequency'])
broker.publish(f'{topic}/temperature', data['temperature']) broker.publish(f'{topic}/temperature', data['temperature'])
def mqtt_on_command(): def mqtt_on_command(client, userdata, message):
""" """
Handle commands to topic Handle commands to topic
hoymiles/{inverter_ser}/command 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__': if __name__ == '__main__':
ahoy_config = dict(cfg.get('ahoy', {})) 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.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.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883))
mqtt_client.loop_start() mqtt_client.loop_start()
mqtt_client.on_message = mqtt_on_command
if not radio.begin(): if not radio.begin():
raise RuntimeError('Can\'t open radio') 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', [])] 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)] = [] 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) loop_interval = ahoy_config.get('interval', 1)
try: try:
while True: while True:
main_loop() main_loop()
if loop_interval: if loop_interval:
time.sleep(time.time() % loop_interval) time.sleep(time.time() % loop_interval)

1
tools/rpi/ahoy.yml.example

@ -17,4 +17,5 @@ ahoy:
- name: 'balkon' - name: 'balkon'
serial: 114172220003 serial: 114172220003
mqtt: mqtt:
send_raw_enabled: false # allow inject debug data via mqtt
topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}' topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}'

8
tools/rpi/hoymiles/__init__.py

@ -104,8 +104,12 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model model = self.inverter_model
command = self.request_command command = self.request_command
model_decoder = __import__(f'hoymiles.decoders') model_decoders = __import__(f'hoymiles.decoders')
device = getattr(model_decoder, f'{model}_Decode{command.upper()}') 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) return device(self.response)

68
tools/rpi/hoymiles/decoders/__init__.py

@ -57,33 +57,75 @@ class UnknownResponse:
@property @property
def hex_ascii(self): def hex_ascii(self):
return ' '.join([f'{b:02x}' for b in self.response]) return ' '.join([f'{b:02x}' for b in self.response])
@property @property
def dump_longs(self): def dump_longs(self):
n = len(self.response)/4 res = self.response
vals = struct.unpack(f'>{int(n)}L', 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 return vals
@property @property
def dump_shorts(self): def dump_shorts(self):
n = len(self.response)/2 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 return vals
class HM600_Decode02(UnknownResponse): @property
def __init__(self, response): def dump_shorts_pad1(self):
self.response = response res = self.response[1:]
n = len(res)/2
class HM600_Decode11(UnknownResponse): vals = None
def __init__(self, response): if n % 2 == 0:
self.response = response vals = struct.unpack(f'>{int(n)}H', res)
return vals
class HM600_Decode12(UnknownResponse): class DEBUG_DecodeAny(UnknownResponse):
def __init__(self, response): def __init__(self, response):
self.response = response self.response = response
class HM600_Decode0A(UnknownResponse): longs = self.dump_longs
def __init__(self, response): if not longs:
self.response = response 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): class HM600_Decode0B(StatusResponse):
def __init__(self, response): def __init__(self, response):

Loading…
Cancel
Save