mirror of
https://github.com/lumapu/ahoy.git
synced 2025-06-14 00:21:44 +02:00
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
This commit is contained in:
parent
42bd240083
commit
0ee867993c
5 changed files with 146 additions and 22 deletions
|
@ -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
|
||||
----
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = None
|
||||
if n % 2 == 0:
|
||||
vals = struct.unpack(f'>{int(n)}H', self.response)
|
||||
return vals
|
||||
|
||||
class HM600_Decode02(UnknownResponse):
|
||||
@property
|
||||
def dump_shorts_pad1(self):
|
||||
res = self.response[1:]
|
||||
n = len(res)/2
|
||||
|
||||
vals = None
|
||||
if n % 2 == 0:
|
||||
vals = struct.unpack(f'>{int(n)}H', res)
|
||||
return vals
|
||||
|
||||
class DEBUG_DecodeAny(UnknownResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
|
||||
class HM600_Decode11(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))
|
||||
|
||||
class HM600_Decode12(UnknownResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
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))
|
||||
|
||||
class HM600_Decode0A(UnknownResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
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…
Add table
Add a link
Reference in a new issue