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.
 
 
 
 
 
 

239 lines
8.3 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
import json
from datetime import datetime
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
import paho.mqtt.client
from configparser import ConfigParser
#from hoymiles import ser_to_hm_addr, ser_to_esb_addr
import hoymiles
cfg = ConfigParser()
cfg.read('ahoy.conf')
mqtt_host = cfg.get('mqtt', 'host', fallback='192.168.1.1')
mqtt_port = cfg.getint('mqtt', 'port', fallback=1883)
mqtt_user = cfg.get('mqtt', 'user', fallback='')
mqtt_password = cfg.get('mqtt', 'password', fallback='')
radio = RF24(22, 0, 1000000)
mqtt_client = paho.mqtt.client.Client()
mqtt_client.username_pw_set(mqtt_user, mqtt_password)
mqtt_client.connect(mqtt_host, mqtt_port)
mqtt_client.loop_start()
# Master Address ('DTU')
dtu_ser = cfg.get('dtu', 'serial', fallback='99978563412') # identical to fc22's
# inverter serial numbers
inv_ser = cfg.get('inverter', 'serial', fallback='444473104619') # my inverter
# all inverters
#...
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
# time of last transmission - to calculcate response time
t_last_tx = 0
def main_loop():
"""
Keep receiving on channel 3. Every once in a while, transmit a request
to one of our inverters on channel 40.
"""
global t_last_tx
hoymiles.print_addr(inv_ser)
hoymiles.print_addr(dtu_ser)
ctr = 1
last_tx_message = ''
rx_channels = [3,6,9,11,23,40,61,75]
rx_channel_id = 0
rx_channel = rx_channels[rx_channel_id]
rx_channel_ack = None
rx_error = 0
tx_channels = [40]
tx_channel_id = 0
tx_channel = tx_channels[tx_channel_id]
radio.setChannel(rx_channel)
radio.setRetries(10, 2)
radio.setPALevel(RF24_PA_LOW)
#radio.setPALevel(RF24_PA_MAX)
radio.setDataRate(RF24_250KBPS)
radio.openReadingPipe(1,hoymiles.ser_to_esb_addr(dtu_ser))
radio.openWritingPipe(hoymiles.ser_to_esb_addr(inv_ser))
while True:
m_buf = []
# Channel selection: Sweep receive start channel
if not rx_channel_ack:
rx_channel_id = ctr % len(rx_channels)
rx_channel = rx_channels[rx_channel_id]
tx_channel_id = tx_channel_id + 1
if tx_channel_id >= len(tx_channels):
tx_channel_id = 0
tx_channel = tx_channels[tx_channel_id]
# Transmit: Compose data
com = hoymiles.InverterTransaction(
request_time = datetime.now(),
inverter_ser=inv_ser,
request = hoymiles.compose_0x80_msg(src_ser_no=dtu_ser, dst_ser_no=inv_ser, subtype=b'\x0b')
)
print(com)
# Transmit: Setup radio
radio.stopListening() # put radio in TX mode
radio.setChannel(tx_channel)
radio.setAutoAck(True)
radio.setRetries(3, 15)
radio.setCRCLength(RF24_CRC_16)
radio.enableDynamicPayloads()
# Transmit: Send payload
t_tx_start = time.monotonic_ns()
tx_status = radio.write(com.request)
t_last_tx = t_tx_end = time.monotonic_ns()
ctr = ctr + 1
# Receive: Setup radio
radio.setChannel(rx_channel)
radio.setAutoAck(False)
radio.setRetries(0, 0)
radio.enableDynamicPayloads()
radio.setCRCLength(RF24_CRC_16)
radio.startListening()
# Receive: Loop
t_end = time.monotonic_ns()+1e9
while time.monotonic_ns() < t_end:
has_payload, pipe_number = radio.available_pipe()
if has_payload:
# Data in nRF24 buffer, read it
rx_error = 0
rx_channel_ack = rx_channel
t_end = time.monotonic_ns()+2e8
size = radio.getDynamicPayloadSize()
payload = radio.read(size)
fragment = hoymiles.InverterPacketFragment(
payload=payload,
ch_rx=rx_channel, ch_tx=tx_channel,
time_rx=datetime.now(),
latency=time.monotonic_ns()-t_last_tx
)
print(fragment)
com.frame_append(fragment)
else:
# No data in nRF rx buffer, search and wait
# Channel lock in (not currently used)
rx_error = rx_error + 1
if rx_error > 0:
rx_channel_ack = None
# Channel hopping
if not rx_channel_ack:
rx_channel_id = rx_channel_id + 1
if rx_channel_id >= len(rx_channels):
rx_channel_id = 0
rx_channel = rx_channels[rx_channel_id]
radio.stopListening()
radio.setChannel(rx_channel)
radio.startListening()
time.sleep(0.005)
inv_ser_hm = hoymiles.ser_to_hm_addr(inv_ser)
try:
payload = com.get_payload()
except BufferError:
payload = None
#print("Garbage")
iv = None
if payload:
plen = len(payload)
dt = com.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
iv = hoymiles.hm600_0b_response_decode(payload)
print(f'{dt} Decoded: {plen}', end='')
print(f' string1=', end='')
print(f' {iv.dc_voltage_0}VDC', end='')
print(f' {iv.dc_current_0}A', end='')
print(f' {iv.dc_power_0}W', end='')
print(f' {iv.dc_energy_total_0}Wh', end='')
print(f' {iv.dc_energy_daily_0}Wh/day', end='')
print(f' string2=', end='')
print(f' {iv.dc_voltage_1}VDC', end='')
print(f' {iv.dc_current_1}A', end='')
print(f' {iv.dc_power_1}W', end='')
print(f' {iv.dc_energy_total_1}Wh', end='')
print(f' {iv.dc_energy_daily_1}Wh/day', end='')
print(f' phase1=', end='')
print(f' {iv.ac_voltage_0}VAC', end='')
print(f' {iv.ac_current_0}A', end='')
print(f' {iv.ac_power_0}W', end='')
print(f' inverter={com.inverter_ser}', end='')
print(f' {iv.ac_frequency}Hz', end='')
print(f' {iv.temperature}°C', end='')
print()
# output to MQTT
if iv:
src = com.inverter_ser
# AC Data
mqtt_client.publish(f'ahoy/{src}/frequency', iv.ac_frequency)
mqtt_client.publish(f'ahoy/{src}/emeter/0/power', iv.ac_power_0)
mqtt_client.publish(f'ahoy/{src}/emeter/0/voltage', iv.ac_voltage_0)
mqtt_client.publish(f'ahoy/{src}/emeter/0/current', iv.ac_current_0)
mqtt_client.publish(f'ahoy/{src}/emeter/0/total', iv.dc_energy_total_0)
# DC Data
mqtt_client.publish(f'ahoy/{src}/emeter-dc/0/total', iv.dc_energy_total_0/1000)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/0/power', iv.dc_power_0)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/0/voltage', iv.dc_voltage_0)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/0/current', iv.dc_current_0)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/1/total', iv.dc_energy_total_1/1000)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/1/power', iv.dc_power_1)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/1/voltage', iv.dc_voltage_1)
mqtt_client.publish(f'ahoy/{src}/emeter-dc/1/current', iv.dc_current_1)
# Global
mqtt_client.publish(f'ahoy/{src}/temperature', iv.temperature)
time.sleep(5)
# Flush console
print(flush=True, end='')
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()