|
|
@ -10,6 +10,52 @@ from datetime import datetime, timedelta |
|
|
|
import crcmod |
|
|
|
|
|
|
|
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') |
|
|
|
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) |
|
|
|
|
|
|
|
def g_unpack(s_fmt, s_buf): |
|
|
|
"""Chunk unpack helper |
|
|
|
|
|
|
|
:param s_fmt: struct format string |
|
|
|
:type s_fmt: str |
|
|
|
:param s_buf: buffer to unpack |
|
|
|
:type s_buf: bytes |
|
|
|
:return: decoded data iterator |
|
|
|
:rtype: generator object |
|
|
|
""" |
|
|
|
|
|
|
|
cs = struct.calcsize(s_fmt) |
|
|
|
s_exc = len(s_buf) % cs |
|
|
|
|
|
|
|
return struct.iter_unpack(s_fmt, s_buf[:len(s_buf) - s_exc]) |
|
|
|
|
|
|
|
def print_table_unpack(s_fmt, payload, cw=6): |
|
|
|
""" |
|
|
|
Print table of decoded numbers with different offsets |
|
|
|
Helps recognizing values in unknown payloads |
|
|
|
|
|
|
|
:param s_fmt: struct format string |
|
|
|
:type s_fmt: str |
|
|
|
:param payload: bytes data |
|
|
|
:type payload: bytes |
|
|
|
:param cw: cell width |
|
|
|
:type cw: int |
|
|
|
:return: None |
|
|
|
""" |
|
|
|
|
|
|
|
l_hexlified = [f'{byte:02x}' for byte in payload] |
|
|
|
|
|
|
|
print(f'{"Pos": <{cw}}', end='') |
|
|
|
print(''.join([f'{num: >{cw}}' for num in range(0, len(payload))])) |
|
|
|
print(f'{"Hex": <{cw}}', end='') |
|
|
|
print(''.join([f'{byte: >{cw}}' for byte in l_hexlified])) |
|
|
|
|
|
|
|
l_fmt = struct.calcsize(s_fmt) |
|
|
|
if len(payload) >= l_fmt: |
|
|
|
for offset in range(0, l_fmt): |
|
|
|
print(f'{s_fmt: <{cw}}', end='') |
|
|
|
print(' ' * cw * offset, end='') |
|
|
|
print(''.join( |
|
|
|
[f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])])) |
|
|
|
|
|
|
|
class Response: |
|
|
|
""" All Response Shared methods """ |
|
|
@ -140,8 +186,18 @@ class UnknownResponse(Response): |
|
|
|
""" |
|
|
|
return ' '.join([f'{byte:02x}' for byte in self.response]) |
|
|
|
|
|
|
|
@property |
|
|
|
def valid_crc(self): |
|
|
|
def validate_crc8(self): |
|
|
|
""" |
|
|
|
Checks if self.response has valid CRC8 |
|
|
|
|
|
|
|
:return: if crc is available and correct |
|
|
|
:rtype: bool |
|
|
|
""" |
|
|
|
# check crc |
|
|
|
pcrc = struct.unpack('>B', self.response[-1:])[0] |
|
|
|
return f_crc8(self.response[:-1]) == pcrc |
|
|
|
|
|
|
|
def validate_crc_m(self): |
|
|
|
""" |
|
|
|
Checks if self.response has valid Modbus CRC |
|
|
|
|
|
|
@ -152,113 +208,10 @@ class UnknownResponse(Response): |
|
|
|
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) < 3: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response |
|
|
|
|
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
@property |
|
|
|
def dump_longs_pad1(self): |
|
|
|
"""Get all data, interpreted as long""" |
|
|
|
if len(self.response) < 5: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response[2:] |
|
|
|
|
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
@property |
|
|
|
def dump_longs_pad2(self): |
|
|
|
"""Get all data, interpreted as long""" |
|
|
|
if len(self.response) < 7: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response[4:] |
|
|
|
|
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
def unpack_table(self, *args): |
|
|
|
"""Access shared debug function""" |
|
|
|
print_table_unpack(*args) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
@property |
|
|
|
def dump_longs_pad3(self): |
|
|
|
"""Get all data, interpreted as long""" |
|
|
|
if len(self.response) < 9: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response[6:] |
|
|
|
|
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
@property |
|
|
|
def dump_shorts(self): |
|
|
|
"""Get all data, interpreted as short""" |
|
|
|
if len(self.response) < 3: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response |
|
|
|
|
|
|
|
rem = len(res) % 4 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 4 == 0: |
|
|
|
rlen = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}H', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
@property |
|
|
|
def dump_shorts_pad1(self): |
|
|
|
"""Get all data, interpreted as short""" |
|
|
|
if len(self.response) < 4: |
|
|
|
return None |
|
|
|
|
|
|
|
res = self.response[1:] |
|
|
|
|
|
|
|
rem = len(res) % 4 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 4 == 0: |
|
|
|
rlen = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}H', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
class EventsResponse(UnknownResponse): |
|
|
|
""" Hoymiles micro-inverter event log decode helper """ |
|
|
@ -337,7 +290,7 @@ class EventsResponse(UnknownResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
|
|
|
|
crc_valid = self.valid_crc |
|
|
|
crc_valid = self.validate_crc_m() |
|
|
|
if crc_valid: |
|
|
|
print(' payload has valid modbus crc') |
|
|
|
self.response = self.response[:-2] |
|
|
@ -365,7 +318,12 @@ class DebugDecodeAny(UnknownResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
|
|
|
|
crc_valid = self.valid_crc |
|
|
|
crc8_valid = self.validate_crc8() |
|
|
|
if crc8_valid: |
|
|
|
print(' payload has valid crc8') |
|
|
|
self.response = self.response[:-1] |
|
|
|
|
|
|
|
crc_valid = self.validate_crc_m() |
|
|
|
if crc_valid: |
|
|
|
print(' payload has valid modbus crc') |
|
|
|
self.response = self.response[:-2] |
|
|
@ -373,41 +331,17 @@ class DebugDecodeAny(UnknownResponse): |
|
|
|
l_payload = len(self.response) |
|
|
|
print(f' payload has {l_payload} bytes') |
|
|
|
|
|
|
|
longs = self.dump_longs |
|
|
|
if not longs: |
|
|
|
print(' type long : unable to decode (len or not mod 4)') |
|
|
|
else: |
|
|
|
print(' type long : ' + str(longs)) |
|
|
|
print() |
|
|
|
print('Field view: int') |
|
|
|
print_table_unpack('>B', self.response) |
|
|
|
|
|
|
|
longs = self.dump_longs_pad1 |
|
|
|
if not longs: |
|
|
|
print(' type long pad1 : unable to decode (len or not mod 4)') |
|
|
|
else: |
|
|
|
print(' type long pad1 : ' + str(longs)) |
|
|
|
print() |
|
|
|
print('Field view: shorts') |
|
|
|
print_table_unpack('>H', self.response) |
|
|
|
|
|
|
|
longs = self.dump_longs_pad2 |
|
|
|
if not longs: |
|
|
|
print(' type long pad2 : unable to decode (len or not mod 4)') |
|
|
|
else: |
|
|
|
print(' type long pad2 : ' + str(longs)) |
|
|
|
|
|
|
|
longs = self.dump_longs_pad3 |
|
|
|
if not longs: |
|
|
|
print(' type long pad3 : unable to decode (len or not mod 4)') |
|
|
|
else: |
|
|
|
print(' type long pad3 : ' + str(longs)) |
|
|
|
|
|
|
|
shorts = self.dump_shorts |
|
|
|
if not shorts: |
|
|
|
print(' type short : unable to decode (len or not mod 2)') |
|
|
|
else: |
|
|
|
print(' type short : ' + str(shorts)) |
|
|
|
|
|
|
|
shorts = self.dump_shorts_pad1 |
|
|
|
if not shorts: |
|
|
|
print(' type short pad1: unable to decode (len or not mod 2)') |
|
|
|
else: |
|
|
|
print(' type short pad1: ' + str(shorts)) |
|
|
|
print() |
|
|
|
print('Field view: longs') |
|
|
|
print_table_unpack('>L', self.response) |
|
|
|
|
|
|
|
try: |
|
|
|
if len(self.response) > 2: |
|
|
@ -423,6 +357,9 @@ class DebugDecodeAny(UnknownResponse): |
|
|
|
|
|
|
|
|
|
|
|
# 1121-Series Intervers, 1 MPPT, 1 Phase |
|
|
|
class Hm300Decode02(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class Hm300Decode0B(StatusResponse): |
|
|
|
""" 1121-series mirco-inverters status data """ |
|
|
|
|
|
|
@ -476,6 +413,9 @@ class Hm300Decode12(EventsResponse): |
|
|
|
|
|
|
|
|
|
|
|
# 1141-Series Inverters, 2 MPPT, 1 Phase |
|
|
|
class Hm600Decode02(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class Hm600Decode0B(StatusResponse): |
|
|
|
""" 1141-series mirco-inverters status data """ |
|
|
|
|
|
|
@ -558,6 +498,9 @@ class Hm600Decode12(EventsResponse): |
|
|
|
|
|
|
|
|
|
|
|
# 1161-Series Inverters, 2 MPPT, 1 Phase |
|
|
|
class Hm1200Decode02(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class Hm1200Decode0B(StatusResponse): |
|
|
|
""" 1161-series mirco-inverters status data """ |
|
|
|
|
|
|
|