mirror of https://github.com/lumapu/ahoy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
3.7 KiB
134 lines
3.7 KiB
"""
|
|
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()
|
|
|