|
|
@ -1,12 +1,18 @@ |
|
|
|
#!/usr/bin/python3 |
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
|
|
|
""" |
|
|
|
Hoymiles Micro-Inverters decoder library |
|
|
|
""" |
|
|
|
|
|
|
|
import struct |
|
|
|
import crcmod |
|
|
|
from datetime import datetime, timedelta |
|
|
|
import crcmod |
|
|
|
|
|
|
|
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') |
|
|
|
|
|
|
|
class Response: |
|
|
|
""" All Response Shared methods """ |
|
|
|
inverter_ser = None |
|
|
|
inverter_name = None |
|
|
|
dtu_ser = None |
|
|
@ -28,6 +34,7 @@ class Response: |
|
|
|
self.time_rx = datetime.now() |
|
|
|
|
|
|
|
def __dict__(self): |
|
|
|
""" Base values, availabe in each __dict__ call """ |
|
|
|
return { |
|
|
|
'inverter_ser': self.inverter_ser, |
|
|
|
'inverter_name': self.inverter_name, |
|
|
@ -36,9 +43,8 @@ class Response: |
|
|
|
class StatusResponse(Response): |
|
|
|
"""Inverter StatusResponse object""" |
|
|
|
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] |
|
|
|
|
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
temperature = None |
|
|
|
frequency = None |
|
|
|
|
|
|
|
def unpack(self, fmt, base): |
|
|
|
""" |
|
|
@ -120,9 +126,6 @@ class UnknownResponse(Response): |
|
|
|
Debugging helper for unknown payload format |
|
|
|
""" |
|
|
|
|
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
|
|
|
|
@property |
|
|
|
def hex_ascii(self): |
|
|
|
""" |
|
|
@ -131,7 +134,7 @@ class UnknownResponse(Response): |
|
|
|
:return: hexlifierd byte string |
|
|
|
:rtype: str |
|
|
|
""" |
|
|
|
return ' '.join([f'{b:02x}' for b in self.response]) |
|
|
|
return ' '.join([f'{byte:02x}' for byte in self.response]) |
|
|
|
|
|
|
|
@property |
|
|
|
def valid_crc(self): |
|
|
@ -153,13 +156,13 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response |
|
|
|
|
|
|
|
r = len(res) % 16 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
n = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(n)}L', res) |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
@ -171,13 +174,13 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response[2:] |
|
|
|
|
|
|
|
r = len(res) % 16 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
n = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(n)}L', res) |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
@ -189,13 +192,13 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response[4:] |
|
|
|
|
|
|
|
r = len(res) % 16 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
n = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(n)}L', res) |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
@ -207,13 +210,13 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response[6:] |
|
|
|
|
|
|
|
r = len(res) % 16 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 16 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 16 == 0: |
|
|
|
n = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(n)}L', res) |
|
|
|
rlen = len(res)/4 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}L', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
@ -225,13 +228,13 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response |
|
|
|
|
|
|
|
r = len(res) % 4 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 4 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 4 == 0: |
|
|
|
n = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(n)}H', res) |
|
|
|
rlen = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}H', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
@ -243,17 +246,18 @@ class UnknownResponse(Response): |
|
|
|
|
|
|
|
res = self.response[1:] |
|
|
|
|
|
|
|
r = len(res) % 4 |
|
|
|
res = res[:r*-1] |
|
|
|
rem = len(res) % 4 |
|
|
|
res = res[:rem*-1] |
|
|
|
|
|
|
|
vals = None |
|
|
|
if len(res) % 4 == 0: |
|
|
|
n = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(n)}H', res) |
|
|
|
rlen = len(res)/2 |
|
|
|
vals = struct.unpack(f'>{int(rlen)}H', res) |
|
|
|
|
|
|
|
return vals |
|
|
|
|
|
|
|
class EventsResponse(UnknownResponse): |
|
|
|
""" Hoymiles micro-inverter event log decode helper """ |
|
|
|
|
|
|
|
alarm_codes = { |
|
|
|
1: 'Inverter start', |
|
|
@ -337,10 +341,10 @@ class EventsResponse(UnknownResponse): |
|
|
|
status = self.response[:2] |
|
|
|
|
|
|
|
chunk_size = 12 |
|
|
|
for c in range(2, len(self.response), chunk_size): |
|
|
|
chunk = self.response[c:c+chunk_size] |
|
|
|
for i_chunk in range(2, len(self.response), 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]) |
|
|
|
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(end='', flush=True) |
|
|
|
|
|
|
|
class DEBUG_DecodeAny(UnknownResponse): |
|
|
|
class DebugDecodeAny(UnknownResponse): |
|
|
|
"""Default decoder""" |
|
|
|
|
|
|
|
def __init__(self, *args, **params): |
|
|
@ -415,206 +419,247 @@ class DEBUG_DecodeAny(UnknownResponse): |
|
|
|
|
|
|
|
|
|
|
|
# 1121-Series Intervers, 1 MPPT, 1 Phase |
|
|
|
class HM300_Decode0B(StatusResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm300Decode0B(StatusResponse): |
|
|
|
""" 1121-series mirco-inverters status data """ |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_0(self): |
|
|
|
""" String 1 VDC """ |
|
|
|
return self.unpack('>H', 2)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_0(self): |
|
|
|
""" String 1 ampere """ |
|
|
|
return self.unpack('>H', 4)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_0(self): |
|
|
|
""" String 1 watts """ |
|
|
|
return self.unpack('>H', 6)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_0(self): |
|
|
|
""" String 1 total energy in Wh """ |
|
|
|
return self.unpack('>L', 8)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_0(self): |
|
|
|
""" String 1 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 12)[0] |
|
|
|
|
|
|
|
|
|
|
|
@property |
|
|
|
def ac_voltage_0(self): |
|
|
|
""" Phase 1 VAC """ |
|
|
|
return self.unpack('>H', 14)[0]/10 |
|
|
|
@property |
|
|
|
def ac_current_0(self): |
|
|
|
""" Phase 1 ampere """ |
|
|
|
return self.unpack('>H', 22)[0]/100 |
|
|
|
@property |
|
|
|
def ac_power_0(self): |
|
|
|
""" Phase 1 watts """ |
|
|
|
return self.unpack('>H', 18)[0]/10 |
|
|
|
@property |
|
|
|
def frequency(self): |
|
|
|
""" Grid frequency in Hertz """ |
|
|
|
return self.unpack('>H', 16)[0]/100 |
|
|
|
@property |
|
|
|
def temperature(self): |
|
|
|
""" Inverter temperature in °C """ |
|
|
|
return self.unpack('>H', 26)[0]/10 |
|
|
|
|
|
|
|
class HM300_Decode11(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm300Decode11(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class HM300_Decode12(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm300Decode12(EventsResponse): |
|
|
|
""" Inverter major events log """ |
|
|
|
|
|
|
|
|
|
|
|
# 1141-Series Inverters, 2 MPPT, 1 Phase |
|
|
|
class HM600_Decode0B(StatusResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm600Decode0B(StatusResponse): |
|
|
|
""" 1141-series mirco-inverters status data """ |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_0(self): |
|
|
|
""" String 1 VDC """ |
|
|
|
return self.unpack('>H', 2)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_0(self): |
|
|
|
""" String 1 ampere """ |
|
|
|
return self.unpack('>H', 4)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_0(self): |
|
|
|
""" String 1 watts """ |
|
|
|
return self.unpack('>H', 6)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_0(self): |
|
|
|
""" String 1 total energy in Wh """ |
|
|
|
return self.unpack('>L', 14)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_0(self): |
|
|
|
""" String 1 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 22)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_1(self): |
|
|
|
""" String 2 VDC """ |
|
|
|
return self.unpack('>H', 8)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_1(self): |
|
|
|
""" String 2 ampere """ |
|
|
|
return self.unpack('>H', 10)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_1(self): |
|
|
|
""" String 2 watts """ |
|
|
|
return self.unpack('>H', 12)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_1(self): |
|
|
|
""" String 2 total energy in Wh """ |
|
|
|
return self.unpack('>L', 18)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_1(self): |
|
|
|
""" String 2 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 24)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def ac_voltage_0(self): |
|
|
|
""" Phase 1 VAC """ |
|
|
|
return self.unpack('>H', 26)[0]/10 |
|
|
|
@property |
|
|
|
def ac_current_0(self): |
|
|
|
""" Phase 1 ampere """ |
|
|
|
return self.unpack('>H', 34)[0]/10 |
|
|
|
@property |
|
|
|
def ac_power_0(self): |
|
|
|
""" Phase 1 watts """ |
|
|
|
return self.unpack('>H', 30)[0]/10 |
|
|
|
@property |
|
|
|
def frequency(self): |
|
|
|
""" Grid frequency in Hertz """ |
|
|
|
return self.unpack('>H', 28)[0]/100 |
|
|
|
@property |
|
|
|
def temperature(self): |
|
|
|
""" Inverter temperature in °C """ |
|
|
|
return self.unpack('>H', 38)[0]/10 |
|
|
|
@property |
|
|
|
def alarm_count(self): |
|
|
|
""" Event counter """ |
|
|
|
return self.unpack('>H', 40)[0] |
|
|
|
|
|
|
|
class HM600_Decode11(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm600Decode11(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class HM600_Decode12(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm600Decode12(EventsResponse): |
|
|
|
""" Inverter major events log """ |
|
|
|
|
|
|
|
|
|
|
|
# 1161-Series Inverters, 4 MPPT, 1 Phase |
|
|
|
class HM1200_Decode0B(StatusResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm1200Decode0B(StatusResponse): |
|
|
|
""" 1161-series mirco-inverters status data """ |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_0(self): |
|
|
|
""" String 1 VDC """ |
|
|
|
return self.unpack('>H', 2)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_0(self): |
|
|
|
""" String 1 ampere """ |
|
|
|
return self.unpack('>H', 4)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_0(self): |
|
|
|
""" String 1 watts """ |
|
|
|
return self.unpack('>H', 8)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_0(self): |
|
|
|
""" String 1 total energy in Wh """ |
|
|
|
return self.unpack('>L', 12)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_0(self): |
|
|
|
""" String 1 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 20)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_1(self): |
|
|
|
""" String 2 VDC """ |
|
|
|
return self.unpack('>H', 2)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_1(self): |
|
|
|
""" String 2 ampere """ |
|
|
|
return self.unpack('>H', 4)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_1(self): |
|
|
|
""" String 2 watts """ |
|
|
|
return self.unpack('>H', 10)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_1(self): |
|
|
|
""" String 2 total energy in Wh """ |
|
|
|
return self.unpack('>L', 16)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_1(self): |
|
|
|
""" String 2 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 22)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_2(self): |
|
|
|
""" String 3 VDC """ |
|
|
|
return self.unpack('>H', 24)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_2(self): |
|
|
|
""" String 3 ampere """ |
|
|
|
return self.unpack('>H', 26)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_2(self): |
|
|
|
""" String 3 watts """ |
|
|
|
return self.unpack('>H', 30)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_2(self): |
|
|
|
""" String 3 total energy in Wh """ |
|
|
|
return self.unpack('>L', 34)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_2(self): |
|
|
|
""" String 3 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 42)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def dc_voltage_3(self): |
|
|
|
""" String 4 VDC """ |
|
|
|
return self.unpack('>H', 24)[0]/10 |
|
|
|
@property |
|
|
|
def dc_current_3(self): |
|
|
|
""" String 4 ampere """ |
|
|
|
return self.unpack('>H', 28)[0]/100 |
|
|
|
@property |
|
|
|
def dc_power_3(self): |
|
|
|
""" String 4 watts """ |
|
|
|
return self.unpack('>H', 32)[0]/10 |
|
|
|
@property |
|
|
|
def dc_energy_total_3(self): |
|
|
|
""" String 4 total energy in Wh """ |
|
|
|
return self.unpack('>L', 38)[0] |
|
|
|
@property |
|
|
|
def dc_energy_daily_3(self): |
|
|
|
""" String 4 daily energy in Wh """ |
|
|
|
return self.unpack('>H', 44)[0] |
|
|
|
|
|
|
|
@property |
|
|
|
def ac_voltage_0(self): |
|
|
|
""" Phase 1 VAC """ |
|
|
|
return self.unpack('>H', 46)[0]/10 |
|
|
|
@property |
|
|
|
def ac_current_0(self): |
|
|
|
""" Phase 1 ampere """ |
|
|
|
return self.unpack('>H', 54)[0]/100 |
|
|
|
@property |
|
|
|
def ac_power_0(self): |
|
|
|
""" Phase 1 watts """ |
|
|
|
return self.unpack('>H', 50)[0]/10 |
|
|
|
@property |
|
|
|
def frequency(self): |
|
|
|
""" Grid frequency in Hertz """ |
|
|
|
return self.unpack('>H', 48)[0]/100 |
|
|
|
@property |
|
|
|
def temperature(self): |
|
|
|
""" Inverter temperature in °C """ |
|
|
|
return self.unpack('>H', 58)[0]/10 |
|
|
|
|
|
|
|
class HM1200_Decode11(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm1200Decode11(EventsResponse): |
|
|
|
""" Inverter generic events log """ |
|
|
|
|
|
|
|
class HM1200_Decode12(EventsResponse): |
|
|
|
def __init__(self, *args, **params): |
|
|
|
super().__init__(*args, **params) |
|
|
|
class Hm1200Decode12(EventsResponse): |
|
|
|
""" Inverter major events log """ |
|
|
|