mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-08 14:45:55 +02:00
Ahoy Python Rewrite
Is was clear, the cmd approach does not decode payloads reliably. The modular form allows for easy tinkering. This implements * hoymiles protocol * transport-layer enables for retransmit of missed fragments * full payload decode * device specific decoders * transaction tracking enables decoding of different datasets * multi-inverter support * configuration format change to YAML (required for multi-inverter) First PoC, lots of things have to be relocated, rewritten and exteded. Currently only supports Hoymiles HM-600, more device decodes have to be added by users who have the hardware.
This commit is contained in:
parent
d7f9f6d3be
commit
9d75ca0c34
7 changed files with 594 additions and 341 deletions
|
@ -40,7 +40,7 @@ contact the inverter every second on channel 40, and listen for replies.
|
||||||
|
|
||||||
Whenever it sees a reply, it will decoded and logged to the given log file.
|
Whenever it sees a reply, it will decoded and logged to the given log file.
|
||||||
|
|
||||||
$ sudo python3 ahoy.py | tee -a log2.log
|
$ sudo python3 ahoy.py --config /home/dtu/ahoy.yml | tee -a log2.log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,8 +65,8 @@ A brief example log is supplied in the `example-logs` folder.
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Local settings are read from ~/ahoy.conf
|
Local settings are read from ahoy.yml
|
||||||
An example is provided as ahoy.conf.example
|
An example is provided as ahoy.yml.example
|
||||||
|
|
||||||
Todo
|
Todo
|
||||||
----
|
----
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
[mqtt]
|
|
||||||
host = 192.168.84.2
|
|
||||||
port = 1883
|
|
||||||
user = bla
|
|
||||||
password = blub
|
|
||||||
|
|
||||||
[dtu]
|
|
||||||
serial = 99978563412
|
|
||||||
|
|
||||||
[inverter]
|
|
||||||
serial = 444473104619
|
|
|
@ -1,239 +1,162 @@
|
||||||
"""
|
#!/usr/bin/env python3
|
||||||
First attempt at providing basic 'master' ('DTU') functionality
|
# -*- coding: utf-8 -*-
|
||||||
for Hoymiles micro inverters.
|
|
||||||
Based in particular on demostrated first contact by 'of22'.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
|
||||||
import time
|
import time
|
||||||
import struct
|
|
||||||
import crcmod
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import argparse
|
||||||
|
import hoymiles
|
||||||
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
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
|
import paho.mqtt.client
|
||||||
from configparser import ConfigParser
|
import yaml
|
||||||
#from hoymiles import ser_to_hm_addr, ser_to_esb_addr
|
from yaml.loader import SafeLoader
|
||||||
import hoymiles
|
|
||||||
|
|
||||||
cfg = ConfigParser()
|
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway')
|
||||||
cfg.read('ahoy.conf')
|
parser.add_argument("-c", "--config-file", nargs="?",
|
||||||
mqtt_host = cfg.get('mqtt', 'host', fallback='192.168.1.1')
|
help="configuration file")
|
||||||
mqtt_port = cfg.getint('mqtt', 'port', fallback=1883)
|
global_config = parser.parse_args()
|
||||||
mqtt_user = cfg.get('mqtt', 'user', fallback='')
|
|
||||||
mqtt_password = cfg.get('mqtt', 'password', fallback='')
|
if global_config.config_file:
|
||||||
|
with open(global_config.config_file) as yf:
|
||||||
|
cfg = yaml.load(yf, Loader=SafeLoader)
|
||||||
|
else:
|
||||||
|
with open(global_config.config_file) as yf:
|
||||||
|
cfg = yaml.load('ahoy.yml', Loader=SafeLoader)
|
||||||
|
|
||||||
radio = RF24(22, 0, 1000000)
|
radio = RF24(22, 0, 1000000)
|
||||||
mqtt_client = paho.mqtt.client.Client()
|
hmradio = hoymiles.HoymilesNRF(device=radio)
|
||||||
mqtt_client.username_pw_set(mqtt_user, mqtt_password)
|
mqtt_client = None
|
||||||
mqtt_client.connect(mqtt_host, mqtt_port)
|
|
||||||
mqtt_client.loop_start()
|
|
||||||
|
|
||||||
# Master Address ('DTU')
|
command_queue = {}
|
||||||
dtu_ser = cfg.get('dtu', 'serial', fallback='99978563412') # identical to fc22's
|
|
||||||
|
|
||||||
# inverter serial numbers
|
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
|
||||||
inv_ser = cfg.get('inverter', 'serial', fallback='444473104619') # my inverter
|
hoymiles.HOYMILES_DEBUG_LOGGING=True
|
||||||
|
|
||||||
# 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():
|
def main_loop():
|
||||||
"""
|
inverters = [
|
||||||
Keep receiving on channel 3. Every once in a while, transmit a request
|
inverter for inverter in ahoy_config.get('inverters', [])
|
||||||
to one of our inverters on channel 40.
|
if not inverter.get('disabled', False)]
|
||||||
"""
|
|
||||||
|
|
||||||
global t_last_tx
|
for inverter in inverters:
|
||||||
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
|
print(f'Poll inverter {inverter["serial"]}')
|
||||||
|
poll_inverter(inverter)
|
||||||
|
|
||||||
hoymiles.print_addr(inv_ser)
|
def poll_inverter(inverter):
|
||||||
hoymiles.print_addr(dtu_ser)
|
inverter_ser = inverter.get('serial')
|
||||||
|
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
|
||||||
|
|
||||||
ctr = 1
|
if len(command_queue[str(inverter_ser)]) > 0:
|
||||||
last_tx_message = ''
|
payload = command_queue[str(inverter_ser)].pop(0)
|
||||||
|
else:
|
||||||
|
payload = hoymiles.compose_set_time_payload()
|
||||||
|
|
||||||
rx_channels = [3,6,9,11,23,40,61,75]
|
payload_ttl = 4
|
||||||
rx_channel_id = 0
|
while payload_ttl > 0:
|
||||||
rx_channel = rx_channels[rx_channel_id]
|
payload_ttl = payload_ttl - 1
|
||||||
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(
|
com = hoymiles.InverterTransaction(
|
||||||
request_time = datetime.now(),
|
radio=hmradio,
|
||||||
inverter_ser=inv_ser,
|
dtu_ser=dtu_ser,
|
||||||
request = hoymiles.compose_0x80_msg(src_ser_no=dtu_ser, dst_ser_no=inv_ser, subtype=b'\x0b')
|
inverter_ser=inverter_ser,
|
||||||
)
|
request=next(hoymiles.compose_esb_packet(
|
||||||
print(com)
|
payload,
|
||||||
|
seq=b'\x80',
|
||||||
|
src=dtu_ser,
|
||||||
|
dst=inverter_ser
|
||||||
|
)))
|
||||||
|
response = None
|
||||||
|
while com.rxtx():
|
||||||
|
try:
|
||||||
|
response = com.get_payload()
|
||||||
|
payload_ttl = 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error while retrieving data: {e}')
|
||||||
|
pass
|
||||||
|
|
||||||
# Transmit: Setup radio
|
if response:
|
||||||
radio.stopListening() # put radio in TX mode
|
dt = datetime.now()
|
||||||
radio.setChannel(tx_channel)
|
print(f'{dt} Payload: ' + hoymiles.hexify_payload(response))
|
||||||
radio.setAutoAck(True)
|
decoder = hoymiles.ResponseDecoder(response,
|
||||||
radio.setRetries(3, 15)
|
request=com.request,
|
||||||
radio.setCRCLength(RF24_CRC_16)
|
inverter_ser=inverter_ser
|
||||||
radio.enableDynamicPayloads()
|
)
|
||||||
|
result = decoder.decode()
|
||||||
|
if isinstance(result, hoymiles.decoders.StatusResponse):
|
||||||
|
data = result.__dict__()
|
||||||
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
|
print(f'{dt} Decoded: {data["temperature"]}', end='')
|
||||||
|
phase_id = 0
|
||||||
|
for phase in data['phases']:
|
||||||
|
print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
|
||||||
|
phase_id = phase_id + 1
|
||||||
|
string_id = 0
|
||||||
|
for string in data['strings']:
|
||||||
|
print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='')
|
||||||
|
string_id = string_id + 1
|
||||||
|
print()
|
||||||
|
|
||||||
# Transmit: Send payload
|
if mqtt_client:
|
||||||
t_tx_start = time.monotonic_ns()
|
mqtt_send_status(mqtt_client, inverter_ser, data)
|
||||||
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
|
def mqtt_send_status(broker, interter_ser, data):
|
||||||
if iv:
|
topic = f'ahoy/{inverter_ser}'
|
||||||
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)
|
# AC Data
|
||||||
|
phase_id = 0
|
||||||
|
for phase in data['phases']:
|
||||||
|
broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
|
||||||
|
broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
|
||||||
|
broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
|
||||||
|
phase_id = phase_id + 1
|
||||||
|
|
||||||
# Flush console
|
# DC Data
|
||||||
print(flush=True, end='')
|
string_id = 0
|
||||||
|
for string in data['strings']:
|
||||||
|
broker.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
|
||||||
|
broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
|
||||||
|
broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
|
||||||
|
broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
|
||||||
|
string_id = string_id + 1
|
||||||
|
# Global
|
||||||
|
broker.publish(f'{topic}/frequency', data['frequency'])
|
||||||
|
broker.publish(f'{topic}/temperature', data['temperature'])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def mqtt_on_command():
|
||||||
|
"""
|
||||||
|
Handle commands to topic
|
||||||
|
ahoy/{inverter_ser}/command
|
||||||
|
frame it and put onto command_queue
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('Receiving mqtt commands is yet to be implemented')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ahoy_config = dict(cfg.get('ahoy', {}))
|
||||||
|
|
||||||
|
mqtt_config = ahoy_config.get('mqtt', [])
|
||||||
|
if mqtt_config.get('disabled', True):
|
||||||
|
mqtt_client = paho.mqtt.client.Client()
|
||||||
|
mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None))
|
||||||
|
mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883))
|
||||||
|
mqtt_client.loop_start()
|
||||||
|
|
||||||
if not radio.begin():
|
if not radio.begin():
|
||||||
raise RuntimeError("radio hardware is not responding")
|
raise RuntimeError('Can\'t open radio')
|
||||||
|
|
||||||
radio.setPALevel(RF24_PA_LOW) # RF24_PA_MAX is default
|
#command_queue.append(hoymiles.compose_02_payload())
|
||||||
|
#command_queue.append(hoymiles.compose_11_payload())
|
||||||
|
|
||||||
# radio.printDetails(); # (smaller) function that prints raw register values
|
inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])]
|
||||||
# radio.printPrettyDetails(); # (larger) function that prints human readable data
|
for inverter_ser in inverters:
|
||||||
|
command_queue[str(inverter_ser)] = []
|
||||||
|
|
||||||
|
loop_interval = ahoy_config.get('interval', 1)
|
||||||
try:
|
try:
|
||||||
main_loop()
|
while True:
|
||||||
|
main_loop()
|
||||||
|
if loop_interval:
|
||||||
|
time.sleep(time.time() % loop_interval)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(" Keyboard Interrupt detected. Exiting...")
|
|
||||||
radio.powerDown()
|
radio.powerDown()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
20
tools/rpi/ahoy.yml.example
Normal file
20
tools/rpi/ahoy.yml.example
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
ahoy:
|
||||||
|
interval: 0
|
||||||
|
sunset: true
|
||||||
|
mqtt:
|
||||||
|
disabled: false
|
||||||
|
host: example-broker.local
|
||||||
|
port: 1883
|
||||||
|
user: 'username'
|
||||||
|
password: 'password'
|
||||||
|
|
||||||
|
dtu:
|
||||||
|
serial: 99978563000
|
||||||
|
|
||||||
|
inverters:
|
||||||
|
- name: 'balkon'
|
||||||
|
serial: 114172220003
|
||||||
|
mqtt:
|
||||||
|
topic: 'ahoy/114172220143' # defaults to 'ahoy/{serial}'
|
|
@ -2,13 +2,18 @@ import struct
|
||||||
import crcmod
|
import crcmod
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from datetime import datetime
|
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
|
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
||||||
|
from .decoders import *
|
||||||
|
|
||||||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
||||||
|
|
||||||
|
|
||||||
|
HOYMILES_TRANSACTION_LOGGING=True
|
||||||
|
HOYMILES_DEBUG_LOGGING=True
|
||||||
|
|
||||||
def ser_to_hm_addr(s):
|
def ser_to_hm_addr(s):
|
||||||
"""
|
"""
|
||||||
Calculate the 4 bytes that the HM devices use in their internal messages to
|
Calculate the 4 bytes that the HM devices use in their internal messages to
|
||||||
|
@ -17,7 +22,6 @@ def ser_to_hm_addr(s):
|
||||||
bcd = int(str(s)[-8:], base=16)
|
bcd = int(str(s)[-8:], base=16)
|
||||||
return struct.pack('>L', bcd)
|
return struct.pack('>L', bcd)
|
||||||
|
|
||||||
|
|
||||||
def ser_to_esb_addr(s):
|
def ser_to_esb_addr(s):
|
||||||
"""
|
"""
|
||||||
Convert a Hoymiles inverter/DTU serial number into its
|
Convert a Hoymiles inverter/DTU serial number into its
|
||||||
|
@ -33,42 +37,6 @@ def ser_to_esb_addr(s):
|
||||||
air_order = ser_to_hm_addr(s)[::-1] + b'\x01'
|
air_order = ser_to_hm_addr(s)[::-1] + b'\x01'
|
||||||
return air_order[::-1]
|
return air_order[::-1]
|
||||||
|
|
||||||
|
|
||||||
def compose_0x80_msg(dst_ser_no=72220200, src_ser_no=72220200, ts=None, subtype=b'\x0b'):
|
|
||||||
"""
|
|
||||||
Create a valid 0x80 request with the given parameters, and containing the
|
|
||||||
current system time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not ts:
|
|
||||||
ts = int(time.time())
|
|
||||||
|
|
||||||
# "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 = subtype + b'\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
|
|
||||||
|
|
||||||
pp = pp + b'\x00\x00\x00\x05\x00\x00\x00\x00'
|
|
||||||
|
|
||||||
# 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 print_addr(a):
|
def print_addr(a):
|
||||||
print(f"ser# {a} ", end='')
|
print(f"ser# {a} ", end='')
|
||||||
print(f" -> HM {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='')
|
print(f" -> HM {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='')
|
||||||
|
@ -77,71 +45,68 @@ def print_addr(a):
|
||||||
# time of last transmission - to calculcate response time
|
# time of last transmission - to calculcate response time
|
||||||
t_last_tx = 0
|
t_last_tx = 0
|
||||||
|
|
||||||
class hm600_02_response_decode:
|
class ResponseDecoderFactory:
|
||||||
""" TBD """
|
model = None
|
||||||
def __init__(self, response):
|
request = None
|
||||||
|
response = None
|
||||||
|
|
||||||
|
def __init__(self, response, **params):
|
||||||
self.response = response
|
self.response = response
|
||||||
|
|
||||||
class hm600_11_response_decode:
|
if 'request' in params:
|
||||||
""" TBD """
|
self.request = params['request']
|
||||||
def __init__(self, response):
|
elif hasattr(response, 'request'):
|
||||||
self.response = response
|
self.request = response.request
|
||||||
|
|
||||||
class hm600_0b_response_decode:
|
if 'inverter_ser' in params:
|
||||||
def __init__(self, response):
|
self.inverter_ser = params['inverter_ser']
|
||||||
self.response = response
|
self.model = self.inverter_model
|
||||||
|
|
||||||
def unpack(self, fmt, base):
|
def unpack(self, fmt, base):
|
||||||
size = struct.calcsize(fmt)
|
size = struct.calcsize(fmt)
|
||||||
return struct.unpack(fmt, self.response[base:base+size])
|
return struct.unpack(fmt, self.response[base:base+size])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_0(self):
|
def inverter_model(self):
|
||||||
return self.unpack('>H', 2)[0]/10
|
if not self.inverter_ser:
|
||||||
@property
|
raise ValueError('Inverter serial while decoding response')
|
||||||
def dc_current_0(self):
|
|
||||||
return self.unpack('>H', 4)[0]/100
|
ser_db = [
|
||||||
@property
|
('HM300', r'^112171......'),
|
||||||
def dc_power_0(self):
|
('HM350', r'^112172......'),
|
||||||
return self.unpack('>H', 6)[0]/10
|
('HM600', r'^114172......'),
|
||||||
@property
|
('HM700', r'^114174......'),
|
||||||
def dc_energy_total_0(self):
|
('HM1200', r'^116170......')
|
||||||
return self.unpack('>L', 14)[0]
|
]
|
||||||
@property
|
ser_str = str(self.inverter_ser)
|
||||||
def dc_energy_daily_0(self):
|
|
||||||
return self.unpack('>H', 22)[0]
|
model = None
|
||||||
|
for m, r in ser_db:
|
||||||
|
if re.match(r, ser_str):
|
||||||
|
model = m
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(model):
|
||||||
|
return model
|
||||||
|
raise NotImplementedError('Model lookup failed for serial {ser_str}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_1(self):
|
def request_command(self):
|
||||||
return self.unpack('>H', 8)[0]/10
|
r_code = self.request[10]
|
||||||
@property
|
return f'{r_code:02x}'
|
||||||
def dc_current_1(self):
|
|
||||||
return self.unpack('>H', 10)[0]/100
|
|
||||||
@property
|
|
||||||
def dc_power_1(self):
|
|
||||||
return self.unpack('>H', 12)[0]/10
|
|
||||||
@property
|
|
||||||
def dc_energy_total_1(self):
|
|
||||||
return self.unpack('>L', 18)[0]
|
|
||||||
@property
|
|
||||||
def dc_energy_daily_1(self):
|
|
||||||
return self.unpack('>H', 24)[0]
|
|
||||||
|
|
||||||
@property
|
class ResponseDecoder(ResponseDecoderFactory):
|
||||||
def ac_voltage_0(self):
|
def __init__(self, response, **params):
|
||||||
return self.unpack('>H', 26)[0]/10
|
ResponseDecoderFactory.__init__(self, response, **params)
|
||||||
@property
|
|
||||||
def ac_current_0(self):
|
def decode(self):
|
||||||
return self.unpack('>H', 34)[0]/10
|
model = self.inverter_model
|
||||||
@property
|
command = self.request_command
|
||||||
def ac_power_0(self):
|
|
||||||
return self.unpack('>H', 30)[0]/10
|
model_decoder = __import__(f'hoymiles.decoders')
|
||||||
@property
|
device = getattr(model_decoder, f'{model}_Decode{command.upper()}')
|
||||||
def ac_frequency(self):
|
|
||||||
return self.unpack('>H', 28)[0]/100
|
return device(self.response)
|
||||||
@property
|
|
||||||
def temperature(self):
|
|
||||||
return self.unpack('>H', 38)[0]/10
|
|
||||||
|
|
||||||
class InverterPacketFragment:
|
class InverterPacketFragment:
|
||||||
def __init__(self, time_rx=None, payload=None, ch_rx=None, ch_tx=None, **params):
|
def __init__(self, time_rx=None, payload=None, ch_rx=None, ch_tx=None, **params):
|
||||||
|
@ -204,42 +169,258 @@ class InverterPacketFragment:
|
||||||
raw = " ".join([f"{b:02x}" for b in self.frame])
|
raw = " ".join([f"{b:02x}" for b in self.frame])
|
||||||
return f"{dt} Received {size} bytes{channel}: {raw}"
|
return f"{dt} Received {size} bytes{channel}: {raw}"
|
||||||
|
|
||||||
|
class HoymilesNRF:
|
||||||
|
tx_channel_id = 0
|
||||||
|
tx_channel_list = [40]
|
||||||
|
rx_channel_id = 0
|
||||||
|
rx_channel_list = [3,6,9,11,23,40,61,75]
|
||||||
|
rx_channel_ack = False
|
||||||
|
rx_error = 0
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
self.radio = device
|
||||||
|
|
||||||
|
def transmit(self, packet):
|
||||||
|
"""
|
||||||
|
Transmit Packet
|
||||||
|
"""
|
||||||
|
|
||||||
|
#dst_esb_addr = b'\x01' + packet[1:5]
|
||||||
|
#src_esb_addr = b'\x01' + packet[6:9]
|
||||||
|
|
||||||
|
#hexify_payload(dst_esb_addr)
|
||||||
|
#hexify_payload(src_esb_addr)
|
||||||
|
|
||||||
|
self.radio.stopListening() # put radio in TX mode
|
||||||
|
self.radio.setDataRate(RF24_250KBPS)
|
||||||
|
#self.radio.openReadingPipe(1, src_esb_addr )
|
||||||
|
#self.radio.openWritingPipe( dst_esb_addr )
|
||||||
|
self.radio.setChannel(self.tx_channel)
|
||||||
|
self.radio.setAutoAck(True)
|
||||||
|
self.radio.setRetries(3, 15)
|
||||||
|
self.radio.setCRCLength(RF24_CRC_16)
|
||||||
|
self.radio.enableDynamicPayloads()
|
||||||
|
|
||||||
|
return self.radio.write(packet)
|
||||||
|
|
||||||
|
def receive(self, timeout=None):
|
||||||
|
"""
|
||||||
|
Receive Packets
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not timeout:
|
||||||
|
timeout=12e8
|
||||||
|
|
||||||
|
self.radio.setChannel(self.rx_channel)
|
||||||
|
self.radio.setAutoAck(False)
|
||||||
|
self.radio.setRetries(0, 0)
|
||||||
|
self.radio.enableDynamicPayloads()
|
||||||
|
self.radio.setCRCLength(RF24_CRC_16)
|
||||||
|
self.radio.startListening()
|
||||||
|
|
||||||
|
fragments = []
|
||||||
|
|
||||||
|
# Receive: Loop
|
||||||
|
t_end = time.monotonic_ns()+timeout
|
||||||
|
while time.monotonic_ns() < t_end:
|
||||||
|
|
||||||
|
has_payload, pipe_number = self.radio.available_pipe()
|
||||||
|
if has_payload:
|
||||||
|
# Data in nRF24 buffer, read it
|
||||||
|
self.rx_error = 0
|
||||||
|
self.rx_channel_ack = True
|
||||||
|
t_end = time.monotonic_ns()+5e8
|
||||||
|
|
||||||
|
size = self.radio.getDynamicPayloadSize()
|
||||||
|
payload = self.radio.read(size)
|
||||||
|
fragment = InverterPacketFragment(
|
||||||
|
payload=payload,
|
||||||
|
ch_rx=self.rx_channel, ch_tx=self.tx_channel,
|
||||||
|
time_rx=datetime.now()
|
||||||
|
)
|
||||||
|
yield(fragment)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# No data in nRF rx buffer, search and wait
|
||||||
|
# Channel lock in (not currently used)
|
||||||
|
self.rx_error = self.rx_error + 1
|
||||||
|
if self.rx_error > 0:
|
||||||
|
self.rx_channel_ack = False
|
||||||
|
# Channel hopping
|
||||||
|
if self.next_rx_channel():
|
||||||
|
self.radio.stopListening()
|
||||||
|
self.radio.setChannel(self.rx_channel)
|
||||||
|
self.radio.startListening()
|
||||||
|
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
def next_rx_channel(self):
|
||||||
|
if not self.rx_channel_ack:
|
||||||
|
self.rx_channel_id = self.rx_channel_id + 1
|
||||||
|
if self.rx_channel_id >= len(self.rx_channel_list):
|
||||||
|
self.rx_channel_id = 0
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tx_channel(self):
|
||||||
|
return self.tx_channel_list[self.tx_channel_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rx_channel(self):
|
||||||
|
return self.rx_channel_list[self.rx_channel_id]
|
||||||
|
|
||||||
|
def frame_payload(payload):
|
||||||
|
payload_crc = f_crc_m(payload)
|
||||||
|
payload = payload + struct.pack('>H', payload_crc)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
|
||||||
|
if len(fragment) > 17:
|
||||||
|
raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes')
|
||||||
|
|
||||||
|
p = b''
|
||||||
|
p = p + b'\x15'
|
||||||
|
p = p + ser_to_hm_addr(dst)
|
||||||
|
p = p + ser_to_hm_addr(src)
|
||||||
|
p = p + seq
|
||||||
|
|
||||||
|
p = p + fragment
|
||||||
|
|
||||||
|
crc8 = f_crc8(p)
|
||||||
|
p = p + struct.pack('B', crc8)
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
def compose_esb_packet(packet, mtu=17, **params):
|
||||||
|
for i in range(0, len(packet), mtu):
|
||||||
|
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
||||||
|
yield(fragment)
|
||||||
|
|
||||||
|
def compose_set_time_payload(timestamp=None):
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
payload = b'\x0b\x00'
|
||||||
|
payload = payload + struct.pack('>L', timestamp) # big-endian: msb at low address
|
||||||
|
payload = payload + b'\x00\x00\x00\x05\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
return frame_payload(payload)
|
||||||
|
|
||||||
|
def compose_02_payload(timestamp=None):
|
||||||
|
payload = b'\x02'
|
||||||
|
if timestamp:
|
||||||
|
payload = payload + b'\x00'
|
||||||
|
payload = payload + struct.pack('>L', timestamp) # big-endian: msb at low address
|
||||||
|
payload = payload + b'\x00\x00\x00\x05\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
return frame_payload(payload)
|
||||||
|
|
||||||
|
def compose_11_payload():
|
||||||
|
payload = b'\x11'
|
||||||
|
|
||||||
|
return frame_payload(payload)
|
||||||
|
|
||||||
|
|
||||||
class InverterTransaction:
|
class InverterTransaction:
|
||||||
|
tx_queue = []
|
||||||
|
scratch = []
|
||||||
|
inverter_ser = None
|
||||||
|
inverter_addr = None
|
||||||
|
dtu_ser = None
|
||||||
|
req_type = None
|
||||||
|
|
||||||
|
radio = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
request_time=datetime.now(),
|
request_time=None,
|
||||||
inverter_ser=None,
|
inverter_ser=None,
|
||||||
dtu_ser=None,
|
dtu_ser=None,
|
||||||
|
radio=None,
|
||||||
**params):
|
**params):
|
||||||
|
|
||||||
|
if radio:
|
||||||
|
self.radio = radio
|
||||||
|
|
||||||
|
if not request_time:
|
||||||
|
request_time=datetime.now()
|
||||||
|
|
||||||
self.scratch = []
|
self.scratch = []
|
||||||
if 'scratch' in params:
|
if 'scratch' in params:
|
||||||
self.scratch = params['scratch']
|
self.scratch = params['scratch']
|
||||||
|
|
||||||
self.inverter_ser = inverter_ser
|
self.inverter_ser = inverter_ser
|
||||||
if inverter_ser:
|
if inverter_ser:
|
||||||
self.peer_src = ser_to_hm_addr(inverter_ser)
|
self.inverter_addr = ser_to_hm_addr(inverter_ser)
|
||||||
|
|
||||||
self.dtu_ser = dtu_ser
|
self.dtu_ser = dtu_ser
|
||||||
if dtu_ser:
|
if dtu_ser:
|
||||||
self.dtu_dst = ser_to_hm_addr(dtu_ser)
|
self.dtu_addr = ser_to_hm_addr(dtu_ser)
|
||||||
|
|
||||||
self.peer_src, self.peer_dst, self.req_type = (None,None,None)
|
|
||||||
|
|
||||||
self.request = None
|
self.request = None
|
||||||
if 'request' in params:
|
if 'request' in params:
|
||||||
self.request = params['request']
|
self.request = params['request']
|
||||||
self.peer_src, self.peer_dst, skip, self.req_type = struct.unpack('>LLBB', params['request'][1:11])
|
self.queue_tx(self.request)
|
||||||
|
self.inverter_addr, self.dtu_addr, seq, self.req_type = struct.unpack('>LLBB', params['request'][1:11])
|
||||||
self.request_time = request_time
|
self.request_time = request_time
|
||||||
|
|
||||||
|
def rxtx(self):
|
||||||
|
"""
|
||||||
|
Transmit next packet from tx_queue if available
|
||||||
|
and wait for responses
|
||||||
|
"""
|
||||||
|
if not self.radio:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not len(self.tx_queue):
|
||||||
|
return False
|
||||||
|
|
||||||
|
packet = self.tx_queue.pop(0)
|
||||||
|
|
||||||
|
if HOYMILES_TRANSACTION_LOGGING:
|
||||||
|
dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
|
print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}')
|
||||||
|
|
||||||
|
self.radio.transmit(packet)
|
||||||
|
|
||||||
|
wait = False
|
||||||
|
try:
|
||||||
|
for response in self.radio.receive():
|
||||||
|
if HOYMILES_TRANSACTION_LOGGING:
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
self.frame_append(response)
|
||||||
|
wait = True
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return wait
|
||||||
|
|
||||||
def frame_append(self, payload_frame):
|
def frame_append(self, payload_frame):
|
||||||
|
"""
|
||||||
|
Append received raw frame to local scratch buffer
|
||||||
|
"""
|
||||||
self.scratch.append(payload_frame)
|
self.scratch.append(payload_frame)
|
||||||
|
|
||||||
|
def queue_tx(self, frame):
|
||||||
|
"""
|
||||||
|
Enqueue packet for transmission if radio is available
|
||||||
|
"""
|
||||||
|
if not self.radio:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.tx_queue.append(frame)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def get_payload(self, src=None):
|
def get_payload(self, src=None):
|
||||||
"""
|
"""
|
||||||
Reconstruct Hoymiles payload from scratch
|
Reconstruct Hoymiles payload from scratch
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not src:
|
if not src:
|
||||||
src = self.peer_src
|
src = self.inverter_addr
|
||||||
|
|
||||||
# Collect all frames from source_address src
|
# Collect all frames from source_address src
|
||||||
frames = [frame for frame in self.scratch if frame.src == src]
|
frames = [frame for frame in self.scratch if frame.src == src]
|
||||||
|
@ -251,30 +432,42 @@ class InverterTransaction:
|
||||||
self.time_rx = end_frame.time_rx
|
self.time_rx = end_frame.time_rx
|
||||||
tr_len = end_frame.seq - 0x80
|
tr_len = end_frame.seq - 0x80
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise BufferError('Missing packet: Last packet')
|
raise BufferError(f'Missing packet: Last packet {len(self.scratch)}')
|
||||||
|
|
||||||
# Rebuild payload from unordered frames
|
# Rebuild payload from unordered frames
|
||||||
payload = b''
|
payload = b''
|
||||||
seq_missing = []
|
for frame_id in range(1, tr_len):
|
||||||
for i in range(1, tr_len):
|
|
||||||
try:
|
try:
|
||||||
data_frame = next(item for item in frames if item.seq == i)
|
data_frame = next(item for item in frames if item.seq == frame_id)
|
||||||
payload = payload + data_frame.data
|
payload = payload + data_frame.data
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
seq_missing.append(i)
|
self.__retransmit_frame(frame_id)
|
||||||
pass
|
raise BufferError(f'Frame {frame_id} missing: Request Retransmit')
|
||||||
|
|
||||||
payload = payload + end_frame.data
|
payload = payload + end_frame.data
|
||||||
|
|
||||||
# check crc
|
# check crc
|
||||||
pcrc = struct.unpack('>H', payload[-2:])[0]
|
pcrc = struct.unpack('>H', payload[-2:])[0]
|
||||||
if f_crc_m(payload[:-2]) != pcrc:
|
if f_crc_m(payload[:-2]) != pcrc:
|
||||||
raise BufferError('Payload failed CRC check.')
|
raise ValueError('Payload failed CRC check.')
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
def __retransmit_frame(self, frame_id):
|
||||||
|
"""
|
||||||
|
Build and queue retransmit request
|
||||||
|
"""
|
||||||
|
packet = compose_esb_fragment(b'',
|
||||||
|
seq=int(0x80 + frame_id).to_bytes(1, 'big'),
|
||||||
|
src=self.dtu_ser,
|
||||||
|
dst=self.inverter_ser)
|
||||||
|
|
||||||
|
return self.queue_tx(packet)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
size = len(self.request)
|
size = len(self.request)
|
||||||
raw = " ".join([f"{b:02x}" for b in self.request])
|
return f'{dt} Transmit | {hexify_payload(self.request)}'
|
||||||
return f'{dt} Transmit | {raw}'
|
|
||||||
|
def hexify_payload(byte_var):
|
||||||
|
return ' '.join([f"{b:02x}" for b in byte_var])
|
||||||
|
|
143
tools/rpi/hoymiles/decoders/__init__.py
Normal file
143
tools/rpi/hoymiles/decoders/__init__.py
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import struct
|
||||||
|
|
||||||
|
class StatusResponse:
|
||||||
|
e_keys = ['voltage','current','power','energy_total','energy_daily']
|
||||||
|
|
||||||
|
def unpack(self, fmt, base):
|
||||||
|
size = struct.calcsize(fmt)
|
||||||
|
return struct.unpack(fmt, self.response[base:base+size])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def phases(self):
|
||||||
|
phases = []
|
||||||
|
p_exists = True
|
||||||
|
while p_exists:
|
||||||
|
p_exists = False
|
||||||
|
phase_id = len(phases)
|
||||||
|
phase = {}
|
||||||
|
for key in self.e_keys:
|
||||||
|
prop = f'ac_{key}_{phase_id}'
|
||||||
|
if hasattr(self, prop):
|
||||||
|
p_exists = True
|
||||||
|
phase[key] = getattr(self, prop)
|
||||||
|
if p_exists:
|
||||||
|
phases.append(phase)
|
||||||
|
|
||||||
|
return phases
|
||||||
|
|
||||||
|
@property
|
||||||
|
def strings(self):
|
||||||
|
strings = []
|
||||||
|
s_exists = True
|
||||||
|
while s_exists:
|
||||||
|
s_exists = False
|
||||||
|
string_id = len(strings)
|
||||||
|
string = {}
|
||||||
|
for key in self.e_keys:
|
||||||
|
prop = f'dc_{key}_{string_id}'
|
||||||
|
if hasattr(self, prop):
|
||||||
|
s_exists = True
|
||||||
|
string[key] = getattr(self, prop)
|
||||||
|
if s_exists:
|
||||||
|
strings.append(string)
|
||||||
|
|
||||||
|
return strings
|
||||||
|
|
||||||
|
def __dict__(self):
|
||||||
|
data = {}
|
||||||
|
data['phases'] = self.phases
|
||||||
|
data['strings'] = self.strings
|
||||||
|
data['temperature'] = self.temperature
|
||||||
|
data['frequency'] = self.frequency
|
||||||
|
return data
|
||||||
|
|
||||||
|
class UnknownResponse:
|
||||||
|
@property
|
||||||
|
def hex_ascii(self):
|
||||||
|
return ' '.join([f'{b:02x}' for b in self.response])
|
||||||
|
@property
|
||||||
|
def dump_longs(self):
|
||||||
|
n = len(self.response)/4
|
||||||
|
vals = struct.unpack(f'>{int(n)}L', self.response)
|
||||||
|
return vals
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dump_shorts(self):
|
||||||
|
n = len(self.response)/2
|
||||||
|
vals = struct.unpack(f'>{int(n)}H', self.response)
|
||||||
|
return vals
|
||||||
|
|
||||||
|
class HM600_Decode02(UnknownResponse):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
class HM600_Decode11(UnknownResponse):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
class HM600_Decode12(UnknownResponse):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
class HM600_Decode0A(UnknownResponse):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
class HM600_Decode0B(StatusResponse):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dc_voltage_0(self):
|
||||||
|
return self.unpack('>H', 2)[0]/10
|
||||||
|
@property
|
||||||
|
def dc_current_0(self):
|
||||||
|
return self.unpack('>H', 4)[0]/100
|
||||||
|
@property
|
||||||
|
def dc_power_0(self):
|
||||||
|
return self.unpack('>H', 6)[0]/10
|
||||||
|
@property
|
||||||
|
def dc_energy_total_0(self):
|
||||||
|
return self.unpack('>L', 14)[0]
|
||||||
|
@property
|
||||||
|
def dc_energy_daily_0(self):
|
||||||
|
return self.unpack('>H', 22)[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dc_voltage_1(self):
|
||||||
|
return self.unpack('>H', 8)[0]/10
|
||||||
|
@property
|
||||||
|
def dc_current_1(self):
|
||||||
|
return self.unpack('>H', 10)[0]/100
|
||||||
|
@property
|
||||||
|
def dc_power_1(self):
|
||||||
|
return self.unpack('>H', 12)[0]/10
|
||||||
|
@property
|
||||||
|
def dc_energy_total_1(self):
|
||||||
|
return self.unpack('>L', 18)[0]
|
||||||
|
@property
|
||||||
|
def dc_energy_daily_1(self):
|
||||||
|
return self.unpack('>H', 24)[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ac_voltage_0(self):
|
||||||
|
return self.unpack('>H', 26)[0]/10
|
||||||
|
@property
|
||||||
|
def ac_current_0(self):
|
||||||
|
return self.unpack('>H', 34)[0]/10
|
||||||
|
@property
|
||||||
|
def ac_power_0(self):
|
||||||
|
return self.unpack('>H', 30)[0]/10
|
||||||
|
@property
|
||||||
|
def frequency(self):
|
||||||
|
return self.unpack('>H', 28)[0]/100
|
||||||
|
@property
|
||||||
|
def temperature(self):
|
||||||
|
return self.unpack('>H', 38)[0]/10
|
||||||
|
|
||||||
|
class HM600_Decode0C(HM600_Decode0B):
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# TBD
|
|
||||||
|
|
||||||
class ESBFrameFactory:
|
|
||||||
def __init__(self, payload):
|
|
||||||
self.payload = payload
|
|
||||||
|
|
||||||
class ESBTransactionFactory:
|
|
||||||
"""
|
|
||||||
Put a payload into ESB packets for transmission
|
|
||||||
"""
|
|
||||||
def __init__(self, src, dst, **params):
|
|
||||||
self.src = src
|
|
||||||
self.dst = dst
|
|
Loading…
Add table
Reference in a new issue