mirror of https://github.com/lumapu/ahoy.git
Martin Grill
3 years ago
2 changed files with 142 additions and 0 deletions
@ -0,0 +1,137 @@ |
|||
""" |
|||
First attempt at providing basic 'master' ('DTU') functionality |
|||
for Hoymiles micro inverters. |
|||
Based in particular on demostrated first contact by 'of22'. |
|||
""" |
|||
import sys |
|||
import argparse |
|||
import time |
|||
import struct |
|||
import crcmod |
|||
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS |
|||
|
|||
radio = RF24(22, 0, 1000000) |
|||
|
|||
# Master Address ('DTU') |
|||
dtu_ser = 99978563412 # identical to fc22's |
|||
|
|||
# inverter serial numbers |
|||
inv_ser = 444473104619 # identical to fc22's #99972220200 |
|||
|
|||
# all inverters |
|||
#... |
|||
|
|||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') |
|||
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) |
|||
|
|||
def ser_to_hm_addr(s): |
|||
""" |
|||
Calculate the 4 bytes that the HM devices use in their internal messages to |
|||
address each other. |
|||
""" |
|||
bcd = int(str(s)[-8:], base=16) |
|||
return struct.pack('>L', bcd) |
|||
|
|||
def ser_to_esb_addr(s): |
|||
""" |
|||
Convert a Hoymiles inverter/DTU serial number into its |
|||
corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes). |
|||
* |
|||
The inverters use a BCD representation of the last 8 |
|||
digits of their serial number, in reverse byte order, |
|||
followed by \x01. |
|||
""" |
|||
return ser_to_hm_addr(s)[::-1] + b'\x01' |
|||
|
|||
def compose_0x80_msg(dst_ser_no=72220200, src_ser_no=72220200, ts=None): |
|||
""" |
|||
Create a valid 0x80 request with the given parameters, and containing the current system time. |
|||
""" |
|||
|
|||
if not ts: |
|||
ts = 0x623C8ECF # identical to fc22's for testing # doc: 1644758171 |
|||
|
|||
# "framing" |
|||
p = b'' |
|||
p = p + b'\x15' |
|||
p = p + ser_to_hm_addr(dst_ser_no) |
|||
p = p + ser_to_hm_addr(src_ser_no) |
|||
p = p + b'\x80' |
|||
|
|||
# encapsulated payload |
|||
pp = b'\x0b\x00' |
|||
pp = pp + struct.pack('>L', ts) # big-endian: msb at low address |
|||
#pp = pp + b'\x00' * 8 # of22 adds a \x05 at position 19 |
|||
|
|||
pp = pp + b'\x00\x00\x00\x05\x00\x00\x00\x00' |
|||
|
|||
# CRC_M |
|||
crc_m = f_crc_m(pp) |
|||
|
|||
p = p + pp |
|||
p = p + struct.pack('>H', crc_m) |
|||
|
|||
crc8 = f_crc8(p) |
|||
p = p + struct.pack('B', crc8) |
|||
return p |
|||
|
|||
|
|||
def main_loop(): |
|||
""" |
|||
Keep receiving on channel 3. Every once in a while, transmit a request |
|||
to one of our inverters on channel 40. |
|||
""" |
|||
ctr = 1 |
|||
while True: |
|||
radio.setChannel(3) |
|||
radio.enableDynamicPayloads() |
|||
radio.setAutoAck(False) |
|||
radio.setPALevel(RF24_PA_MAX) |
|||
radio.setDataRate(RF24_250KBPS) |
|||
radio.openWritingPipe(ser_to_esb_addr(inv_ser)) |
|||
radio.flush_rx() |
|||
radio.flush_tx() |
|||
radio.openReadingPipe(0,ser_to_esb_addr(dtu_ser)) |
|||
radio.openReadingPipe(1,ser_to_esb_addr(inv_ser)) |
|||
radio.startListening() |
|||
|
|||
if ctr==1: |
|||
radio.printPrettyDetails() |
|||
|
|||
t_end = time.monotonic_ns()+1e9 |
|||
while time.monotonic_ns() < t_end: |
|||
has_payload, pipe_number = radio.available_pipe() |
|||
if has_payload: |
|||
size = radio.payloadSize |
|||
payload = radio.read(size) |
|||
print(f"Received {size} bytes on pipe {pipe_number}: {payload}") |
|||
|
|||
radio.stopListening() # put radio in TX mode |
|||
radio.setChannel(41) |
|||
radio.openWritingPipe(ser_to_esb_addr(inv_ser)) |
|||
ts = int(time.time()) |
|||
payload = compose_0x80_msg(src_ser_no=dtu_ser, dst_ser_no=inv_ser, ts=ts) |
|||
print(f"{ctr:5d}: len={len(payload)} | " + " ".join([f"{b:02x}" for b in payload])) |
|||
radio.write(payload) # will always yield 'True' b/c auto-ack is disabled |
|||
ctr = ctr + 1 |
|||
|
|||
|
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
|
|||
if not radio.begin(): |
|||
raise RuntimeError("radio hardware is not responding") |
|||
|
|||
radio.setPALevel(RF24_PA_LOW) # RF24_PA_MAX is default |
|||
|
|||
# radio.printDetails(); # (smaller) function that prints raw register values |
|||
radio.printPrettyDetails(); # (larger) function that prints human readable data |
|||
|
|||
try: |
|||
main_loop() |
|||
|
|||
except KeyboardInterrupt: |
|||
print(" Keyboard Interrupt detected. Exiting...") |
|||
radio.powerDown() |
|||
sys.exit() |
Loading…
Reference in new issue