mirror of
https://github.com/lumapu/ahoy.git
synced 2025-06-02 18:51:38 +02:00
First attempt at querying an inverter using Python. Not yet functional.
This commit is contained in:
parent
59aa635ebe
commit
48b7b50cab
1 changed files with 134 additions and 0 deletions
134
tools/rpi/ahoy.py
Normal file
134
tools/rpi/ahoy.py
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue