From 78a35b5513837a1941f126944df5b2ddbf8664c6 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:09:10 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 3 + app/src/gen3/infos_g3.py | 20 +--- app/src/gen3plus/infos_g3p.py | 70 +++++++++++- app/src/infos.py | 204 +++++++++++++++++++++++++++------- app/src/modbus.py | 10 ++ app/tests/test_infos.py | 23 +++- app/tests/test_infos_g3.py | 4 +- app/tests/test_infos_g3p.py | 64 +++++++++-- 8 files changed, 328 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bd297..a3bc961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 88f9207..480fc94 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -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}, diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 58c53c9..443dfac 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -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': '> 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 diff --git a/app/src/infos.py b/app/src/infos.py index 4358969..92609a1 100644 --- a/app/src/infos.py +++ b/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'] diff --git a/app/src/modbus.py b/app/src/modbus.py index eec6d17..c3a1d67 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -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], diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index 8d0c268..18eb5e4 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -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] diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py index 0c84c20..1bab29e 100644 --- a/app/tests/test_infos_g3.py +++ b/app/tests/test_infos_g3.py @@ -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() diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 095a29b..3fbefa9 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -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]