From 036dd6d1dce54e3d6b8d9305c57c55b2d343aea2 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:39:34 +0100 Subject: [PATCH] S allius/issue281 (#282) * accept DCU serial number starting with '410' * determine sensor-list by serial number * adapt unit test for DCU support * send first batterie measurements to home assistant * add test case for sensor-list==3036 * add more registers for batteries * improve error logging (Monitoring SN) * update the add-on repro only for one stage * add configuration for energie storages * add License and Readme file to the add-on * addon: add date and time to dev and debug docker container tag * disable duplicate code check for config.py * cleanup unit test, remove trailing whitespaces * update changelog * fix example config for batteries * cleanup config.jinja template * fix comments * improve help texts --- CHANGELOG.md | 5 +- app/src/cnf/config.py | 34 +++++- app/src/cnf/config_read_json.py | 3 +- app/src/cnf/default_config.toml | 31 +++++- app/src/gen3plus/infos_g3p.py | 39 ++++++- app/src/gen3plus/solarman_v5.py | 29 +++-- app/src/infos.py | 76 +++++++++++-- app/src/inverter_base.py | 2 + app/src/modbus_tcp.py | 6 +- app/src/proxy.py | 4 +- app/tests/test_config.py | 104 ++++++++++++++++-- app/tests/test_config_read_json.py | 14 +++ app/tests/test_infos_g3p.py | 31 ++++++ app/tests/test_solarman.py | 99 ++++++++++++++++- ha_addons/Makefile | 137 ++++++++++++++---------- ha_addons/docker-bake.hcl | 4 +- ha_addons/ha_addon/DOCS.md | 19 +++- ha_addons/ha_addon/translations/de.yaml | 25 +++-- ha_addons/ha_addon/translations/en.yaml | 28 +++-- ha_addons/templates/config.jinja | 43 +++++--- ha_addons/templates/debug_data.json | 1 - ha_addons/templates/dev_data.json | 1 - sonar-project.properties | 4 + system_tests/test_tcp_socket_v2.py | 84 ++++++++++++++- 24 files changed, 691 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abc9f03..3dee3ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Respect logging.ini file, if LOG_ENV isn't set well [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288) - Remove trailing apostrophe in the log output [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288) -- Update AddOn base docker image to version 17.1.3 and python3 to 3.12.9-r0 +- update AddOn base docker image to version 17.2.1 +- addon: add date and time to dev container version +- Update AddOn python3 to 3.12.9-r0 - add initial DCU support -- update AddOn base docker image to version 17.1.2 - update aiohttp to version 3.11.12 - fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180) diff --git a/app/src/cnf/config.py b/app/src/cnf/config.py index a8f16db..1bc59a9 100644 --- a/app/src/cnf/config.py +++ b/app/src/cnf/config.py @@ -78,7 +78,8 @@ class Config(): } }, 'inverters': { - 'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): { + 'allow_all': Use(bool), + And(Use(str), lambda s: len(s) == 16): { Optional('monitor_sn', default=0): Use(int), Optional('node_id', default=""): And(Use(str), Use(lambda s: s + '/' @@ -93,7 +94,7 @@ class Config(): }, Optional('modbus_polling', default=True): Use(bool), Optional('suggested_area', default=""): Use(str), - Optional('sensor_list', default=0x2b0): Use(int), + Optional('sensor_list', default=0): Use(int), Optional('pv1'): { Optional('type'): Use(str), Optional('manufacturer'): Use(str), @@ -119,6 +120,33 @@ class Config(): Optional('manufacturer'): Use(str), } } + }, + 'batteries': { + And(Use(str), lambda s: len(s) == 16): { + Optional('monitor_sn', default=0): Use(int), + Optional('node_id', default=""): And(Use(str), + Use(lambda s: s + '/' + if len(s) > 0 + and s[-1] != '/' + else s)), + Optional('client_mode'): { + 'host': Use(str), + Optional('port', default=8899): + And(Use(int), lambda n: 1024 <= n <= 65535), + Optional('forward', default=False): Use(bool), + }, + Optional('modbus_polling', default=True): Use(bool), + Optional('suggested_area', default=""): Use(str), + Optional('sensor_list', default=0): Use(int), + Optional('pv1'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv2'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + } + } } }, ignore_extra_keys=True ) @@ -178,7 +206,7 @@ here. The default config reader is handled in the Config.init method''' rd_config = reader.get_config() config = cls.act_config.copy() for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters', - 'gen3plus']: + 'gen3plus', 'batteries']: if key in rd_config: config[key] = config[key] | rd_config[key] diff --git a/app/src/cnf/config_read_json.py b/app/src/cnf/config_read_json.py index 785dae7..e763813 100644 --- a/app/src/cnf/config_read_json.py +++ b/app/src/cnf/config_read_json.py @@ -31,7 +31,8 @@ class ConfigReadJson(ConfigIfc): def convert_to_obj(self, data): conf = {} for key, val in data.items(): - if key == 'inverters' and isinstance(val, list): + if (key == 'inverters' or key == 'batteries') and \ + isinstance(val, list): self.convert_inv_arr(conf, key, val) else: self._extend_key(conf, key, val) diff --git a/app/src/cnf/default_config.toml b/app/src/cnf/default_config.toml index 6c9ba77..ef7518f 100644 --- a/app/src/cnf/default_config.toml +++ b/app/src/cnf/default_config.toml @@ -113,7 +113,7 @@ inverters.allow_all = false # only allow known inverters ## ## For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT ## definition. To do this, the corresponding configuration block is started with -## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned +## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned ## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set ## in the configuration block ## @@ -132,7 +132,7 @@ pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module de ## ## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT ## definition. To do this, the corresponding configuration block is started with -## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned +## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned ## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode) ## can be set in the configuration block ## @@ -157,6 +157,33 @@ pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +########################################################################################## +## +## For each GEN3PLUS enrgy storage system, the serial number must be mapped to an MQTT +## definition. To do this, the corresponding configuration block is started with +## `[batteries.“<16-digit serial number>”]` so that all subsequent parameters are assigned +## to this energy storage system. Further device-specific parameters (e.g. polling mode, +## client mode) can be set in the configuration block +## +## The serial numbers of all GEN3PLUS energy storage systems/batteries start with `410`! +## Each GEN3PLUS device is supplied with a “Monitoring SN:”. This can be found on a +## sticker enclosed with the inverter. +## + +[batteries."4100000000000001"] +monitor_sn = 3000000000 # The GEN3PLUS "Monitoring SN:" +node_id = '' # MQTT replacement for devices serial number +suggested_area = '' # suggested installation place for home-assistant +modbus_polling = true # Enable optional MODBUS polling + +# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment +# the next line and configure the fixed IP of your inverter +#client_mode = {host = '192.168.0.1', port = 8899, forward = true} + +pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr + + ########################################################################################## ### ### If the proxy mode is configured, commands from TSUN can be sent to the inverter via diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 0a6264f..b42b2be 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -116,6 +116,33 @@ class RegisterMap: 0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': ' suggested area string from the config file''' # iterate over RegisterMap.map and get the register values - for _, row in chain(RegisterMap.map.items(), - RegisterMap.map_02b0.items(), - RegisterMap.map_3026.items()): + sensor = self.get_db_value(Register.SENSOR_LIST) + if "3026" == sensor: + items = RegisterMap.map_3026.items() + elif "02b0" == sensor: + items = RegisterMap.map_02b0.items() + else: + items = {} + + for _, row in chain(RegisterMap.map.items(), items): info_id = row['reg'] if self.__hide_topic(row): res = self.ha_remove(info_id, node_id, snr) # noqa: E501 diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 837b78c..4fcb71b 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -2,6 +2,7 @@ import struct import logging import time import asyncio +from itertools import chain from datetime import datetime from async_ifc import AsyncIfc @@ -376,12 +377,23 @@ class SolarmanV5(SolarmanBase): self.ifc.fwd_add(build_msg) self.ifc.fwd_add(struct.pack(' {inv}') if (type(inv) is dict and 'monitor_sn' in inv and inv['monitor_sn'] == snr): - self.__set_config_parms(inv) + self.__set_config_parms(inv, key) self.db.set_pv_module_details(inv) logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 @@ -411,9 +424,11 @@ class SolarmanV5(SolarmanBase): if 'allow_all' not in inverters or not inverters['allow_all']: self.inc_counter('Unknown_SNR') self.unique_id = None - logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501 + logging.error(f"Ignore message from unknow inverter with Monitoring-SN: {serial_no})!\n" # noqa: E501 + " !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501 return - logger.warning(f'SerialNo {serial_no} not known but accepted!') + logging.warning(f"Monitoring-SN: {serial_no} not configured but accepted!" # noqa: E501 + " !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501 self.unique_id = serial_no @@ -460,11 +475,11 @@ class SolarmanV5(SolarmanBase): def mb_timout_cb(self, exp_cnt): self.mb_timer.start(self.mb_timeout) - self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG) + self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.INFO) if 1 == (exp_cnt % 30): # logging.info("Regular Modbus Status request") - self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG) + self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.INFO) def at_cmd_forbidden(self, cmd: str, connection: str) -> bool: return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \ diff --git a/app/src/infos.py b/app/src/infos.py index bcdb847..2212d78 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -121,6 +121,32 @@ class Register(Enum): TS_INPUT = 600 TS_GRID = 601 TS_TOTAL = 602 + BATT_PV1_VOLT = 1000 + BATT_PV1_CUR = 1001 + BATT_PV2_VOLT = 1002 + BATT_PV2_CUR = 1003 + BATT_38 = 1004 + BATT_3a = 1005 + BATT_3c = 1006 + BATT_3e = 1007 + BATT_40 = 1010 + BATT_42 = 1011 + BATT_SOC = 1012 + BATT_46 = 1013 + BATT_48 = 1014 + BATT_4a = 1015 + BATT_4c = 1016 + BATT_4e = 1017 + BATT_66 = 1018 + BATT_68 = 1019 + BATT_6a = 1020 + BATT_6c = 1021 + BATT_6e = 1022 + BATT_70 = 1023 + BATT_72 = 1024 + BATT_74 = 1025 + BATT_76 = 1026 + BATT_78 = 1027 VALUE_1 = 9000 TEST_REG1 = 10000 TEST_REG2 = 10001 @@ -266,6 +292,7 @@ class Infos: __info_devs = { 'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501 'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION, 'mac': Register.MAC_ADDR, 'sn': Register.COLLECTOR_SNR}, # noqa: E501 + 'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501 'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501 'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501 @@ -273,6 +300,10 @@ class Infos: 'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501 'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501 'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501 + + 'batterie': {'via': 'controller', 'name': 'Batterie', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501 + 'bat_inp_pv1': {'via': 'batterie', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501 + 'bat_inp_pv2': {'via': 'batterie', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER}, # noqa: E501 } __comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501 @@ -518,15 +549,15 @@ class Infos: Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501 # controller: - Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501 - Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501 - Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 - 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.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501 + Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.INFO, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501 + Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.INFO, '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.INFO, '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 @@ -536,6 +567,33 @@ class Infos: Register.PROD_COMPL_TYPE: {'name': ['other', 'Prod_Compliance_Type'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.BATT_PV1_VOLT: {'name': ['batterie', 'pv1', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_PV1_CUR: {'name': ['batterie', 'pv1', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_PV2_VOLT: {'name': ['batterie', 'pv2', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_PV2_CUR: {'name': ['batterie', 'pv2', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_38: {'name': ['batterie', 'Reg_38'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_38_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_3a: {'name': ['batterie', 'Reg_3a'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_3a_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_3c: {'name': ['batterie', 'Reg_3c'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_3c_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_3e: {'name': ['batterie', 'Reg_3e'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_3e_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_40: {'name': ['batterie', 'Reg_40'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_40_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_42: {'name': ['batterie', 'Reg_42'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_42_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_SOC: {'name': ['batterie', 'SOC'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'batterie', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'soc_', 'fmt': FMT_FLOAT, 'name': 'State of Charge', 'icon': 'mdi:battery-90'}}, # noqa: E501 + # Register.BATT_46: {'name': ['batterie', 'Reg_46'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_46_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + # Register.BATT_48: {'name': ['batterie', 'Reg_48'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_48_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + # Register.BATT_4a: {'name': ['batterie', 'Reg_4a'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_4a_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + # Register.BATT_4c: {'name': ['batterie', 'Reg_4c'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_4c_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + # Register.BATT_4e: {'name': ['batterie', 'Reg_4e'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_4e_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + + Register.BATT_66: {'name': ['batterie', 'Reg_66'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_66_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_68: {'name': ['batterie', 'Reg_68'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_68_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_6a: {'name': ['batterie', 'Reg_6a'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_6a_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_6c: {'name': ['batterie', 'Reg_6c'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_6c_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_6e: {'name': ['batterie', 'Reg_6e'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_6e_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_70: {'name': ['batterie', 'Reg_70'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_70_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_72: {'name': ['batterie', 'Reg_72'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_72_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_74: {'name': ['batterie', 'Reg_74'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_74_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_76: {'name': ['batterie', 'Reg_76'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_76_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_78: {'name': ['batterie', 'Reg_78'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_78_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 } @property diff --git a/app/src/inverter_base.py b/app/src/inverter_base.py index 9daa55b..a4036c9 100644 --- a/app/src/inverter_base.py +++ b/app/src/inverter_base.py @@ -151,6 +151,8 @@ class InverterBase(InverterIfc, Proxy): # home assistant has changed the status back to online try: if (('inverter' in stream.new_data and stream.new_data['inverter']) + or ('batterie' in stream.new_data and + stream.new_data['batterie']) or ('collector' in stream.new_data and stream.new_data['collector']) or self.mqtt.ha_restarts != self.__ha_restarts): diff --git a/app/src/modbus_tcp.py b/app/src/modbus_tcp.py index f74b4a0..7ae635d 100644 --- a/app/src/modbus_tcp.py +++ b/app/src/modbus_tcp.py @@ -1,6 +1,7 @@ import logging import traceback import asyncio +from itertools import chain from cnf.config import Config from gen3plus.inverter_g3p import InverterG3P @@ -42,14 +43,15 @@ class ModbusTcp(): self.tim_restart = tim_restart inverters = Config.get('inverters') + batteries = Config.get('batteries') # logging.info(f'Inverters: {inverters}') - for inv in inverters.values(): + for _, inv in chain(inverters.items(), batteries.items()): if (type(inv) is dict and 'monitor_sn' in inv and 'client_mode' in inv): client = inv['client_mode'] - logger.info(f"'client_mode' for snr: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501 + logger.info(f"'client_mode' for Monitoring-SN: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501 loop.create_task(self.modbus_loop(client['host'], client['port'], inv['monitor_sn'], diff --git a/app/src/proxy.py b/app/src/proxy.py index 3f4f263..8c935f7 100644 --- a/app/src/proxy.py +++ b/app/src/proxy.py @@ -1,6 +1,7 @@ import asyncio import logging import json +from itertools import chain from cnf.config import Config from mqtt import Mqtt @@ -56,8 +57,9 @@ class Proxy(): # reset at midnight when you restart the proxy just before # midnight! inverters = Config.get('inverters') + batteries = Config.get('batteries') # logger.debug(f'Proxys: {inverters}') - for inv in inverters.values(): + for _, inv in chain(inverters.items(), batteries.items()): if (type(inv) is dict): node_id = inv['node_id'] cls.db_stat.reg_clr_at_midnight(f'{cls.entity_prfx}{node_id}', diff --git a/app/tests/test_config.py b/app/tests/test_config.py index 96b7478..aa0cd52 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -76,7 +76,7 @@ def ConfigDefault(): 'type': 'RSM40-8-395M'}, 'pv2': {'manufacturer': 'Risen', 'type': 'RSM40-8-395M'}, - 'sensor_list': 688 + 'sensor_list': 0 }, 'Y170000000000001': { 'modbus_polling': True, @@ -91,8 +91,21 @@ def ConfigDefault(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } + }, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'suggested_area': '', + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'sensor_list': 0, + } } } @@ -140,6 +153,18 @@ def ConfigComplete(): 'type': 'type4'}, 'suggested_area': 'Garage2', 'sensor_list': 688} + }, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'suggested_area': 'Garage3', + 'node_id': 'Bat-Garage3/', + 'pv1': {'manufacturer': 'man5', + 'type': 'type5'}, + 'pv2': {'manufacturer': 'man6', + 'type': 'type6'}, + 'sensor_list': 12326} } } @@ -147,6 +172,19 @@ def test_default_config(): Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) validated = Config.def_config assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'sensor_list': 0, + 'suggested_area': '' + } + }, 'inverters': { 'allow_all': False, 'R170000000000001': { @@ -158,7 +196,7 @@ def test_default_config(): 'modbus_polling': False, 'monitor_sn': 0, 'suggested_area': '', - 'sensor_list': 688}, + 'sensor_list': 0}, 'Y170000000000001': { 'modbus_polling': True, 'monitor_sn': 2000000000, @@ -172,7 +210,7 @@ def test_default_config(): 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, 'suggested_area': '', - 'sensor_list': 688}}} + 'sensor_list': 0}}} def test_full_config(ConfigComplete): cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, @@ -181,9 +219,14 @@ def test_full_config(ConfigComplete): 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'batteries': { + '4100000000000001': {'modbus_polling': True, 'monitor_sn': 3000000000, 'node_id': 'Bat-Garage3/', 'sensor_list': 0x3026, 'suggested_area': 'Garage3', 'pv1': {'type': 'type5', 'manufacturer': 'man5'}, 'pv2': {'type': 'type6', 'manufacturer': 'man6'}} + }, 'inverters': {'allow_all': False, 'R170000000000001': {'modbus_polling': False, 'node_id': 'PV-Garage/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}}, - 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}}}} + 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}} + } + } try: validated = Config.conf_schema.validate(cnf) except Exception: @@ -240,6 +283,19 @@ def test_read_cnf1(): assert err == None cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'sensor_list': 0, + 'suggested_area': '' + } + }, 'inverters': { 'allow_all': False, 'R170000000000001': { @@ -251,7 +307,7 @@ def test_read_cnf1(): 'type': 'RSM40-8-395M'}, 'pv2': {'manufacturer': 'Risen', 'type': 'RSM40-8-395M'}, - 'sensor_list': 688 + 'sensor_list': 0 }, 'Y170000000000001': { 'modbus_polling': True, @@ -266,7 +322,7 @@ def test_read_cnf1(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } @@ -287,6 +343,19 @@ def test_read_cnf2(): assert err == None cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'sensor_list': 0, + 'suggested_area': '' + } + }, 'inverters': { 'allow_all': False, 'R170000000000001': { @@ -298,7 +367,7 @@ def test_read_cnf2(): 'type': 'RSM40-8-395M'}, 'pv2': {'manufacturer': 'Risen', 'type': 'RSM40-8-395M'}, - 'sensor_list': 688 + 'sensor_list': 0 }, 'Y170000000000001': { 'modbus_polling': True, @@ -313,7 +382,7 @@ def test_read_cnf2(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } @@ -342,6 +411,19 @@ def test_read_cnf4(): assert err == None cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'batteries': { + '4100000000000001': { + 'modbus_polling': True, + 'monitor_sn': 3000000000, + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'sensor_list': 0, + 'suggested_area': '' + } + }, 'inverters': { 'allow_all': False, 'R170000000000001': { @@ -353,7 +435,7 @@ def test_read_cnf4(): 'type': 'RSM40-8-395M'}, 'pv2': {'manufacturer': 'Risen', 'type': 'RSM40-8-395M'}, - 'sensor_list': 688 + 'sensor_list': 0 }, 'Y170000000000001': { 'modbus_polling': True, @@ -368,7 +450,7 @@ def test_read_cnf4(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } diff --git a/app/tests/test_config_read_json.py b/app/tests/test_config_read_json.py index e1d4c6a..647885a 100644 --- a/app/tests/test_config_read_json.py +++ b/app/tests/test_config_read_json.py @@ -382,6 +382,20 @@ def test_full_config(ConfigComplete): "sensor_list": 688 } ], + "batteries": [ + { + "serial": "4100000000000001", + "modbus_polling": true, + "monitor_sn": 3000000000, + "node_id": "Bat-Garage3", + "suggested_area": "Garage3", + "pv1.manufacturer": "man5", + "pv1.type": "type5", + "pv2.manufacturer": "man6", + "pv2.type": "type6", + "sensor_list": 12326 + } + ], "tsun.enabled": true, "solarman.enabled": true, "inverters.allow_all": false, diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 7da5924..f49cb50 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -138,6 +138,7 @@ def test_build_4210(inverter_data: bytes): def test_build_ha_conf1(): i = InfosG3P(client_mode=False) i.static_init() # initialize counter + i.set_db_def_value(Register.SENSOR_LIST, "02b0") tests = 0 for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'): @@ -212,6 +213,7 @@ def test_build_ha_conf2(): def test_build_ha_conf3(): i = InfosG3P(client_mode=True) i.static_init() # initialize counter + i.set_db_def_value(Register.SENSOR_LIST, "02b0") tests = 0 for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'): @@ -283,6 +285,35 @@ def test_build_ha_conf4(): assert tests==1 +def test_build_ha_conf5(): + i = InfosG3P(client_mode=True) + i.static_init() # initialize counter + i.set_db_def_value(Register.SENSOR_LIST, "3026") + + tests = 0 + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'): + + if id == 'out_power_123': + assert False + elif id == 'daily_gen_123': + assert False + elif id == 'power_pv1_123': + assert False + elif id == 'power_pv2_123': + assert False + elif id == 'power_pv3_123': + assert False + elif id == 'power_pv4_123': + assert False + elif id == 'signal_123': + assert comp == 'sensor' + assert d_json == json.dumps({}) + tests +=1 + elif id == 'inv_count_456': + assert False + + assert tests==1 + def test_exception_and_calc(inverter_data: bytes): # patch table to convert temperature from °F to °C diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 6f11bec..f379489 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -130,6 +130,12 @@ def get_sn() -> bytes: def get_sn_int() -> int: return 2070233889 +def get_dcu_sn() -> bytes: + return b'\x20\x43\x65\x7b' + +def get_dcu_sn_int() -> int: + return 2070233888 + def get_inv_no() -> bytes: return b'T170000000000001' @@ -672,6 +678,65 @@ def msg_unknown_cmd_rsp(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def dcu_dev_ind_msg(): # 0x4110 + msg = b'\xa5\x3a\x01\x10\x41\x91\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32' + msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53' + msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35' + msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e' + msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26' + msg += b'\x30\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x7a\x75\x68\x61\x75\x73\x65\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x01\x01\x01\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_dev_rsp_msg(): # 0x1110 + msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01' + msg += total() + msg += hb() + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_data_ind_msg(): # 0x4210 + msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde' + msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00' + msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34' + msg += b'\x0d\x3a\x00\x00\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00' + msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89' + msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89' + msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00' + msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_data_rsp_msg(): # 0x1210 + msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01' + msg += total() + msg += hb() + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def config_tsun_allow_all(): Config.act_config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}} @@ -682,7 +747,11 @@ def config_no_tsun_inv1(): @pytest.fixture def config_tsun_inv1(): - Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}} + Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}} + +@pytest.fixture +def config_tsun_dcu1(): + Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}} def test_read_message(device_ind_msg): Config.act_config = {'solarman':{'enabled': True}} @@ -963,6 +1032,34 @@ def test_read_two_messages3(config_tsun_allow_all, device_ind_msg2, device_rsp_m assert m.ifc.tx_fifo.get()==b'' m.close() +def test_read_two_messages4(config_tsun_dcu1, dcu_dev_ind_msg, dcu_dev_rsp_msg, dcu_data_ind_msg, dcu_data_rsp_msg): + _ = config_tsun_dcu1 + m = MemoryStream(dcu_dev_ind_msg, (0,)) + m.append_msg(dcu_data_ind_msg) + assert 0 == m.sensor_list + m._init_new_client_conn() + m.read() # read complete msg, and dispatch msg + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.header_len==11 + assert m.snr == 2070233888 + assert m.unique_id == '2070233888' + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:92' + assert m.msg_recvd[0]['data_len']==314 + assert m.msg_recvd[1]['control']==0x4210 + assert m.msg_recvd[1]['seq']=='02:93' + assert m.msg_recvd[1]['data_len']==111 + assert '3026' == m.db.get_db_value(Register.SENSOR_LIST, None) + assert 0x3026 == m.sensor_list + assert m.ifc.fwd_fifo.get()==dcu_dev_ind_msg+dcu_data_ind_msg + assert m.ifc.tx_fifo.get()==dcu_dev_rsp_msg+dcu_data_rsp_msg + + m._init_new_client_conn() + assert m.ifc.tx_fifo.get()==b'' + m.close() + def test_unkown_frame_code(config_tsun_inv1, inverter_ind_msg_81, inverter_rsp_msg_81): _ = config_tsun_inv1 m = MemoryStream(inverter_ind_msg_81, (0,)) diff --git a/ha_addons/Makefile b/ha_addons/Makefile index 5e51a3a..235370d 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -8,19 +8,65 @@ JINJA = jinja2 IMAGE = tsun-gen3-addon -# Folders +# Source folders for building the local add-on SRC=../app SRC_PROXY=$(SRC)/src CNF_PROXY=$(SRC)/config +# Target folders for building the local add-on and the docker container ADDON_PATH = ha_addon DST=$(ADDON_PATH)/rootfs DST_PROXY=$(DST)/home/proxy +# base director of the add-on repro for installing the add-on git repros INST_BASE=../../ha-addons/ha-addons +# Template folder for build the config.yaml variants TEMPL=templates +# help variable STAGE determine the target to build +dev: STAGE=dev +debug : STAGE=debug +rc : STAGE=rc +rel : STAGE=rel + + +export BUILD_DATE := ${shell date -Iminutes} +BUILD_ID := ${shell date +'%y%m%d%H%M'} +VERSION := $(shell cat $(SRC)/.version) +export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.) + +PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/) +PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/) + + +dev debug: local_add_on + @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) + export VERSION=$(VERSION)-$@-$(BUILD_ID) && \ + export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \ + docker buildx bake -f docker-bake.hcl $@ + +rc rel: local_add_on + @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) + @echo login at $(PUBLIC_URL) as $(PUBLIC_USER) + @DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)" + export VERSION=$(VERSION)-$@ && \ + export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \ + docker buildx bake -f docker-bake.hcl $@ + +clean: + rm -r -f $(DST_PROXY) + rm -f $(DST)/requirements.txt + rm -f $(ADDON_PATH)/config.yaml + rm -f $(TEMPL)/.data.json + docker logout ghcr.io + +############# +# Build the local add-on with a rootfs and config.yaml +# The rootfs is needed to build the add-on Docker container +# +local_add_on: rootfs $(ADDON_PATH)/config.yaml + # collect source files SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\ $(wildcard $(SRC_PROXY)/*.ini)\ @@ -34,50 +80,8 @@ CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml) TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%) CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%) -export BUILD_DATE := ${shell date -Iminutes} -VERSION := $(shell cat $(SRC)/.version) -export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.) - -PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/) -PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/) - - -dev debug: build - @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) - export VERSION=$(VERSION)-$@ && \ - export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \ - docker buildx bake -f docker-bake.hcl $@ - -rc rel: build - @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) - @echo login at $(PUBLIC_URL) as $(PUBLIC_USER) - @DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)" - export VERSION=$(VERSION)-$@ && \ - export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \ - docker buildx bake -f docker-bake.hcl $@ - - -build: rootfs $(ADDON_PATH)/config.yaml repro - -clean: - rm -r -f $(DST_PROXY) - rm -f $(DST)/requirements.txt - rm -f $(ADDON_PATH)/config.yaml - rm -f $(TEMPL)/.data.json - docker logout ghcr.io - -# -# Build rootfs and config.yaml as local add-on -# The rootfs is needed to build the add-on Dockercontainers -# - rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt -STAGE=dev -debug : STAGE=debug -rc : STAGE=rc -rel : STAGE=rel - $(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/% @echo Copy $< to $@ @mkdir -p $(@D) @@ -93,21 +97,22 @@ $(DST)/requirements.txt : $(SRC)/requirements.txt @cp $< $@ $(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json - $(JINJA) --strict -D AppVersion=$(VERSION) --format=json $^ -o $@ + $(JINJA) --strict -D AppVersion=$(VERSION) -D BuildID=$(BUILD_ID) --format=json $^ -o $@ +# build a common data.json file from STAGE depending source files +# don't touch the destination if the checksum of src and dst is equal $(TEMPL)/.data.json: FORCE rsync --checksum $(TEMPL)/$(STAGE)_data.json $@ FORCE : ; -# -# Build repository for Home Assistant Add-On +############# +# Build repository for Home Assistant Add-Onx # -INST=$(INST_BASE)/ha_addon_dev repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml rootfs/run.sh -repro_root = CHANGELOG.md +repro_root = CHANGELOG.md LICENSE.md repro_templates = config.yaml repro_subdirs = translations rootfs repro_vers = debug dev rc rel @@ -117,16 +122,42 @@ repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$( repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file))) -repro: $(repro_all_subdirs) $(repro_all_templates) $(repro_all_files) $(repro_root_files) +debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \ + $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \ + $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_debug/$(file)) \ + $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_debug/$(file)) + +dev: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_dev/$(file)) \ + $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_dev/$(file)) \ + $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_dev/$(file)) \ + $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_dev/$(file)) + +rc: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rc/$(file)) \ + $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rc/$(file)) \ + $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rc/$(file)) \ + $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rc/$(file)) + +rel: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rel/$(file)) \ + $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rel/$(file)) \ + $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rel/$(file)) \ + $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rel/$(file)) $(repro_all_subdirs) : mkdir -p $@ -$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version - $(JINJA) --strict -D AppVersion=$(VERSION)-$* $< $(filter %.json,$^) -o $@ +$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version FORCE + $(JINJA) --strict -D AppVersion=$(VERSION)-$* -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@ -$(repro_root_files) : %/CHANGELOG.md : ../CHANGELOG.md + +$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../% cp $< $@ +$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../% + cp $< $@ +$(filter $(INST_BASE)/ha_addon_rc/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rc/% : ../% + cp $< $@ +$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rel/% : ../% + cp $< $@ + $(filter $(INST_BASE)/ha_addon_debug/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_debug/% : ha_addon/% cp $< $@ @@ -136,5 +167,3 @@ $(filter $(INST_BASE)/ha_addon_rc/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_ cp $< $@ $(filter $(INST_BASE)/ha_addon_rel/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rel/% : ha_addon/% cp $< $@ - - diff --git a/ha_addons/docker-bake.hcl b/ha_addons/docker-bake.hcl index 5c978a2..9c87499 100644 --- a/ha_addons/docker-bake.hcl +++ b/ha_addons/docker-bake.hcl @@ -74,12 +74,12 @@ target "_prod" { } target "debug" { inherits = ["_common", "_debug"] - tags = ["${IMAGE}:debug"] + tags = ["${IMAGE}:debug", "${IMAGE}:${VERSION}"] } target "dev" { inherits = ["_common"] - tags = ["${IMAGE}:dev"] + tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"] } target "preview" { diff --git a/ha_addons/ha_addon/DOCS.md b/ha_addons/ha_addon/DOCS.md index e4ed379..d601798 100644 --- a/ha_addons/ha_addon/DOCS.md +++ b/ha_addons/ha_addon/DOCS.md @@ -68,8 +68,8 @@ Example add-on configuration for GEN3PLUS inverters: inverters: - serial: Y17000000000000 monitor_sn: 2000000000 - node_id: PV-Garage - suggested_area: Garage + node_id: inv_1 + suggested_area: Roof modbus_polling: true client_mode.host: 192.168.x.x client_mode.port: 8899 @@ -84,6 +84,21 @@ inverters: pv4.type: SF-M18/144550 ``` +Example add-on configuration for GEN3PLUS energie storages: + +```yaml +batteries: + - serial: 4100000000000000 + monitor_sn: 2300000000 + node_id: bat_1 + suggested_area: Garage + modbus_polling: false + pv1.manufacturer: Shinefar + pv1.type: SF-M18/144550 + pv2.manufacturer: Shinefar + pv2.type: SF-M18/144550 +``` + **Note**: _This is just an example, you need to replace the values with your own!_ more information about the configuration can be found in the [configuration details page][configdetails]. diff --git a/ha_addons/ha_addon/translations/de.yaml b/ha_addons/ha_addon/translations/de.yaml index cf48599..36ee8cd 100755 --- a/ha_addons/ha_addon/translations/de.yaml +++ b/ha_addons/ha_addon/translations/de.yaml @@ -11,7 +11,20 @@ configuration: Konfigurationsblock gesetzt werden. Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` und die der GEN3PLUS - Wechselrichter mir `Y17`oder `47`! + Wechselrichter mit `Y17`oder `Y47`! + + Siehe Beispielkonfiguration im Dokumentations-Tab + batteries: + name: Batterien + description: >+ + Für jeden Energiespeicher muss die Seriennummer des Speichers einer MQTT + Definition zugeordnet werden. Dazu wird der entsprechende Konfigurationsblock mit der + 16-stellige Seriennummer gestartet, so dass alle nachfolgenden Parameter diesem + Speicher zugeordnet sind. + Weitere speicherspezifische Parameter (z.B. Polling Mode) können im + Konfigurationsblock gesetzt werden. + + Die Seriennummer der GEN3PLUS Batteriespeicher beginnen mit `410`! Siehe Beispielkonfiguration im Dokumentations-Tab @@ -25,14 +38,14 @@ configuration: ein => normaler Proxy-Betrieb. aus => Der Wechselrichter wird vom Internet isoliert. solarman.enabled: - name: Verbindung zur Solarman Cloud - nur für GEN3PLUS Wechselrichter + name: Verbindung zur Solarman/TSUN Cloud - nur für GEN3PLUS Wechselrichter description: >+ - Schaltet die Verbindung zur Solarman Cloud ein/aus. - Diese Verbindung ist erforderlich, wenn Sie Daten an die Solarman Cloud senden möchten, - z.B. um die Solarman Apps zu nutzen oder Firmware-Updates zu erhalten. + Schaltet die Verbindung zur Solarman oder TSUN Cloud ein/aus. + Diese Verbindung ist erforderlich, wenn Sie Daten an die Cloud senden möchten, + z.B. um die Solarman App oder TSUN Smart App zu nutzen oder Firmware-Updates zu erhalten. ein => normaler Proxy-Betrieb. - aus => Der Wechselrichter wird vom Internet isoliert. + aus => Die GEN3PLUS Geräte werden vom Internet isoliert. inverters.allow_all: name: Erlaube Verbindungen von sämtlichen Wechselrichtern description: >- diff --git a/ha_addons/ha_addon/translations/en.yaml b/ha_addons/ha_addon/translations/en.yaml index 42d01da..7ffb6f3 100755 --- a/ha_addons/ha_addon/translations/en.yaml +++ b/ha_addons/ha_addon/translations/en.yaml @@ -7,13 +7,27 @@ configuration: definition. To do this, the corresponding configuration block is started with 16-digit serial number so that all subsequent parameters are assigned to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set - in the configuration block + in the configuration block. The serial numbers of all GEN3 inverters start with `R17` and that of the GEN3PLUS - inverters with ‘Y17’ or ‘47’! + inverters with ‘Y17’ or ‘Y47’! For reference see example configuration in Documentation Tab + batteries: + name: Energy Storages + description: >+ + For each energy storage device, the serial number of the storage device must be + assigned to an MQTT definition. To do this, the corresponding configuration block + is started with the 16-digit serial number so that all subsequent parameters are + assigned to this energy storage. Further inverter-specific parameters (e.g. polling + mode) can be set in the configuration block. + + The serial numbers of all GEN3PLUS energy storages start with ‘410’! + + For reference see example configuration in Documentation Tab + + tsun.enabled: name: Connection to TSUN Cloud - for GEN3 inverter only description: >+ @@ -24,14 +38,14 @@ configuration: on => normal proxy operation. off => The Inverter become isolated from Internet. solarman.enabled: - name: Connection to Solarman Cloud - for GEN3PLUS inverter only + name: Connection to Solarman/TSUN Cloud - for GEN3PLUS inverter only description: >+ - switch on/off connection to the Solarman cloud. - This connection is only required if you want send data to the Solarman cloud - eg. to use the Solarman APPs or receive firmware updates. + switch on/off connection to the Solarman or TSUN cloud. + This connection is only required if you want send data to the cloud + eg. to use the Solarman APP, the TSUN Smart APP or receive firmware updates. on => normal proxy operation. - off => The Inverter become isolated from Internet + off => The GEN3PLUS devices become isolated from Internet inverters.allow_all: name: Allow all connections from all inverters description: >- diff --git a/ha_addons/templates/config.jinja b/ha_addons/templates/config.jinja index 7ffbc1d..cc47edc 100755 --- a/ha_addons/templates/config.jinja +++ b/ha_addons/templates/config.jinja @@ -1,6 +1,6 @@ name: {{name}} description: {{description}} -version: {% if version is defined and version|length %} {{version}} {% else %} {{AppVersion}} {% endif %} +version: {% if version is defined and version|length %} {{version}} {% elif BuildID is defined and BuildID|length %} {{AppVersion}}-{{BuildID}} {% else %} {{AppVersion}} {% endif %} image: {{image}} url: https://github.com/s-allius/tsun-gen3-proxy slug: {{slug}} @@ -30,7 +30,6 @@ ports: # Definition of parameters in the configuration tab of the addon # parameters are available within the container as /data/options.json # and should become picked up by the proxy - current workaround as a transfer script -# TODO: check again for multi hierarchie parameters schema: inverters: @@ -42,11 +41,6 @@ schema: client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)? client_mode.port: port? client_mode.forward: bool? - #strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt. - # - string: str - # type: str - # manufacturer: str - # daher diese variante pv1.manufacturer: str? pv1.type: str? pv2.manufacturer: str? @@ -62,6 +56,19 @@ schema: tsun.enabled: bool solarman.enabled: bool inverters.allow_all: bool + batteries: + - serial: match(^(410).{13}$) + monitor_sn: int + node_id: str + suggested_area: str + modbus_polling: bool + client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)? + client_mode.port: port? + client_mode.forward: bool? + pv1.manufacturer: str? + pv1.type: str? + pv2.manufacturer: str? + pv2.type: str? # optionale parameter @@ -90,17 +97,21 @@ schema: # If any default value is given, the option becomes a required value. options: inverters: - - serial: R17E760702080400 - node_id: PV-Garage + - serial: R17E000000000000 + monitor_sn: 0 + node_id: inv_1 + suggested_area: Roof + modbus_polling: false + pv1.manufacturer: Shinefar + pv1.type: SF-M18/144550 + pv2.manufacturer: Shinefar + pv2.type: SF-M18/144550 + batteries: + - serial: 4100000000000000 + monitor_sn: 0 + node_id: bat_1 suggested_area: Garage modbus_polling: false - # strings: - # - string: PV1 - # type: SF-M18/144550 - # manufacturer: Shinefar - # - string: PV2 - # type: SF-M18/144550 - # manufacturer: Shinefar pv1.manufacturer: Shinefar pv1.type: SF-M18/144550 pv2.manufacturer: Shinefar diff --git a/ha_addons/templates/debug_data.json b/ha_addons/templates/debug_data.json index bd876fa..fcfdfe7 100644 --- a/ha_addons/templates/debug_data.json +++ b/ha_addons/templates/debug_data.json @@ -2,7 +2,6 @@ { "name": "TSUN-Proxy (Debug)", "description": "MQTT Proxy for TSUN Photovoltaic Inverters with Debug Logging", - "version": "debug", "image": "docker.io/sallius/tsun-gen3-addon", "slug": "tsun-proxy-debug", "advanced": true, diff --git a/ha_addons/templates/dev_data.json b/ha_addons/templates/dev_data.json index d033c12..a7fbb82 100644 --- a/ha_addons/templates/dev_data.json +++ b/ha_addons/templates/dev_data.json @@ -2,7 +2,6 @@ { "name": "TSUN-Proxy (Dev)", "description": "MQTT Proxy for TSUN Photovoltaic Inverters", - "version": "dev", "image": "docker.io/sallius/tsun-gen3-addon", "slug": "tsun-proxy-dev", "advanced": false, diff --git a/sonar-project.properties b/sonar-project.properties index 61d8dbd..612d2d1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -15,6 +15,10 @@ sonar.sources=app/src/ sonar.python.version=3.12 sonar.tests=system_tests/,app/tests/ sonar.exclusions=**/.vscode/**/* + +# disable code dupication check for config grammar +sonar.cpd.exclusions=app/src/cnf/config.py + # Name your criteria sonar.issue.ignore.multicriteria=e1,e2 diff --git a/system_tests/test_tcp_socket_v2.py b/system_tests/test_tcp_socket_v2.py index b3521a8..4bf904b 100644 --- a/system_tests/test_tcp_socket_v2.py +++ b/system_tests/test_tcp_socket_v2.py @@ -10,6 +10,12 @@ SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080') def get_sn() -> bytes: return bytes.fromhex(SOLARMAN_SNR) +def get_dcu_sn() -> bytes: + return b'\x20\x43\x65\x7b' + +def get_dcu_no() -> bytes: + return b'4100000000000001' + def get_inv_no() -> bytes: return b'T170000000000001' @@ -105,6 +111,62 @@ def MsgInvalidInfo(): # Contact Info message wrong start byte msg += b'\x15' return msg +@pytest.fixture +def dcu_dev_ind_msg(): # 0x4110 + msg = b'\xa5\x3a\x01\x10\x41\x00\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32' + msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53' + msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35' + msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e' + msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26' + msg += b'\x30\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x7a\x75\x68\x61\x75\x73\x65\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x01\x01\x01\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_dev_rsp_msg(): # 0x1110 + msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01\x4a\xf6\xa6' + msg += b'\x67\x3c\x00\x00\x00' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_data_ind_msg(): # 0x4210 + msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde' + msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00' + msg += get_dcu_no() + msg += b'\x0d\x3a\x00\x00\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00' + msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89' + msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89' + msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00' + msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def dcu_data_rsp_msg(): # 0x1210 + msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01\xd1\x96\x04' + msg += b'\x66\x3c\x00\x00\x00' + msg += correct_checksum(msg) + msg += b'\x15' + return msg @pytest.fixture(scope="session") def ClientConnection(): @@ -181,4 +243,24 @@ def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactR # time.sleep(2.5) checkResponse(data, MsgContactResp) - \ No newline at end of file +def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg): + s = ClientConnection + try: + s.sendall(dcu_dev_ind_msg) + # time.sleep(2.5) + data = s.recv(1024) + except TimeoutError: + pass + # time.sleep(2.5) + checkResponse(data, dcu_dev_rsp_msg) + +def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg): + s = ClientConnection + try: + s.sendall(dcu_data_ind_msg) + # time.sleep(2.5) + data = s.recv(1024) + except TimeoutError: + pass + # time.sleep(2.5) + checkResponse(data, dcu_data_rsp_msg)