mirror of https://github.com/lumapu/ahoy.git
Martin Grill
3 years ago
1 changed files with 134 additions and 0 deletions
@ -0,0 +1,134 @@ |
|||||
|
""" |
||||
|
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 = 99912345678 |
||||
|
|
||||
|
# inverter serial numbers |
||||
|
inv_ser = 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 = 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 |
||||
|
|
||||
|
# 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)) |
||||
|
payload = compose_0x80_msg(src_ser_no=dtu_ser, dst_ser_no=inv_ser) |
||||
|
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