mirror of https://github.com/lumapu/ahoy.git
				
				
			
				 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