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

61
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)

1
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}'

8
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)

68
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):

Loading…
Cancel
Save