Browse Source

WIP: Partially decode event log 0x11 and 0x12

pull/27/head
Jan-Jonas Sämann 3 years ago
parent
commit
3aff763e8f
  1. 180
      tools/rpi/hoymiles/decoders/__init__.py

180
tools/rpi/hoymiles/decoders/__init__.py

@ -2,18 +2,34 @@
# -*- coding: utf-8 -*-
import struct
import crcmod
from datetime import timedelta
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
class StatusResponse:
"""Inverter StatusResponse object"""
e_keys = ['voltage','current','power','energy_total','energy_daily']
def unpack(self, fmt, base):
"""
Data unpack helper
:param str fmt: struct format string
:param int base: unpack base position from self.response bytes
:return: unpacked values
:rtype: tuple
"""
size = struct.calcsize(fmt)
return struct.unpack(fmt, self.response[base:base+size])
@property
def phases(self):
"""
AC power data
:retrun: list of dict's
:rtype: list
"""
phases = []
p_exists = True
while p_exists:
@ -32,6 +48,12 @@ class StatusResponse:
@property
def strings(self):
"""
DC PV-string data
:retrun: list of dict's
:rtype: list
"""
strings = []
s_exists = True
while s_exists:
@ -49,6 +71,12 @@ class StatusResponse:
return strings
def __dict__(self):
"""
Get all known data
:return: dict of properties
:rtype: dict
"""
data = {}
data['phases'] = self.phases
data['strings'] = self.strings
@ -57,18 +85,34 @@ class StatusResponse:
return data
class UnknownResponse:
"""
Debugging helper for unknown payload format
"""
@property
def hex_ascii(self):
"""
Generate white-space separated byte representation
:return: hexlifierd byte string
:rtype: str
"""
return ' '.join([f'{b:02x}' for b in self.response])
@property
def valid_crc(self):
"""
Checks if self.response has valid Modbus CRC
:return: if crc is available and correct
:rtype: bool
"""
# check crc
pcrc = struct.unpack('>H', self.response[-2:])[0]
return f_crc_m(self.response[:-2]) == pcrc
@property
def dump_longs(self):
"""Get all data, interpreted as long"""
if len(self.response) < 5:
return None
@ -86,6 +130,7 @@ class UnknownResponse:
@property
def dump_longs_pad1(self):
"""Get all data, interpreted as long"""
if len(self.response) < 7:
return None
@ -103,6 +148,7 @@ class UnknownResponse:
@property
def dump_longs_pad2(self):
"""Get all data, interpreted as long"""
if len(self.response) < 9:
return None
@ -120,6 +166,7 @@ class UnknownResponse:
@property
def dump_longs_pad3(self):
"""Get all data, interpreted as long"""
if len(self.response) < 11:
return None
@ -137,6 +184,7 @@ class UnknownResponse:
@property
def dump_shorts(self):
"""Get all data, interpreted as short"""
if len(self.response) < 5:
return None
@ -154,6 +202,7 @@ class UnknownResponse:
@property
def dump_shorts_pad1(self):
"""Get all data, interpreted as short"""
if len(self.response) < 6:
return None
@ -169,7 +218,79 @@ class UnknownResponse:
return vals
class HM600_Decode11(UnknownResponse):
class EventsResponse(UnknownResponse):
alarm_codes = {
1: 'Inverter start',
2: 'Producing power',
121: 'Over temperature protection',
125: 'Grid configuration parameter error',
126: 'Software error code 126',
127: 'Firmware error',
128: 'Software error code 128',
129: 'Software error code 129',
130: 'Offline',
141: 'Grid overvoltage',
142: 'Average grid overvoltage',
143: 'Grid undervoltage',
144: 'Grid overfrequency',
145: 'Grid underfrequency',
146: 'Rapid grid frequency change',
147: 'Power grid outage',
148: 'Grid disconnection',
149: 'Island detected',
205: 'Input port 1 & 2 overvoltage',
206: 'Input port 3 & 4 overvoltage',
207: 'Input port 1 & 2 undervoltage',
208: 'Input port 3 & 4 undervoltage',
209: 'Port 1 no input',
210: 'Port 2 no input',
211: 'Port 3 no input',
212: 'Port 4 no input',
213: 'PV-1 & PV-2 abnormal wiring',
214: 'PV-3 & PV-4 abnormal wiring',
215: 'PV-1 Input overvoltage',
216: 'PV-1 Input undervoltage',
217: 'PV-2 Input overvoltage',
218: 'PV-2 Input undervoltage',
219: 'PV-3 Input overvoltage',
220: 'PV-3 Input undervoltage',
221: 'PV-4 Input overvoltage',
222: 'PV-4 Input undervoltage',
301: 'Hardware error code 301',
302: 'Hardware error code 302',
303: 'Hardware error code 303',
304: 'Hardware error code 304',
305: 'Hardware error code 305',
306: 'Hardware error code 306',
307: 'Hardware error code 307',
308: 'Hardware error code 308',
309: 'Hardware error code 309',
310: 'Hardware error code 310',
311: 'Hardware error code 311',
312: 'Hardware error code 312',
313: 'Hardware error code 313',
314: 'Hardware error code 314',
5041: 'Error code-04 Port 1',
5042: 'Error code-04 Port 2',
5043: 'Error code-04 Port 3',
5044: 'Error code-04 Port 4',
5051: 'PV Input 1 Overvoltage/Undervoltage',
5052: 'PV Input 2 Overvoltage/Undervoltage',
5053: 'PV Input 3 Overvoltage/Undervoltage',
5054: 'PV Input 4 Overvoltage/Undervoltage',
5060: 'Abnormal bias',
5070: 'Over temperature protection',
5080: 'Grid Overvoltage/Undervoltage',
5090: 'Grid Overfrequency/Underfrequency',
5100: 'Island detected',
5120: 'EEPROM reading and writing error',
5150: '10 min value grid overvoltage',
5200: 'Firmware error',
8310: 'Shut down',
9000: 'Microinverter is suspected of being stolen'
}
def __init__(self, response):
self.response = response
@ -183,16 +304,26 @@ class HM600_Decode11(UnknownResponse):
chunk_size = 12
for c in range(2, len(self.response), chunk_size):
chunk = self.response[c:c+chunk_size]
print(' '.join([f'{b:02x}' for b in chunk]) + ': ')
print(' BBLHl : ' + str(struct.unpack('>BBLHl', chunk)))
print()
class HM600_Decode12(HM600_Decode11):
def __init__(self, response):
super().__init__(response)
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
a_text = self.alarm_codes.get(a_code, 'N/A')
print(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
for fmt in ['BBHHHHH']:
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
print(end='', flush=True)
class DEBUG_DecodeAny(UnknownResponse):
"""Default decoder"""
def __init__(self, response):
"""
Try interpret and print unknown response data
:param bytes response: response payload bytes
"""
self.response = response
crc_valid = self.valid_crc
@ -239,6 +370,18 @@ class DEBUG_DecodeAny(UnknownResponse):
else:
print(' type short pad1: ' + str(shorts))
try:
if len(self.response) > 2:
print(' type utf-8 : ' + self.response.decode('utf-8'))
except UnicodeDecodeError:
print(' type utf-8 : utf-8 decode error')
try:
if len(self.response) > 2:
print(' type ascii : ' + self.response.decode('ascii'))
except UnicodeDecodeError:
print(' type ascii : ascii decode error')
# 1121-Series Intervers, 1 MPPT, 1 Phase
class HM300_Decode0B(StatusResponse):
@ -278,6 +421,15 @@ class HM300_Decode0B(StatusResponse):
def temperature(self):
return self.unpack('>H', 26)[0]/10
class HM300_Decode11(EventsResponse):
def __init__(self, response):
super().__init__(response)
class HM300_Decode12(EventsResponse):
def __init__(self, response):
super().__init__(response)
# 1141-Series Inverters, 2 MPPT, 1 Phase
class HM600_Decode0B(StatusResponse):
@ -332,9 +484,13 @@ class HM600_Decode0B(StatusResponse):
def temperature(self):
return self.unpack('>H', 38)[0]/10
class HM600_Decode0C(HM600_Decode0B):
class HM600_Decode11(EventsResponse):
def __init__(self, response):
self.response = response
super().__init__(response)
class HM600_Decode12(EventsResponse):
def __init__(self, response):
super().__init__(response)
# 1161-Series Inverters, 4 MPPT, 1 Phase
@ -421,3 +577,11 @@ class HM1200_Decode0B(StatusResponse):
@property
def temperature(self):
return self.unpack('>H', 58)[0]/10
class HM1200_Decode11(EventsResponse):
def __init__(self, response):
super().__init__(response)
class HM1200_Decode12(EventsResponse):
def __init__(self, response):
super().__init__(response)

Loading…
Cancel
Save