mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-28 16:26:12 +02:00
Merge branch 'pypackage' of https://github.com/Sprinterfreak/ahoy into pypackage
This commit is contained in:
commit
809dec69c7
74 changed files with 7866 additions and 672 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}'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = 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):
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
paho-mqtt
|
||||
crcmod
|
||||
paho-mqtt>=1.5
|
||||
crcmod>=1.7
|
||||
PyYAML>=5.0
|
||||
|
|
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue