Merge branch 'pypackage' of https://github.com/Sprinterfreak/ahoy into pypackage

This commit is contained in:
Thomas Basler 2022-05-07 10:33:14 +01:00
commit 809dec69c7
74 changed files with 7866 additions and 672 deletions

View file

@ -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
----

View file

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

View file

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

View file

@ -109,6 +109,7 @@ int main(int argc, char** argv)
dstaddrs.push_back(string("1Node"));
dstaddrs.push_back(string("2Node"));
dstaddrs.push_back(serno2shockburstaddrbytes(114174608145));
dstaddrs.push_back("\x45\x81\x60\x74\x01");
dstaddrs.push_back(serno2shockburstaddrbytes(114174608177));
// channels that we will scan
@ -127,7 +128,7 @@ int main(int argc, char** argv)
cout << " - ";
}
cout << " " << flush;
delay(10);
//delay(10);
}
cout << endl;
}

View file

@ -11,6 +11,7 @@
#include <sstream>
#include <time.h> // CLOCK_MONOTONIC_RAW, timespec, clock_gettime()
#include <RF24/RF24.h> // RF24, RF24_PA_LOW, delay()
#include <unistd.h> // usleep()
using namespace std;
@ -45,7 +46,7 @@ void receiveForever(int ch, string myaddr)
while (true)
{
uint8_t pipe;
delay(500);
usleep(500000);
if (radio.failureDetected) {
cout << "!f! " << flush;
}

View file

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

View file

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

View file

@ -1,2 +1,3 @@
paho-mqtt
crcmod
paho-mqtt>=1.5
crcmod>=1.7
PyYAML>=5.0

View file

@ -1,84 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import codecs
import re
import time
from datetime import datetime
import hoymiles
logdata = """
2022-05-01 12:29:02.139673 Transmit 368223: channel=40 len=27 ack=False | 15 72 22 01 43 78 56 34 12 80 0b 00 62 6e 60 ee 00 00 00 05 00 00 00 00 7e 58 25
2022-05-01 12:29:02.184796 Received 27 bytes on channel 3 after tx 6912328ns: 95 72 22 01 43 72 22 01 43 01 00 01 01 4e 00 9d 02 0a 01 50 00 9d 02 10 00 00 91
2022-05-01 12:29:02.184796 Decoder src=72220143, dst=72220143, cmd=1, u1=33.4V, i1=1.57A, p1=52.2W, u2=33.6V, i2=1.57A, p2=52.8W, uk1=1, uk2=0
2022-05-01 12:29:02.226251 Received 27 bytes on channel 75 after tx 48355619ns: 95 72 22 01 43 72 22 01 43 02 88 1f 00 00 7f 08 00 94 00 97 08 e2 13 89 03 eb ec
2022-05-01 12:29:02.226251 Decoder src=72220143, dst=72220143, cmd=2, ac_u1=227.4V, ac_f=50.01Hz, ac_p1=100.3W, uk1=34847, uk2=0, uk3=32520, uk4=148, uk5=151
2022-05-01 12:29:02.273766 Received 23 bytes on channel 75 after tx 95876606ns: 95 72 22 01 43 72 22 01 43 83 00 01 00 2c 03 e8 00 d8 00 06 0c 35 37
2022-05-01 12:29:02.273766 Decoder src=72220143, dst=72220143, cmd=131, ac_i1=0.44A, t=21.60C, uk1=1, uk3=1000, uk5=6, uk6=3125
"""
def payload_from_log(line):
values = re.match(r'(?P<datetime>\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+) Received.*: (?P<data>[0-9a-z ]+)$', line)
if values:
payload=values.group('data')
return hoymiles.InverterPacketFragment(
time_rx=datetime.strptime(values.group('datetime'), '%Y-%m-%d %H:%M:%S.%f'),
payload=bytes.fromhex(payload)
)
with open('example-logs/example.log', 'r') as fh:
for line in fh:
kind = re.match(r'\d{4}-\d{2}-\d{2} \d\d:\d\d:\d\d.\d+ (?P<type>Transmit|Received)', line)
if kind:
if kind.group('type') == 'Transmit':
u, data = line.split('|')
rx_buffer = hoymiles.InverterTransaction(
request=bytes.fromhex(data))
elif kind.group('type') == 'Received':
try:
payload = payload_from_log(line)
print(payload)
except BufferError as err:
print(f'Debug: {err}')
payload = None
pass
if payload:
rx_buffer.frame_append(payload)
try:
#packet = rx_buffer.get_payload(72220143)
packet = rx_buffer.get_payload()
except BufferError as err:
print(f'Debug: {err}')
packet = None
pass
if packet:
plen = len(packet)
dt = rx_buffer.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
iv = hoymiles.hm600_0b_response_decode(packet)
print(f'{dt} Decoded: {plen}', end='')
print(f' string1=', end='')
print(f' {iv.dc_voltage_0}VDC', end='')
print(f' {iv.dc_current_0}A', end='')
print(f' {iv.dc_power_0}W', end='')
print(f' {iv.dc_energy_total_0}Wh', end='')
print(f' {iv.dc_energy_daily_0}Wh/day', end='')
print(f' string2=', end='')
print(f' {iv.dc_voltage_1}VDC', end='')
print(f' {iv.dc_current_1}A', end='')
print(f' {iv.dc_power_1}W', end='')
print(f' {iv.dc_energy_total_1}Wh', end='')
print(f' {iv.dc_energy_daily_1}Wh/day', end='')
print(f' phase1=', end='')
print(f' {iv.ac_voltage_0}VAC', end='')
print(f' {iv.ac_current_0}A', end='')
print(f' {iv.ac_power_0}W', end='')
print(f' inverter=', end='')
print(f' {iv.ac_frequency}Hz', end='')
print(f' {iv.temperature}°C', end='')
print()
print('', end='', flush=True)