From 3aff763e8f7c6a5c8bf55dd2e70ec130a8aab26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Wed, 11 May 2022 18:58:09 +0200 Subject: [PATCH] WIP: Partially decode event log 0x11 and 0x12 --- tools/rpi/hoymiles/decoders/__init__.py | 180 ++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 8 deletions(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 6836e73e..68bef7f5 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/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)