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.
239 lines
8.3 KiB
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()
|
|
|