mirror of https://github.com/lumapu/ahoy.git
Browse Source
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.pull/25/head
Jan-Jonas Sämann
3 years ago
7 changed files with 603 additions and 350 deletions
@ -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() |
|
||||
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) |
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway') |
||||
mqtt_client = paho.mqtt.client.Client() |
parser.add_argument("-c", "--config-file", nargs="?", |
||||
mqtt_client.username_pw_set(mqtt_user, mqtt_password) |
help="configuration file") |
||||
mqtt_client.connect(mqtt_host, mqtt_port) |
global_config = parser.parse_args() |
||||
mqtt_client.loop_start() |
|
||||
|
|
||||
# Master Address ('DTU') |
|
||||
dtu_ser = cfg.get('dtu', 'serial', fallback='99978563412') # identical to fc22's |
|
||||
|
|
||||
# inverter serial numbers |
if global_config.config_file: |
||||
inv_ser = cfg.get('inverter', 'serial', fallback='444473104619') # my inverter |
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) |
||||
|
|
||||
# all inverters |
radio = RF24(22, 0, 1000000) |
||||
#... |
hmradio = hoymiles.HoymilesNRF(device=radio) |
||||
|
mqtt_client = None |
||||
|
|
||||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') |
command_queue = {} |
||||
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) |
|
||||
|
|
||||
# time of last transmission - to calculcate response time |
hoymiles.HOYMILES_TRANSACTION_LOGGING=True |
||||
t_last_tx = 0 |
hoymiles.HOYMILES_DEBUG_LOGGING=True |
||||
|
|
||||
def main_loop(): |
def main_loop(): |
||||
|
inverters = [ |
||||
|
inverter for inverter in ahoy_config.get('inverters', []) |
||||
|
if not inverter.get('disabled', False)] |
||||
|
|
||||
|
for inverter in inverters: |
||||
|
if hoymiles.HOYMILES_DEBUG_LOGGING: |
||||
|
print(f'Poll inverter {inverter["serial"]}') |
||||
|
poll_inverter(inverter) |
||||
|
|
||||
|
def poll_inverter(inverter): |
||||
|
inverter_ser = inverter.get('serial') |
||||
|
dtu_ser = ahoy_config.get('dtu', {}).get('serial') |
||||
|
|
||||
|
if len(command_queue[str(inverter_ser)]) > 0: |
||||
|
payload = command_queue[str(inverter_ser)].pop(0) |
||||
|
else: |
||||
|
payload = hoymiles.compose_set_time_payload() |
||||
|
|
||||
|
payload_ttl = 4 |
||||
|
while payload_ttl > 0: |
||||
|
payload_ttl = payload_ttl - 1 |
||||
|
com = hoymiles.InverterTransaction( |
||||
|
radio=hmradio, |
||||
|
dtu_ser=dtu_ser, |
||||
|
inverter_ser=inverter_ser, |
||||
|
request=next(hoymiles.compose_esb_packet( |
||||
|
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 |
||||
|
|
||||
|
if response: |
||||
|
dt = datetime.now() |
||||
|
print(f'{dt} Payload: ' + hoymiles.hexify_payload(response)) |
||||
|
decoder = hoymiles.ResponseDecoder(response, |
||||
|
request=com.request, |
||||
|
inverter_ser=inverter_ser |
||||
|
) |
||||
|
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() |
||||
|
|
||||
|
if mqtt_client: |
||||
|
mqtt_send_status(mqtt_client, inverter_ser, data) |
||||
|
|
||||
|
|
||||
|
def mqtt_send_status(broker, interter_ser, data): |
||||
|
topic = f'ahoy/{inverter_ser}' |
||||
|
|
||||
|
# 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 |
||||
|
|
||||
|
# DC Data |
||||
|
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']) |
||||
|
|
||||
|
def mqtt_on_command(): |
||||
""" |
""" |
||||
Keep receiving on channel 3. Every once in a while, transmit a request |
Handle commands to topic |
||||
to one of our inverters on channel 40. |
ahoy/{inverter_ser}/command |
||||
|
frame it and put onto command_queue |
||||
""" |
""" |
||||
|
raise NotImplementedError('Receiving mqtt commands is yet to be implemented') |
||||
|
|
||||
global t_last_tx |
if __name__ == '__main__': |
||||
|
ahoy_config = dict(cfg.get('ahoy', {})) |
||||
hoymiles.print_addr(inv_ser) |
|
||||
hoymiles.print_addr(dtu_ser) |
mqtt_config = ahoy_config.get('mqtt', []) |
||||
|
if mqtt_config.get('disabled', True): |
||||
ctr = 1 |
mqtt_client = paho.mqtt.client.Client() |
||||
last_tx_message = '' |
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)) |
||||
rx_channels = [3,6,9,11,23,40,61,75] |
mqtt_client.loop_start() |
||||
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(): |
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() |
||||
|
@ -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}' |
@ -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…
Reference in new issue