mirror of
https://github.com/lumapu/ahoy.git
synced 2025-07-31 23:18:25 +02:00
Improve python code quality
Mostly fix naming convetion, add descriptions. Cleanup code, left behind from tinkering.
This commit is contained in:
parent
2934c23688
commit
e4eebb67e4
4 changed files with 279 additions and 163 deletions
|
@ -1,62 +1,65 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hoymiles micro-inverters python shared code
|
||||||
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
import crcmod
|
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import crcmod
|
||||||
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 *
|
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=False
|
HOYMILES_TRANSACTION_LOGGING=False
|
||||||
HOYMILES_DEBUG_LOGGING=False
|
HOYMILES_DEBUG_LOGGING=False
|
||||||
|
|
||||||
def ser_to_hm_addr(s):
|
def ser_to_hm_addr(inverter_ser):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
address each other.
|
address each other.
|
||||||
|
|
||||||
:param str s: inverter serial
|
:param str inverter_ser: inverter serial
|
||||||
:return: inverter address
|
:return: inverter address
|
||||||
:rtype: bytes
|
:rtype: bytes
|
||||||
"""
|
"""
|
||||||
bcd = int(str(s)[-8:], base=16)
|
bcd = int(str(inverter_ser)[-8:], base=16)
|
||||||
return struct.pack('>L', bcd)
|
return struct.pack('>L', bcd)
|
||||||
|
|
||||||
def ser_to_esb_addr(s):
|
def ser_to_esb_addr(inverter_ser):
|
||||||
"""
|
"""
|
||||||
Convert a Hoymiles inverter/DTU serial number into its
|
Convert a Hoymiles inverter/DTU serial number into its
|
||||||
corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes).
|
corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes).
|
||||||
|
|
||||||
The NRF library expects these in LSB to MSB order, even though the transceiver
|
The NRF library expects these in LSB to MSB order, even though the transceiver
|
||||||
itself will then output them in MSB-to-LSB order over the air.
|
itself will then output them in MSB-to-LSB order over the air.
|
||||||
|
|
||||||
The inverters use a BCD representation of the last 8
|
The inverters use a BCD representation of the last 8
|
||||||
digits of their serial number, in reverse byte order,
|
digits of their serial number, in reverse byte order,
|
||||||
followed by \x01.
|
followed by \x01.
|
||||||
|
|
||||||
:param str s: inverter serial
|
:param str inverter_ser: inverter serial
|
||||||
:return: ESB inverter address
|
:return: ESB inverter address
|
||||||
:rtype: bytes
|
:rtype: bytes
|
||||||
"""
|
"""
|
||||||
air_order = ser_to_hm_addr(s)[::-1] + b'\x01'
|
air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01'
|
||||||
return air_order[::-1]
|
return air_order[::-1]
|
||||||
|
|
||||||
def print_addr(a):
|
def print_addr(inverter_ser):
|
||||||
"""
|
"""
|
||||||
Debug print addresses
|
Debug print addresses
|
||||||
|
|
||||||
:param str a: inverter serial
|
:param str inverter_ser: inverter serial
|
||||||
"""
|
"""
|
||||||
print(f"ser# {a} ", end='')
|
print(f"ser# {inverter_ser} ", end='')
|
||||||
print(f" -> HM {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='')
|
print(f" -> HM {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='')
|
||||||
print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}")
|
print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}")
|
||||||
|
|
||||||
# time of last transmission - to calculcate response time
|
|
||||||
t_last_tx = 0
|
|
||||||
|
|
||||||
class ResponseDecoderFactory:
|
class ResponseDecoderFactory:
|
||||||
"""
|
"""
|
||||||
|
@ -115,16 +118,16 @@ class ResponseDecoderFactory:
|
||||||
raise ValueError('Inverter serial while decoding response')
|
raise ValueError('Inverter serial while decoding response')
|
||||||
|
|
||||||
ser_db = [
|
ser_db = [
|
||||||
('HM300', r'^1121........'),
|
('Hm300', r'^1121........'),
|
||||||
('HM600', r'^1141........'),
|
('Hm600', r'^1141........'),
|
||||||
('HM1200', r'^1161........'),
|
('Hm1200', r'^1161........'),
|
||||||
]
|
]
|
||||||
ser_str = str(self.inverter_ser)
|
ser_str = str(self.inverter_ser)
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
for m, r in ser_db:
|
for s_model, r_match in ser_db:
|
||||||
if re.match(r, ser_str):
|
if re.match(r_match, ser_str):
|
||||||
model = m
|
model = s_model
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(model):
|
if len(model):
|
||||||
|
@ -162,12 +165,12 @@ class ResponseDecoder(ResponseDecoderFactory):
|
||||||
model = self.inverter_model
|
model = self.inverter_model
|
||||||
command = self.request_command
|
command = self.request_command
|
||||||
|
|
||||||
model_decoders = __import__(f'hoymiles.decoders')
|
model_decoders = __import__('hoymiles.decoders')
|
||||||
if hasattr(model_decoders, f'{model}_Decode{command.upper()}'):
|
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
|
||||||
device = getattr(model_decoders, f'{model}_Decode{command.upper()}')
|
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
|
||||||
else:
|
else:
|
||||||
if HOYMILES_DEBUG_LOGGING:
|
if HOYMILES_DEBUG_LOGGING:
|
||||||
device = getattr(model_decoders, f'DEBUG_DecodeAny')
|
device = getattr(model_decoders, 'DebugDecodeAny')
|
||||||
|
|
||||||
return device(self.response,
|
return device(self.response,
|
||||||
time_rx=self.time_rx,
|
time_rx=self.time_rx,
|
||||||
|
@ -188,6 +191,8 @@ class InverterPacketFragment:
|
||||||
:type ch_rx: int
|
:type ch_rx: int
|
||||||
:param ch_tx: channel where request was sent
|
:param ch_tx: channel where request was sent
|
||||||
:type ch_tx: int
|
:type ch_tx: int
|
||||||
|
|
||||||
|
:raises BufferError: when data gets lost on SPI bus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not time_rx:
|
if not time_rx:
|
||||||
|
@ -255,11 +260,11 @@ class InverterPacketFragment:
|
||||||
:return: log line received frame
|
:return: log line received frame
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
dt = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
|
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
size = len(self.frame)
|
size = len(self.frame)
|
||||||
channel = f' channel {self.ch_rx}' if self.ch_rx else ''
|
channel = f' channel {self.ch_rx}' if self.ch_rx else ''
|
||||||
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"{c_datetime} Received {size} bytes{channel}: {raw}"
|
||||||
|
|
||||||
class HoymilesNRF:
|
class HoymilesNRF:
|
||||||
"""Hoymiles NRF24 Interface"""
|
"""Hoymiles NRF24 Interface"""
|
||||||
|
@ -330,6 +335,7 @@ class HoymilesNRF:
|
||||||
|
|
||||||
has_payload, pipe_number = self.radio.available_pipe()
|
has_payload, pipe_number = self.radio.available_pipe()
|
||||||
if has_payload:
|
if has_payload:
|
||||||
|
|
||||||
# Data in nRF24 buffer, read it
|
# Data in nRF24 buffer, read it
|
||||||
self.rx_error = 0
|
self.rx_error = 0
|
||||||
self.rx_channel_ack = True
|
self.rx_channel_ack = True
|
||||||
|
@ -342,9 +348,11 @@ class HoymilesNRF:
|
||||||
ch_rx=self.rx_channel, ch_tx=self.tx_channel,
|
ch_rx=self.rx_channel, ch_tx=self.tx_channel,
|
||||||
time_rx=datetime.now()
|
time_rx=datetime.now()
|
||||||
)
|
)
|
||||||
yield(fragment)
|
|
||||||
|
yield fragment
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# No data in nRF rx buffer, search and wait
|
# No data in nRF rx buffer, search and wait
|
||||||
# Channel lock in (not currently used)
|
# Channel lock in (not currently used)
|
||||||
self.rx_error = self.rx_error + 1
|
self.rx_error = self.rx_error + 1
|
||||||
|
@ -407,7 +415,7 @@ def frame_payload(payload):
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
|
def compose_esb_fragment(fragment, seq=b'\x80', src=99999999, dst=1, **params):
|
||||||
"""
|
"""
|
||||||
Build standart ESB request fragment
|
Build standart ESB request fragment
|
||||||
|
|
||||||
|
@ -423,20 +431,19 @@ def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
|
||||||
:raises ValueError: if fragment size larger 16 byte
|
:raises ValueError: if fragment size larger 16 byte
|
||||||
"""
|
"""
|
||||||
if len(fragment) > 17:
|
if len(fragment) > 17:
|
||||||
raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes')
|
raise ValueError(f'ESB fragment exeeds mtu: Fragment size {len(fragment)} bytes')
|
||||||
|
|
||||||
p = b''
|
packet = b'\x15'
|
||||||
p = p + b'\x15'
|
packet = packet + ser_to_hm_addr(dst)
|
||||||
p = p + ser_to_hm_addr(dst)
|
packet = packet + ser_to_hm_addr(src)
|
||||||
p = p + ser_to_hm_addr(src)
|
packet = packet + seq
|
||||||
p = p + seq
|
|
||||||
|
|
||||||
p = p + fragment
|
packet = packet + fragment
|
||||||
|
|
||||||
crc8 = f_crc8(p)
|
crc8 = f_crc8(packet)
|
||||||
p = p + struct.pack('B', crc8)
|
packet = packet + struct.pack('B', crc8)
|
||||||
|
|
||||||
return p
|
return packet
|
||||||
|
|
||||||
def compose_esb_packet(packet, mtu=17, **params):
|
def compose_esb_packet(packet, mtu=17, **params):
|
||||||
"""
|
"""
|
||||||
|
@ -449,7 +456,7 @@ def compose_esb_packet(packet, mtu=17, **params):
|
||||||
"""
|
"""
|
||||||
for i in range(0, len(packet), mtu):
|
for i in range(0, len(packet), mtu):
|
||||||
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
||||||
yield(fragment)
|
yield fragment
|
||||||
|
|
||||||
def compose_set_time_payload(timestamp=None):
|
def compose_set_time_payload(timestamp=None):
|
||||||
"""
|
"""
|
||||||
|
@ -480,6 +487,7 @@ class InverterTransaction:
|
||||||
inverter_addr = None
|
inverter_addr = None
|
||||||
dtu_ser = None
|
dtu_ser = None
|
||||||
req_type = None
|
req_type = None
|
||||||
|
time_rx = None
|
||||||
|
|
||||||
radio = None
|
radio = None
|
||||||
|
|
||||||
|
@ -538,15 +546,15 @@ class InverterTransaction:
|
||||||
if not self.radio:
|
if not self.radio:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not len(self.tx_queue):
|
if len(self.tx_queue) == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
packet = self.tx_queue.pop(0)
|
packet = self.tx_queue.pop(0)
|
||||||
|
|
||||||
if HOYMILES_TRANSACTION_LOGGING:
|
if HOYMILES_TRANSACTION_LOGGING:
|
||||||
dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}')
|
print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
|
||||||
|
|
||||||
self.radio.transmit(packet)
|
self.radio.transmit(packet)
|
||||||
|
|
||||||
wait = False
|
wait = False
|
||||||
|
@ -554,7 +562,7 @@ class InverterTransaction:
|
||||||
for response in self.radio.receive():
|
for response in self.radio.receive():
|
||||||
if HOYMILES_TRANSACTION_LOGGING:
|
if HOYMILES_TRANSACTION_LOGGING:
|
||||||
print(response)
|
print(response)
|
||||||
|
|
||||||
self.frame_append(response)
|
self.frame_append(response)
|
||||||
wait = True
|
wait = True
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
@ -654,9 +662,9 @@ class InverterTransaction:
|
||||||
:return: log line of payload for transmission
|
:return: log line of payload for transmission
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
c_datetime = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
size = len(self.request)
|
size = len(self.request)
|
||||||
return f'{dt} Transmit | {hexify_payload(self.request)}'
|
return f'{c_datetime} Transmit | {hexify_payload(self.request)}'
|
||||||
|
|
||||||
def hexify_payload(byte_var):
|
def hexify_payload(byte_var):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hoymiles micro-inverters main application
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import struct
|
import struct
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import argparse
|
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
|
|
||||||
import paho.mqtt.client
|
|
||||||
import yaml
|
import yaml
|
||||||
from yaml.loader import SafeLoader
|
from yaml.loader import SafeLoader
|
||||||
|
import paho.mqtt.client
|
||||||
|
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
||||||
|
import hoymiles
|
||||||
|
|
||||||
def main_loop():
|
def main_loop():
|
||||||
"""Main loop"""
|
"""Main loop"""
|
||||||
|
@ -61,14 +65,14 @@ def poll_inverter(inverter, retries=4):
|
||||||
try:
|
try:
|
||||||
response = com.get_payload()
|
response = com.get_payload()
|
||||||
payload_ttl = 0
|
payload_ttl = 0
|
||||||
except Exception as e:
|
except Exception as e_all:
|
||||||
print(f'Error while retrieving data: {e}')
|
print(f'Error while retrieving data: {e_all}')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Handle the response data if any
|
# Handle the response data if any
|
||||||
if response:
|
if response:
|
||||||
dt = datetime.now()
|
c_datetime = datetime.now()
|
||||||
print(f'{dt} Payload: ' + hoymiles.hexify_payload(response))
|
print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
|
||||||
decoder = hoymiles.ResponseDecoder(response,
|
decoder = hoymiles.ResponseDecoder(response,
|
||||||
request=com.request,
|
request=com.request,
|
||||||
inverter_ser=inverter_ser
|
inverter_ser=inverter_ser
|
||||||
|
@ -77,7 +81,7 @@ def poll_inverter(inverter, retries=4):
|
||||||
if isinstance(result, hoymiles.decoders.StatusResponse):
|
if isinstance(result, hoymiles.decoders.StatusResponse):
|
||||||
data = result.__dict__()
|
data = result.__dict__()
|
||||||
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
print(f'{dt} Decoded: {data["temperature"]}', end='')
|
print(f'{c_datetime} Decoded: {data["temperature"]}', end='')
|
||||||
phase_id = 0
|
phase_id = 0
|
||||||
for phase in data['phases']:
|
for phase in data['phases']:
|
||||||
print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
|
print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
|
||||||
|
@ -185,17 +189,17 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
# Load ahoy.yml config file
|
# Load ahoy.yml config file
|
||||||
try:
|
try:
|
||||||
if isinstance(global_config.config_file, str) == True:
|
if isinstance(global_config.config_file, str):
|
||||||
with open(global_config.config_file, 'r') as yf:
|
with open(global_config.config_file, 'r') as fh_yaml:
|
||||||
cfg = yaml.load(yf, Loader=SafeLoader)
|
cfg = yaml.load(fh_yaml, Loader=SafeLoader)
|
||||||
else:
|
else:
|
||||||
with open('ahoy.yml', 'r') as yf:
|
with open('ahoy.yml', 'r') as fh_yaml:
|
||||||
cfg = yaml.load(yf, Loader=SafeLoader)
|
cfg = yaml.load(fh_yaml, Loader=SafeLoader)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Could not load config file. Try --help")
|
print("Could not load config file. Try --help")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
except yaml.YAMLError as ye:
|
except yaml.YAMLError as e_yaml:
|
||||||
print('Failed to load config frile {global_config.config_file}: {ye}')
|
print('Failed to load config frile {global_config.config_file}: {e_yaml}')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
ahoy_config = dict(cfg.get('ahoy', {}))
|
ahoy_config = dict(cfg.get('ahoy', {}))
|
||||||
|
@ -241,18 +245,18 @@ if __name__ == '__main__':
|
||||||
if not radio.begin():
|
if not radio.begin():
|
||||||
raise RuntimeError('Can\'t open radio')
|
raise RuntimeError('Can\'t open radio')
|
||||||
|
|
||||||
inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])]
|
g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])]
|
||||||
for inverter in ahoy_config.get('inverters', []):
|
for g_inverter in ahoy_config.get('inverters', []):
|
||||||
inverter_ser = inverter.get('serial')
|
g_inverter_ser = g_inverter.get('serial')
|
||||||
command_queue[str(inverter_ser)] = []
|
command_queue[str(g_inverter_ser)] = []
|
||||||
|
|
||||||
#
|
#
|
||||||
# Enables and subscribe inverter to mqtt /command-Topic
|
# Enables and subscribe inverter to mqtt /command-Topic
|
||||||
#
|
#
|
||||||
if mqtt_client and inverter.get('mqtt', {}).get('send_raw_enabled', False):
|
if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False):
|
||||||
topic_item = (
|
topic_item = (
|
||||||
str(inverter_ser),
|
str(g_inverter_ser),
|
||||||
inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command'
|
g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command'
|
||||||
)
|
)
|
||||||
mqtt_client.subscribe(topic_item[1])
|
mqtt_client.subscribe(topic_item[1])
|
||||||
mqtt_command_topic_subs.append(topic_item)
|
mqtt_command_topic_subs.append(topic_item)
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hoymiles Micro-Inverters decoder library
|
||||||
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
import crcmod
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import crcmod
|
||||||
|
|
||||||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
|
|
||||||
class Response:
|
class Response:
|
||||||
|
""" All Response Shared methods """
|
||||||
inverter_ser = None
|
inverter_ser = None
|
||||||
inverter_name = None
|
inverter_name = None
|
||||||
dtu_ser = None
|
dtu_ser = None
|
||||||
|
@ -28,6 +34,7 @@ class Response:
|
||||||
self.time_rx = datetime.now()
|
self.time_rx = datetime.now()
|
||||||
|
|
||||||
def __dict__(self):
|
def __dict__(self):
|
||||||
|
""" Base values, availabe in each __dict__ call """
|
||||||
return {
|
return {
|
||||||
'inverter_ser': self.inverter_ser,
|
'inverter_ser': self.inverter_ser,
|
||||||
'inverter_name': self.inverter_name,
|
'inverter_name': self.inverter_name,
|
||||||
|
@ -36,9 +43,8 @@ class Response:
|
||||||
class StatusResponse(Response):
|
class StatusResponse(Response):
|
||||||
"""Inverter StatusResponse object"""
|
"""Inverter StatusResponse object"""
|
||||||
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor']
|
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor']
|
||||||
|
temperature = None
|
||||||
def __init__(self, *args, **params):
|
frequency = None
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
def unpack(self, fmt, base):
|
def unpack(self, fmt, base):
|
||||||
"""
|
"""
|
||||||
|
@ -120,9 +126,6 @@ class UnknownResponse(Response):
|
||||||
Debugging helper for unknown payload format
|
Debugging helper for unknown payload format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **params):
|
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hex_ascii(self):
|
def hex_ascii(self):
|
||||||
"""
|
"""
|
||||||
|
@ -131,7 +134,7 @@ class UnknownResponse(Response):
|
||||||
:return: hexlifierd byte string
|
:return: hexlifierd byte string
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return ' '.join([f'{b:02x}' for b in self.response])
|
return ' '.join([f'{byte:02x}' for byte in self.response])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid_crc(self):
|
def valid_crc(self):
|
||||||
|
@ -153,13 +156,13 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response
|
res = self.response
|
||||||
|
|
||||||
r = len(res) % 16
|
rem = len(res) % 16
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 16 == 0:
|
if len(res) % 16 == 0:
|
||||||
n = len(res)/4
|
rlen = len(res)/4
|
||||||
vals = struct.unpack(f'>{int(n)}L', res)
|
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
@ -171,13 +174,13 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response[2:]
|
res = self.response[2:]
|
||||||
|
|
||||||
r = len(res) % 16
|
rem = len(res) % 16
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 16 == 0:
|
if len(res) % 16 == 0:
|
||||||
n = len(res)/4
|
rlen = len(res)/4
|
||||||
vals = struct.unpack(f'>{int(n)}L', res)
|
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
@ -189,13 +192,13 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response[4:]
|
res = self.response[4:]
|
||||||
|
|
||||||
r = len(res) % 16
|
rem = len(res) % 16
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 16 == 0:
|
if len(res) % 16 == 0:
|
||||||
n = len(res)/4
|
rlen = len(res)/4
|
||||||
vals = struct.unpack(f'>{int(n)}L', res)
|
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
@ -207,13 +210,13 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response[6:]
|
res = self.response[6:]
|
||||||
|
|
||||||
r = len(res) % 16
|
rem = len(res) % 16
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 16 == 0:
|
if len(res) % 16 == 0:
|
||||||
n = len(res)/4
|
rlen = len(res)/4
|
||||||
vals = struct.unpack(f'>{int(n)}L', res)
|
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
@ -225,13 +228,13 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response
|
res = self.response
|
||||||
|
|
||||||
r = len(res) % 4
|
rem = len(res) % 4
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 4 == 0:
|
if len(res) % 4 == 0:
|
||||||
n = len(res)/2
|
rlen = len(res)/2
|
||||||
vals = struct.unpack(f'>{int(n)}H', res)
|
vals = struct.unpack(f'>{int(rlen)}H', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
|
@ -243,17 +246,18 @@ class UnknownResponse(Response):
|
||||||
|
|
||||||
res = self.response[1:]
|
res = self.response[1:]
|
||||||
|
|
||||||
r = len(res) % 4
|
rem = len(res) % 4
|
||||||
res = res[:r*-1]
|
res = res[:rem*-1]
|
||||||
|
|
||||||
vals = None
|
vals = None
|
||||||
if len(res) % 4 == 0:
|
if len(res) % 4 == 0:
|
||||||
n = len(res)/2
|
rlen = len(res)/2
|
||||||
vals = struct.unpack(f'>{int(n)}H', res)
|
vals = struct.unpack(f'>{int(rlen)}H', res)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
class EventsResponse(UnknownResponse):
|
class EventsResponse(UnknownResponse):
|
||||||
|
""" Hoymiles micro-inverter event log decode helper """
|
||||||
|
|
||||||
alarm_codes = {
|
alarm_codes = {
|
||||||
1: 'Inverter start',
|
1: 'Inverter start',
|
||||||
|
@ -337,10 +341,10 @@ class EventsResponse(UnknownResponse):
|
||||||
status = self.response[:2]
|
status = self.response[:2]
|
||||||
|
|
||||||
chunk_size = 12
|
chunk_size = 12
|
||||||
for c in range(2, len(self.response), chunk_size):
|
for i_chunk in range(2, len(self.response), chunk_size):
|
||||||
chunk = self.response[c:c+chunk_size]
|
chunk = self.response[i_chunk:i_chunk+chunk_size]
|
||||||
|
|
||||||
print(' '.join([f'{b:02x}' for b in chunk]) + ': ')
|
print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
|
||||||
|
|
||||||
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
|
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
|
||||||
a_text = self.alarm_codes.get(a_code, 'N/A')
|
a_text = self.alarm_codes.get(a_code, 'N/A')
|
||||||
|
@ -351,7 +355,7 @@ class EventsResponse(UnknownResponse):
|
||||||
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
|
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
|
||||||
print(end='', flush=True)
|
print(end='', flush=True)
|
||||||
|
|
||||||
class DEBUG_DecodeAny(UnknownResponse):
|
class DebugDecodeAny(UnknownResponse):
|
||||||
"""Default decoder"""
|
"""Default decoder"""
|
||||||
|
|
||||||
def __init__(self, *args, **params):
|
def __init__(self, *args, **params):
|
||||||
|
@ -415,206 +419,247 @@ class DEBUG_DecodeAny(UnknownResponse):
|
||||||
|
|
||||||
|
|
||||||
# 1121-Series Intervers, 1 MPPT, 1 Phase
|
# 1121-Series Intervers, 1 MPPT, 1 Phase
|
||||||
class HM300_Decode0B(StatusResponse):
|
class Hm300Decode0B(StatusResponse):
|
||||||
def __init__(self, *args, **params):
|
""" 1121-series mirco-inverters status data """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_0(self):
|
def dc_voltage_0(self):
|
||||||
|
""" String 1 VDC """
|
||||||
return self.unpack('>H', 2)[0]/10
|
return self.unpack('>H', 2)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_0(self):
|
def dc_current_0(self):
|
||||||
|
""" String 1 ampere """
|
||||||
return self.unpack('>H', 4)[0]/100
|
return self.unpack('>H', 4)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_0(self):
|
def dc_power_0(self):
|
||||||
|
""" String 1 watts """
|
||||||
return self.unpack('>H', 6)[0]/10
|
return self.unpack('>H', 6)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_0(self):
|
def dc_energy_total_0(self):
|
||||||
|
""" String 1 total energy in Wh """
|
||||||
return self.unpack('>L', 8)[0]
|
return self.unpack('>L', 8)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_0(self):
|
def dc_energy_daily_0(self):
|
||||||
|
""" String 1 daily energy in Wh """
|
||||||
return self.unpack('>H', 12)[0]
|
return self.unpack('>H', 12)[0]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ac_voltage_0(self):
|
def ac_voltage_0(self):
|
||||||
|
""" Phase 1 VAC """
|
||||||
return self.unpack('>H', 14)[0]/10
|
return self.unpack('>H', 14)[0]/10
|
||||||
@property
|
@property
|
||||||
def ac_current_0(self):
|
def ac_current_0(self):
|
||||||
|
""" Phase 1 ampere """
|
||||||
return self.unpack('>H', 22)[0]/100
|
return self.unpack('>H', 22)[0]/100
|
||||||
@property
|
@property
|
||||||
def ac_power_0(self):
|
def ac_power_0(self):
|
||||||
|
""" Phase 1 watts """
|
||||||
return self.unpack('>H', 18)[0]/10
|
return self.unpack('>H', 18)[0]/10
|
||||||
@property
|
@property
|
||||||
def frequency(self):
|
def frequency(self):
|
||||||
|
""" Grid frequency in Hertz """
|
||||||
return self.unpack('>H', 16)[0]/100
|
return self.unpack('>H', 16)[0]/100
|
||||||
@property
|
@property
|
||||||
def temperature(self):
|
def temperature(self):
|
||||||
|
""" Inverter temperature in °C """
|
||||||
return self.unpack('>H', 26)[0]/10
|
return self.unpack('>H', 26)[0]/10
|
||||||
|
|
||||||
class HM300_Decode11(EventsResponse):
|
class Hm300Decode11(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter generic events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
class HM300_Decode12(EventsResponse):
|
class Hm300Decode12(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter major events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
|
|
||||||
# 1141-Series Inverters, 2 MPPT, 1 Phase
|
# 1141-Series Inverters, 2 MPPT, 1 Phase
|
||||||
class HM600_Decode0B(StatusResponse):
|
class Hm600Decode0B(StatusResponse):
|
||||||
def __init__(self, *args, **params):
|
""" 1141-series mirco-inverters status data """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_0(self):
|
def dc_voltage_0(self):
|
||||||
|
""" String 1 VDC """
|
||||||
return self.unpack('>H', 2)[0]/10
|
return self.unpack('>H', 2)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_0(self):
|
def dc_current_0(self):
|
||||||
|
""" String 1 ampere """
|
||||||
return self.unpack('>H', 4)[0]/100
|
return self.unpack('>H', 4)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_0(self):
|
def dc_power_0(self):
|
||||||
|
""" String 1 watts """
|
||||||
return self.unpack('>H', 6)[0]/10
|
return self.unpack('>H', 6)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_0(self):
|
def dc_energy_total_0(self):
|
||||||
|
""" String 1 total energy in Wh """
|
||||||
return self.unpack('>L', 14)[0]
|
return self.unpack('>L', 14)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_0(self):
|
def dc_energy_daily_0(self):
|
||||||
|
""" String 1 daily energy in Wh """
|
||||||
return self.unpack('>H', 22)[0]
|
return self.unpack('>H', 22)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_1(self):
|
def dc_voltage_1(self):
|
||||||
|
""" String 2 VDC """
|
||||||
return self.unpack('>H', 8)[0]/10
|
return self.unpack('>H', 8)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_1(self):
|
def dc_current_1(self):
|
||||||
|
""" String 2 ampere """
|
||||||
return self.unpack('>H', 10)[0]/100
|
return self.unpack('>H', 10)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_1(self):
|
def dc_power_1(self):
|
||||||
|
""" String 2 watts """
|
||||||
return self.unpack('>H', 12)[0]/10
|
return self.unpack('>H', 12)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_1(self):
|
def dc_energy_total_1(self):
|
||||||
|
""" String 2 total energy in Wh """
|
||||||
return self.unpack('>L', 18)[0]
|
return self.unpack('>L', 18)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_1(self):
|
def dc_energy_daily_1(self):
|
||||||
|
""" String 2 daily energy in Wh """
|
||||||
return self.unpack('>H', 24)[0]
|
return self.unpack('>H', 24)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ac_voltage_0(self):
|
def ac_voltage_0(self):
|
||||||
|
""" Phase 1 VAC """
|
||||||
return self.unpack('>H', 26)[0]/10
|
return self.unpack('>H', 26)[0]/10
|
||||||
@property
|
@property
|
||||||
def ac_current_0(self):
|
def ac_current_0(self):
|
||||||
|
""" Phase 1 ampere """
|
||||||
return self.unpack('>H', 34)[0]/10
|
return self.unpack('>H', 34)[0]/10
|
||||||
@property
|
@property
|
||||||
def ac_power_0(self):
|
def ac_power_0(self):
|
||||||
|
""" Phase 1 watts """
|
||||||
return self.unpack('>H', 30)[0]/10
|
return self.unpack('>H', 30)[0]/10
|
||||||
@property
|
@property
|
||||||
def frequency(self):
|
def frequency(self):
|
||||||
|
""" Grid frequency in Hertz """
|
||||||
return self.unpack('>H', 28)[0]/100
|
return self.unpack('>H', 28)[0]/100
|
||||||
@property
|
@property
|
||||||
def temperature(self):
|
def temperature(self):
|
||||||
|
""" Inverter temperature in °C """
|
||||||
return self.unpack('>H', 38)[0]/10
|
return self.unpack('>H', 38)[0]/10
|
||||||
@property
|
@property
|
||||||
def alarm_count(self):
|
def alarm_count(self):
|
||||||
|
""" Event counter """
|
||||||
return self.unpack('>H', 40)[0]
|
return self.unpack('>H', 40)[0]
|
||||||
|
|
||||||
class HM600_Decode11(EventsResponse):
|
class Hm600Decode11(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter generic events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
class HM600_Decode12(EventsResponse):
|
class Hm600Decode12(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter major events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
|
|
||||||
# 1161-Series Inverters, 4 MPPT, 1 Phase
|
# 1161-Series Inverters, 4 MPPT, 1 Phase
|
||||||
class HM1200_Decode0B(StatusResponse):
|
class Hm1200Decode0B(StatusResponse):
|
||||||
def __init__(self, *args, **params):
|
""" 1161-series mirco-inverters status data """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_0(self):
|
def dc_voltage_0(self):
|
||||||
|
""" String 1 VDC """
|
||||||
return self.unpack('>H', 2)[0]/10
|
return self.unpack('>H', 2)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_0(self):
|
def dc_current_0(self):
|
||||||
|
""" String 1 ampere """
|
||||||
return self.unpack('>H', 4)[0]/100
|
return self.unpack('>H', 4)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_0(self):
|
def dc_power_0(self):
|
||||||
|
""" String 1 watts """
|
||||||
return self.unpack('>H', 8)[0]/10
|
return self.unpack('>H', 8)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_0(self):
|
def dc_energy_total_0(self):
|
||||||
|
""" String 1 total energy in Wh """
|
||||||
return self.unpack('>L', 12)[0]
|
return self.unpack('>L', 12)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_0(self):
|
def dc_energy_daily_0(self):
|
||||||
|
""" String 1 daily energy in Wh """
|
||||||
return self.unpack('>H', 20)[0]
|
return self.unpack('>H', 20)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_1(self):
|
def dc_voltage_1(self):
|
||||||
|
""" String 2 VDC """
|
||||||
return self.unpack('>H', 2)[0]/10
|
return self.unpack('>H', 2)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_1(self):
|
def dc_current_1(self):
|
||||||
|
""" String 2 ampere """
|
||||||
return self.unpack('>H', 4)[0]/100
|
return self.unpack('>H', 4)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_1(self):
|
def dc_power_1(self):
|
||||||
|
""" String 2 watts """
|
||||||
return self.unpack('>H', 10)[0]/10
|
return self.unpack('>H', 10)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_1(self):
|
def dc_energy_total_1(self):
|
||||||
|
""" String 2 total energy in Wh """
|
||||||
return self.unpack('>L', 16)[0]
|
return self.unpack('>L', 16)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_1(self):
|
def dc_energy_daily_1(self):
|
||||||
|
""" String 2 daily energy in Wh """
|
||||||
return self.unpack('>H', 22)[0]
|
return self.unpack('>H', 22)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_2(self):
|
def dc_voltage_2(self):
|
||||||
|
""" String 3 VDC """
|
||||||
return self.unpack('>H', 24)[0]/10
|
return self.unpack('>H', 24)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_2(self):
|
def dc_current_2(self):
|
||||||
|
""" String 3 ampere """
|
||||||
return self.unpack('>H', 26)[0]/100
|
return self.unpack('>H', 26)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_2(self):
|
def dc_power_2(self):
|
||||||
|
""" String 3 watts """
|
||||||
return self.unpack('>H', 30)[0]/10
|
return self.unpack('>H', 30)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_2(self):
|
def dc_energy_total_2(self):
|
||||||
|
""" String 3 total energy in Wh """
|
||||||
return self.unpack('>L', 34)[0]
|
return self.unpack('>L', 34)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_2(self):
|
def dc_energy_daily_2(self):
|
||||||
|
""" String 3 daily energy in Wh """
|
||||||
return self.unpack('>H', 42)[0]
|
return self.unpack('>H', 42)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dc_voltage_3(self):
|
def dc_voltage_3(self):
|
||||||
|
""" String 4 VDC """
|
||||||
return self.unpack('>H', 24)[0]/10
|
return self.unpack('>H', 24)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_current_3(self):
|
def dc_current_3(self):
|
||||||
|
""" String 4 ampere """
|
||||||
return self.unpack('>H', 28)[0]/100
|
return self.unpack('>H', 28)[0]/100
|
||||||
@property
|
@property
|
||||||
def dc_power_3(self):
|
def dc_power_3(self):
|
||||||
|
""" String 4 watts """
|
||||||
return self.unpack('>H', 32)[0]/10
|
return self.unpack('>H', 32)[0]/10
|
||||||
@property
|
@property
|
||||||
def dc_energy_total_3(self):
|
def dc_energy_total_3(self):
|
||||||
|
""" String 4 total energy in Wh """
|
||||||
return self.unpack('>L', 38)[0]
|
return self.unpack('>L', 38)[0]
|
||||||
@property
|
@property
|
||||||
def dc_energy_daily_3(self):
|
def dc_energy_daily_3(self):
|
||||||
|
""" String 4 daily energy in Wh """
|
||||||
return self.unpack('>H', 44)[0]
|
return self.unpack('>H', 44)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ac_voltage_0(self):
|
def ac_voltage_0(self):
|
||||||
|
""" Phase 1 VAC """
|
||||||
return self.unpack('>H', 46)[0]/10
|
return self.unpack('>H', 46)[0]/10
|
||||||
@property
|
@property
|
||||||
def ac_current_0(self):
|
def ac_current_0(self):
|
||||||
|
""" Phase 1 ampere """
|
||||||
return self.unpack('>H', 54)[0]/100
|
return self.unpack('>H', 54)[0]/100
|
||||||
@property
|
@property
|
||||||
def ac_power_0(self):
|
def ac_power_0(self):
|
||||||
|
""" Phase 1 watts """
|
||||||
return self.unpack('>H', 50)[0]/10
|
return self.unpack('>H', 50)[0]/10
|
||||||
@property
|
@property
|
||||||
def frequency(self):
|
def frequency(self):
|
||||||
|
""" Grid frequency in Hertz """
|
||||||
return self.unpack('>H', 48)[0]/100
|
return self.unpack('>H', 48)[0]/100
|
||||||
@property
|
@property
|
||||||
def temperature(self):
|
def temperature(self):
|
||||||
|
""" Inverter temperature in °C """
|
||||||
return self.unpack('>H', 58)[0]/10
|
return self.unpack('>H', 58)[0]/10
|
||||||
|
|
||||||
class HM1200_Decode11(EventsResponse):
|
class Hm1200Decode11(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter generic events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
||||||
class HM1200_Decode12(EventsResponse):
|
class Hm1200Decode12(EventsResponse):
|
||||||
def __init__(self, *args, **params):
|
""" Inverter major events log """
|
||||||
super().__init__(*args, **params)
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hoymiles output plugin library
|
||||||
|
"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
@ -11,15 +16,46 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
class OutputPluginFactory:
|
class OutputPluginFactory:
|
||||||
def __init__(self, **params):
|
def __init__(self, **params):
|
||||||
"""Initialize output plugin"""
|
"""
|
||||||
|
Initialize output plugin
|
||||||
|
|
||||||
self.inverter_ser = params.get('inverter_ser', 0)
|
:param inverter_ser: The inverter serial
|
||||||
|
:type inverter_ser: str
|
||||||
|
:param inverter_name: The configured name for the inverter
|
||||||
|
:type inverter_name: str
|
||||||
|
"""
|
||||||
|
|
||||||
def store_status(self, data):
|
self.inverter_ser = params.get('inverter_ser', '')
|
||||||
|
self.inverter_name = params.get('inverter_name', None)
|
||||||
|
|
||||||
|
def store_status(self, response, **params):
|
||||||
|
"""
|
||||||
|
Default function
|
||||||
|
|
||||||
|
:raises NotImplementedError: when the plugin does not implement store status data
|
||||||
|
"""
|
||||||
raise NotImplementedError('The current output plugin does not implement store_status')
|
raise NotImplementedError('The current output plugin does not implement store_status')
|
||||||
|
|
||||||
class InfluxOutputPlugin(OutputPluginFactory):
|
class InfluxOutputPlugin(OutputPluginFactory):
|
||||||
|
""" Influx2 output plugin """
|
||||||
|
api = None
|
||||||
|
|
||||||
def __init__(self, url, token, **params):
|
def __init__(self, url, token, **params):
|
||||||
|
"""
|
||||||
|
Initialize InfluxOutputPlugin
|
||||||
|
|
||||||
|
The following targets must be present in your InfluxDB. This does not
|
||||||
|
automatically create anything for You.
|
||||||
|
|
||||||
|
:param str url: The url to connect this client to. Like http://localhost:8086
|
||||||
|
:param str token: Influx2 access token which is allowed to write to bucket
|
||||||
|
:param org: Influx2 org, the token belongs to
|
||||||
|
:type org: str
|
||||||
|
:param bucket: Influx2 bucket to store data in (also known as retention policy)
|
||||||
|
:type bucket: str
|
||||||
|
:param measurement: Default measurement-prefix to use
|
||||||
|
:type measurement: str
|
||||||
|
"""
|
||||||
super().__init__(**params)
|
super().__init__(**params)
|
||||||
|
|
||||||
self._bucket = params.get('bucket', 'hoymiles/autogen')
|
self._bucket = params.get('bucket', 'hoymiles/autogen')
|
||||||
|
@ -30,20 +66,20 @@ class InfluxOutputPlugin(OutputPluginFactory):
|
||||||
client = InfluxDBClient(url, token, bucket=self._bucket)
|
client = InfluxDBClient(url, token, bucket=self._bucket)
|
||||||
self.api = client.write_api()
|
self.api = client.write_api()
|
||||||
|
|
||||||
def store_status(self, response):
|
def store_status(self, response, **params):
|
||||||
"""
|
"""
|
||||||
Publish StatusResponse object
|
Publish StatusResponse object
|
||||||
|
|
||||||
:param influxdb.InfluxDBClient influx_client: A connected instance to Influx database
|
:param hoymiles.decoders.StatusResponse response: StatusResponse object
|
||||||
:param str inverter_ser: inverter serial
|
:type response: hoymiles.decoders.StatusResponse
|
||||||
:param hoymiles.StatusResponse data: decoded inverter StatusResponse
|
:param measurement: Custom influx measurement name
|
||||||
:type response: hoymiles.StatusResponse
|
:type measurement: str or None
|
||||||
:param measurement: Influx measurement name
|
|
||||||
:type measurement: str
|
:raises ValueError: when response is not instance of StatusResponse
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(response, StatusResponse):
|
if not isinstance(response, StatusResponse):
|
||||||
raise RuntimeError('Data needs to be instance of StatusResponse')
|
raise ValueError('Data needs to be instance of StatusResponse')
|
||||||
|
|
||||||
data = response.__dict__()
|
data = response.__dict__()
|
||||||
|
|
||||||
|
@ -89,7 +125,30 @@ except ModuleNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class MqttOutputPlugin(OutputPluginFactory):
|
class MqttOutputPlugin(OutputPluginFactory):
|
||||||
|
""" Mqtt output plugin """
|
||||||
|
client = None
|
||||||
|
|
||||||
def __init__(self, *args, **params):
|
def __init__(self, *args, **params):
|
||||||
|
"""
|
||||||
|
Initialize MqttOutputPlugin
|
||||||
|
|
||||||
|
:param host: Broker ip or hostname (defaults to: 127.0.0.1)
|
||||||
|
:type host: str
|
||||||
|
:param port: Broker port
|
||||||
|
:type port: int (defaults to: 1883)
|
||||||
|
:param user: Optional username to login to the broker
|
||||||
|
:type user: str or None
|
||||||
|
:param password: Optional passwort to login to the broker
|
||||||
|
:type password: str or None
|
||||||
|
:param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser})
|
||||||
|
:type topic: str
|
||||||
|
|
||||||
|
:param paho.mqtt.client.Client broker: mqtt-client instance
|
||||||
|
:param str inverter_ser: inverter serial
|
||||||
|
:param hoymiles.StatusResponse data: decoded inverter StatusResponse
|
||||||
|
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
|
||||||
|
:type topic: str
|
||||||
|
"""
|
||||||
super().__init__(*args, **params)
|
super().__init__(*args, **params)
|
||||||
|
|
||||||
mqtt_client = paho.mqtt.client.Client()
|
mqtt_client = paho.mqtt.client.Client()
|
||||||
|
@ -103,36 +162,36 @@ class MqttOutputPlugin(OutputPluginFactory):
|
||||||
"""
|
"""
|
||||||
Publish StatusResponse object
|
Publish StatusResponse object
|
||||||
|
|
||||||
:param paho.mqtt.client.Client broker: mqtt-client instance
|
:param hoymiles.decoders.StatusResponse response: StatusResponse object
|
||||||
:param str inverter_ser: inverter serial
|
|
||||||
:param hoymiles.StatusResponse data: decoded inverter StatusResponse
|
|
||||||
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
|
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
|
||||||
:type topic: str
|
:type topic: str
|
||||||
|
|
||||||
|
:raises ValueError: when response is not instance of StatusResponse
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(response, StatusResponse):
|
if not isinstance(response, StatusResponse):
|
||||||
raise RuntimeError('Data needs to be instance of StatusResponse')
|
raise ValueError('Data needs to be instance of StatusResponse')
|
||||||
|
|
||||||
data = response.__dict__()
|
data = response.__dict__()
|
||||||
|
|
||||||
topic = params.get('topic', f'hoymiles/{inverter_ser}')
|
topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}')
|
||||||
|
|
||||||
# AC Data
|
# AC Data
|
||||||
phase_id = 0
|
phase_id = 0
|
||||||
for phase in data['phases']:
|
for phase in data['phases']:
|
||||||
self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
|
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
|
||||||
self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
|
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
|
||||||
self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
|
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
|
||||||
phase_id = phase_id + 1
|
phase_id = phase_id + 1
|
||||||
|
|
||||||
# DC Data
|
# DC Data
|
||||||
string_id = 0
|
string_id = 0
|
||||||
for string in data['strings']:
|
for string in data['strings']:
|
||||||
self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
|
self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
|
||||||
self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
|
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
|
||||||
self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
|
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
|
||||||
self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
|
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
|
||||||
string_id = string_id + 1
|
string_id = string_id + 1
|
||||||
# Global
|
# Global
|
||||||
self.mqtt_client.publish(f'{topic}/frequency', data['frequency'])
|
self.client.publish(f'{topic}/frequency', data['frequency'])
|
||||||
self.mqtt_client.publish(f'{topic}/temperature', data['temperature'])
|
self.client.publish(f'{topic}/temperature', data['temperature'])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue