report alarm and fault bitfield to ha (#204)
* report alarm and fault bitfield to home assistant * initial verson of message builder for SolarmanV5 - for SolarmaV5 we build he param field for the device and inverter message from the internal database - added param description to the info table for constant values, which are not parsed and stored in internal database * define constants for often used format strings * update changelog
This commit is contained in:
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
- add SolarmanV5 messages builder
|
||||
- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7)
|
||||
|
||||
## [0.11.0] - 2024-10-13
|
||||
|
||||
- fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196)
|
||||
|
||||
@@ -70,22 +70,10 @@ class RegisterMap:
|
||||
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
|
||||
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
|
||||
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
|
||||
0x00000191: {'reg': Register.EVENT_401},
|
||||
0x00000192: {'reg': Register.EVENT_402},
|
||||
0x00000193: {'reg': Register.EVENT_403},
|
||||
0x00000194: {'reg': Register.EVENT_404},
|
||||
0x00000195: {'reg': Register.EVENT_405},
|
||||
0x00000196: {'reg': Register.EVENT_406},
|
||||
0x00000197: {'reg': Register.EVENT_407},
|
||||
0x00000198: {'reg': Register.EVENT_408},
|
||||
0x00000199: {'reg': Register.EVENT_409},
|
||||
0x0000019a: {'reg': Register.EVENT_410},
|
||||
0x0000019b: {'reg': Register.EVENT_411},
|
||||
0x0000019c: {'reg': Register.EVENT_412},
|
||||
0x0000019d: {'reg': Register.EVENT_413},
|
||||
0x0000019e: {'reg': Register.EVENT_414},
|
||||
0x0000019f: {'reg': Register.EVENT_415},
|
||||
0x000001a0: {'reg': Register.EVENT_416},
|
||||
0x00000190: {'reg': Register.EVENT_ALARM},
|
||||
0x000001f4: {'reg': Register.EVENT_FAULT},
|
||||
0x00000258: {'reg': Register.EVENT_BF1},
|
||||
0x000002bc: {'reg': Register.EVENT_BF2},
|
||||
0x00000064: {'reg': Register.INVERTER_STATUS},
|
||||
0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
|
||||
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
|
||||
@@ -11,28 +11,48 @@ class RegisterMap:
|
||||
# make the class read/only by using __slots__
|
||||
__slots__ = ()
|
||||
|
||||
FMT_2_16BIT_VAL = '!HH'
|
||||
FMT_3_16BIT_VAL = '!HHH'
|
||||
FMT_4_16BIT_VAL = '!HHHH'
|
||||
|
||||
map = {
|
||||
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'quotient': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||
0x4102001b: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501
|
||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001d: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501
|
||||
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
||||
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!BBBBBB', 'func': Fmt.mac}, # noqa: E501
|
||||
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!6B', 'func': Fmt.mac}, # noqa: E501
|
||||
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
||||
0x4102005c: {'reg': None, 'fmt': '<B', 'const': 15}, # noqa: E501
|
||||
0x4102005e: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x41020061: {'reg': None, 'fmt': '<BBB', 'const': (15, 0, 255)}, # noqa: E501
|
||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
|
||||
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
|
||||
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||
|
||||
# Start MODBUS Block: 0x3000 (R/O Measurements)
|
||||
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c2: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c4: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c6: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c8: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100ca: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100cc: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
# 0x420100ce
|
||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
|
||||
0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
|
||||
# 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
|
||||
# 0x420100da
|
||||
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
@@ -57,12 +77,34 @@ class RegisterMap:
|
||||
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x42010116: {'reg': Register.INV_UNKNOWN_1, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
# Start MODBUS Block: 0x2000 (R/W Config Paramaneters)
|
||||
0x42010118: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x4201011a: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x4201011c: {'reg': None, 'fmt': FMT_2_16BIT_VAL, 'const': (1, 0)}, # noqa: E501
|
||||
0x42010124: {'reg': None, 'fmt': '!H', 'const': 0xffff}, # noqa: E501
|
||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x42010128: {'reg': None, 'fmt': '!H', 'const': 3}, # noqa: E501
|
||||
0x4201012a: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (1024, 1024, 1024)}, # noqa: E501
|
||||
0x42010130: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1024, 1, 0xffff, 1)}, # noqa: E501
|
||||
0x42010138: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (6, 0x68, 0x68, 0x500)}, # noqa: E501
|
||||
0x42010140: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9cd, 0x7b6, 0x139c, 0x1324)}, # noqa: E501
|
||||
0x42010148: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1, 0x7ae, 0x40f, 0x41)}, # noqa: E501
|
||||
0x42010150: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0xf, 0xa64, 0xa64, 0x6)}, # noqa: E501
|
||||
0x42010158: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x6, 0x9f6, 0x128c, 0x128c)}, # noqa: E501
|
||||
0x42010160: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x1452, 0x1452)}, # noqa: E501
|
||||
0x42010168: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x151, 0x5)}, # noqa: E501
|
||||
0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x42010172: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x1, 0x139c, 0xfa0)}, # noqa: E501
|
||||
0x42010178: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x4e, 0x66, 0x3e8, 0x400)}, # noqa: E501
|
||||
0x42010180: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9ce, 0x7a8, 0x139c, 0x1326)}, # noqa: E501
|
||||
0x42010188: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 0x0, 0)}, # noqa: E501
|
||||
0x42010190: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 1024, 1024)}, # noqa: E501
|
||||
0x4201019a: {'reg': None, 'fmt': FMT_2_16BIT_VAL, 'const': (0, 0xffff)}, # noqa: E501
|
||||
|
||||
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -136,3 +178,23 @@ class InfosG3P(Infos):
|
||||
if update:
|
||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
|
||||
def build(self, len, msg_type: int, rcv_ftype: int):
|
||||
buf = bytearray(len)
|
||||
for idx, row in RegisterMap.map.items():
|
||||
addr = idx & 0xffff
|
||||
ftype = (idx >> 16) & 0xff
|
||||
mtype = (idx >> 24) & 0xff
|
||||
if ftype != rcv_ftype or mtype != msg_type:
|
||||
continue
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if 'const' in row:
|
||||
val = row['const']
|
||||
else:
|
||||
info_id = row['reg']
|
||||
val = self.get_db_value(info_id)
|
||||
if not val:
|
||||
continue
|
||||
Fmt.set_value(buf, addr, row, val)
|
||||
return buf
|
||||
|
||||
204
app/src/infos.py
204
app/src/infos.py
@@ -43,6 +43,8 @@ class Register(Enum):
|
||||
RATED_POWER = 84
|
||||
INVERTER_TEMP = 85
|
||||
INVERTER_STATUS = 86
|
||||
DETECT_STATUS_1 = 87
|
||||
DETECT_STATUS_2 = 88
|
||||
PV1_VOLTAGE = 100
|
||||
PV1_CURRENT = 101
|
||||
PV1_POWER = 102
|
||||
@@ -85,6 +87,10 @@ class Register(Enum):
|
||||
PV5_TOTAL_GENERATION = 241
|
||||
PV6_DAILY_GENERATION = 250
|
||||
PV6_TOTAL_GENERATION = 251
|
||||
INV_UNKNOWN_1 = 252
|
||||
BOOT_STATUS = 253
|
||||
DSP_STATUS = 254
|
||||
|
||||
GRID_VOLTAGE = 300
|
||||
GRID_CURRENT = 301
|
||||
GRID_FREQUENCY = 302
|
||||
@@ -100,22 +106,11 @@ class Register(Enum):
|
||||
IP_ADDRESS = 407
|
||||
POLLING_INTERVAL = 408
|
||||
SENSOR_LIST = 409
|
||||
EVENT_401 = 500
|
||||
EVENT_402 = 501
|
||||
EVENT_403 = 502
|
||||
EVENT_404 = 503
|
||||
EVENT_405 = 504
|
||||
EVENT_406 = 505
|
||||
EVENT_407 = 506
|
||||
EVENT_408 = 507
|
||||
EVENT_409 = 508
|
||||
EVENT_410 = 509
|
||||
EVENT_411 = 510
|
||||
EVENT_412 = 511
|
||||
EVENT_413 = 512
|
||||
EVENT_414 = 513
|
||||
EVENT_415 = 514
|
||||
EVENT_416 = 515
|
||||
SSID = 410
|
||||
EVENT_ALARM = 500
|
||||
EVENT_FAULT = 501
|
||||
EVENT_BF1 = 502
|
||||
EVENT_BF2 = 503
|
||||
TS_INPUT = 600
|
||||
TS_GRID = 601
|
||||
TS_TOTAL = 602
|
||||
@@ -144,17 +139,54 @@ class Fmt:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def hex4(val):
|
||||
return f'{val[0]:04x}'
|
||||
def hex4(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
return f'{val[0]:04x}'
|
||||
else:
|
||||
return int(val, 16)
|
||||
|
||||
@staticmethod
|
||||
def mac(val):
|
||||
return "%02x:%02x:%02x:%02x:%02x:%02x" % val
|
||||
def mac(val: tuple | str, reverse=False) -> str | tuple:
|
||||
if not reverse:
|
||||
return "%02x:%02x:%02x:%02x:%02x:%02x" % val
|
||||
else:
|
||||
return (
|
||||
int(val[0:2], 16), int(val[3:5], 16),
|
||||
int(val[6:8], 16), int(val[9:11], 16),
|
||||
int(val[12:14], 16), int(val[15:], 16))
|
||||
|
||||
@staticmethod
|
||||
def version(val):
|
||||
x = val[0]
|
||||
return f'V{(x >> 12)}.{(x >> 8) & 0xf}.{(x >> 4) & 0xf}{x & 0xf:1X}'
|
||||
def version(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
x = val[0]
|
||||
return f'V{(x >> 12)}.{(x >> 8) & 0xf}' \
|
||||
f'.{(x >> 4) & 0xf}{x & 0xf:1X}'
|
||||
else:
|
||||
arr = val[1:].split('.')
|
||||
return int(arr[0], 10) << 12 | \
|
||||
int(arr[1], 10) << 8 | \
|
||||
int(arr[2][:-1], 10) << 4 | \
|
||||
int(arr[2][-1:], 16)
|
||||
|
||||
@staticmethod
|
||||
def set_value(buf: bytearray, idx: int, row: dict, val):
|
||||
'''Get a value from buf and interpret as in row defined'''
|
||||
fmt = row['fmt']
|
||||
if 'offset' in row:
|
||||
val = val - row['offset']
|
||||
if 'quotient' in row:
|
||||
val = round(val * row['quotient'])
|
||||
if 'ratio' in row:
|
||||
val = round(val / row['ratio'])
|
||||
if 'func' in row:
|
||||
val = row['func'](val, reverse=True)
|
||||
if isinstance(val, str):
|
||||
val = bytes(val, 'UTF8')
|
||||
|
||||
if isinstance(val, tuple):
|
||||
struct.pack_into(fmt, buf, idx, *val)
|
||||
else:
|
||||
struct.pack_into(fmt, buf, idx, val)
|
||||
|
||||
|
||||
class ClrAtMidnight:
|
||||
@@ -251,6 +283,99 @@ class Infos:
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_alarm_val_tpl = '''
|
||||
{% if 'Inverter_Alarm' in value_json and
|
||||
value_json['Inverter_Alarm'] != None %}
|
||||
{% set val_int = value_json['Inverter_Alarm'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noAlarm'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'noUtility, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_fault_val_tpl = '''
|
||||
{% if 'Inverter_Fault' in value_json and
|
||||
value_json['Inverter_Fault'] != None %}
|
||||
{% set val_int = value_json['Inverter_Fault'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noFault'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'Bit9, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
__output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
|
||||
__info_defs = {
|
||||
@@ -286,7 +411,8 @@ class Infos:
|
||||
Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MANUFACTURER: {'name': ['inverter', 'PV6_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.BOOT_STATUS: {'name': ['inverter', 'BOOT_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DSP_STATUS: {'name': ['inverter', 'DSP_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
# proxy:
|
||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
|
||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
@@ -303,22 +429,12 @@ class Infos:
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
||||
|
||||
# events
|
||||
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_411: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_413: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_414: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_ALARM: {'name': ['events', 'Inverter_Alarm'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_alarm_', 'name': 'Inverter Alarm', 'val_tpl': __inv_alarm_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_FAULT: {'name': ['events', 'Inverter_Fault'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_fault_', 'name': 'Inverter Fault', 'val_tpl': __inv_fault_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_BF1: {'name': ['events', 'Inverter_Bitfield_1'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_BF2: {'name': ['events', 'Inverter_bitfield_2'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# grid measures:
|
||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -328,6 +444,8 @@ class Infos:
|
||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
||||
Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# input measures:
|
||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -377,6 +495,10 @@ class Infos:
|
||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.SSID: {'name': ['controller', 'WiFi_SSID'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -686,6 +808,8 @@ class Infos:
|
||||
|
||||
def get_db_value(self, id: Register, not_found_result: any = None):
|
||||
'''get database value'''
|
||||
if id not in self.info_defs:
|
||||
return not_found_result
|
||||
row = self.info_defs[id]
|
||||
if isinstance(row, dict):
|
||||
keys = row['name']
|
||||
|
||||
@@ -40,10 +40,19 @@ class Modbus():
|
||||
|
||||
__crc_tab = []
|
||||
mb_reg_mapping = {
|
||||
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
|
||||
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x3001: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3002: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x3003: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x3004: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x3005: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3006: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
|
||||
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
@@ -74,6 +83,7 @@ class Modbus():
|
||||
0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
# 0x302a
|
||||
}
|
||||
|
||||
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
import json, math
|
||||
import logging
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.infos import Infos
|
||||
from app.src.infos import Infos, Fmt
|
||||
|
||||
def test_statistic_counter():
|
||||
i = Infos()
|
||||
@@ -256,3 +256,24 @@ def test_key_obj():
|
||||
assert level == logging.DEBUG
|
||||
assert unit == 'kWh'
|
||||
assert must_incr == True
|
||||
|
||||
def test_hex4_cnv():
|
||||
tst_val = (0x12ef, )
|
||||
string = Fmt.hex4(tst_val)
|
||||
assert string == '12ef'
|
||||
val = Fmt.hex4(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
def test_mac_cnv():
|
||||
tst_val = (0x12, 0x34, 0x67, 0x89, 0xcd, 0xef)
|
||||
string = Fmt.mac(tst_val)
|
||||
assert string == '12:34:67:89:cd:ef'
|
||||
val = Fmt.mac(string, reverse=True)
|
||||
assert val == tst_val
|
||||
|
||||
def test_version_cnv():
|
||||
tst_val = (0x123f, )
|
||||
string = Fmt.version(tst_val)
|
||||
assert string == 'V1.2.3F'
|
||||
val = Fmt.version(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
@@ -501,10 +501,10 @@ def test_new_data_types(inv_data_new):
|
||||
else:
|
||||
assert False
|
||||
|
||||
assert tests==15
|
||||
assert tests==5
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"Inverter_Alarm": 0, "Inverter_Fault": 0})
|
||||
|
||||
def test_invalid_data_type(invalid_data_seq):
|
||||
i = InfosG3()
|
||||
|
||||
@@ -57,6 +57,7 @@ def inverter_data(): # 0x4210 ftype: 0x01
|
||||
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||
|
||||
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
@@ -85,10 +86,21 @@ def test_parse_4110(str_test_ip, device_data: bytes):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0", "WiFi_SSID": "Allius-Home"},
|
||||
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||
})
|
||||
|
||||
def test_build_4110(str_test_ip, device_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
for key, update in i.parse (device_data, 0x41, 2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(device_data), 0x41, 2)
|
||||
for i in range(11, 20):
|
||||
build_msg[i] = device_data[i]
|
||||
assert device_data == build_msg
|
||||
|
||||
def test_parse_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
@@ -98,16 +110,30 @@ def test_parse_4210(inverter_data: bytes):
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Detect_Status_1": 2, "Detect_Status_2": 0, "Inverter_Temp": 14},
|
||||
"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0},
|
||||
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
||||
"pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91},
|
||||
"pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89},
|
||||
"pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}},
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36}
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36},
|
||||
"inv_unknown": {"Unknown_1": 512}
|
||||
})
|
||||
|
||||
def test_build_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
for i in range(11, 31):
|
||||
build_msg[i] = inverter_data[i]
|
||||
assert inverter_data == build_msg
|
||||
|
||||
def test_build_ha_conf1():
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.static_init() # initialize counter
|
||||
@@ -269,19 +295,43 @@ def test_exception_and_calc(inverter_data: bytes):
|
||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
||||
|
||||
i = InfosG3P(client_mode=False)
|
||||
# i.db.clear()
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xde] == inverter_data[32:0xde]
|
||||
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
|
||||
# remove a table entry and test parsing and building
|
||||
del RegisterMap.map[0x420100d8]['quotient']
|
||||
del RegisterMap.map[0x420100d8]['offset']
|
||||
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
|
||||
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
# test restore table
|
||||
RegisterMap.map[0x420100d8]['offset'] = ofs
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
|
||||
# test orginial table
|
||||
i.db.clear()
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:-1] == inverter_data[32:-1]
|
||||
|
||||
Reference in New Issue
Block a user