From 1e160f3b0fc7a2f3079ebb74f50b8da73fcd8d7f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 23 Dec 2024 00:10:57 +0100 Subject: [PATCH 01/23] set verion 0.13 --- app/.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/.version b/app/.version index d33c3a2..51de330 100644 --- a/app/.version +++ b/app/.version @@ -1 +1 @@ -0.12.0 \ No newline at end of file +0.13.0 \ No newline at end of file From 3234e87b55728c52d5680e63b307366874a7900c Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Tue, 24 Dec 2024 00:13:32 +0100 Subject: [PATCH 02/23] S allius/issue180 (#265) * move default_config.toml into src/cnf/. * improve file handling * remove obsolete rules --- CHANGELOG.md | 2 ++ app/Dockerfile | 1 - app/Makefile | 33 --------------------- app/{config => src/cnf}/default_config.toml | 0 app/src/server.py | 6 ++-- app/tests/test_config.py | 20 ++++++------- app/tests/test_config_read_env.py | 2 +- app/tests/test_config_read_json.py | 10 +++---- ha_addons/Makefile | 1 + 9 files changed, 23 insertions(+), 52 deletions(-) rename app/{config => src/cnf}/default_config.toml (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 380f10c..dc0aa57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180) + ## [0.12.0] - 2024-12-22 - add hadolint configuration diff --git a/app/Dockerfile b/app/Dockerfile index 84ffe88..df85b81 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -59,7 +59,6 @@ RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && # copy the content of the local src and config directory to the working directory COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh -COPY config . COPY src . RUN echo ${VERSION} > /proxy-version.txt \ && date > /build-date.txt diff --git a/app/Makefile b/app/Makefile index b68f782..fb96df2 100644 --- a/app/Makefile +++ b/app/Makefile @@ -7,23 +7,6 @@ IMAGE = tsun-gen3-proxy # Folders SRC=. -SRC_PROXY=$(SRC)/src -CNF_PROXY=$(SRC)/config - -DST=rootfs -DST_PROXY=$(DST)/home/proxy - -# collect source files -SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\ - $(wildcard $(SRC_PROXY)/*.ini)\ - $(wildcard $(SRC_PROXY)/cnf/*.py)\ - $(wildcard $(SRC_PROXY)/gen3/*.py)\ - $(wildcard $(SRC_PROXY)/gen3plus/*.py) -CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml) - -# determine destination files -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) @@ -48,20 +31,4 @@ preview rc rel: docker buildx bake -f docker-bake.hcl $@ - .PHONY: debug dev preview rc rel - - -$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/% - @echo Copy $< to $@ - @mkdir -p $(@D) - @cp $< $@ - -$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/% - @echo Copy $< to $@ - @mkdir -p $(@D) - @cp $< $@ - -$(DST)/requirements.txt : $(SRC)/requirements.txt - @echo Copy $< to $@ - @cp $< $@ diff --git a/app/config/default_config.toml b/app/src/cnf/default_config.toml similarity index 100% rename from app/config/default_config.toml rename to app/src/cnf/default_config.toml diff --git a/app/src/server.py b/app/src/server.py index e7c44af..abc9aae 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -156,8 +156,10 @@ def main(): # pragma: no cover setattr(logging.handlers, "log_path", args.log_path) setattr(logging.handlers, "log_backups", args.log_backups) + os.makedirs(args.log_path, exist_ok=True) - logging.config.fileConfig('logging.ini') + src_dir = os.path.dirname(__file__) + '/' + logging.config.fileConfig(src_dir + 'logging.ini') logging.info(f'Server "{serv_name} - {version}" will be started') logging.info(f'current dir: {os.getcwd()}') logging.info(f"config_path: {args.config_path}") @@ -184,7 +186,7 @@ def main(): # pragma: no cover asyncio.set_event_loop(loop) # read config file - Config.init(ConfigReadToml("default_config.toml")) + Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml")) ConfigReadEnv() ConfigReadJson(args.config_path + "config.json") ConfigReadToml(args.config_path + "config.toml") diff --git a/app/tests/test_config.py b/app/tests/test_config.py index d229dac..96b7478 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -144,7 +144,7 @@ def ConfigComplete(): } def test_default_config(): - Config.init(ConfigReadToml("app/config/default_config.toml")) + 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'}, 'inverters': { @@ -193,7 +193,7 @@ def test_full_config(ConfigComplete): def test_read_empty(ConfigDefault): test_buffer.rd = "" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -216,14 +216,14 @@ def test_no_file(): assert defcnf == None def test_no_file2(): - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) assert Config.err == None ConfigReadToml("_no__file__no_") err = Config.get_error() assert err == None def test_invalid_filename(): - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) assert Config.err == None ConfigReadToml(None) err = Config.get_error() @@ -232,7 +232,7 @@ def test_invalid_filename(): def test_read_cnf1(): test_buffer.rd = "solarman.enabled = false" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -279,7 +279,7 @@ def test_read_cnf1(): def test_read_cnf2(): test_buffer.rd = "solarman.enabled = 'FALSE'" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -322,7 +322,7 @@ def test_read_cnf2(): def test_read_cnf3(ConfigDefault): test_buffer.rd = "solarman.port = 'FALSE'" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -334,7 +334,7 @@ def test_read_cnf3(ConfigDefault): def test_read_cnf4(): test_buffer.rd = "solarman.port = 5000" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -377,7 +377,7 @@ def test_read_cnf4(): def test_read_cnf5(): test_buffer.rd = "solarman.port = 1023" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() @@ -386,7 +386,7 @@ def test_read_cnf5(): def test_read_cnf6(): test_buffer.rd = "solarman.port = 65536" - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadToml("config/config.toml") err = Config.get_error() diff --git a/app/tests/test_config_read_env.py b/app/tests/test_config_read_env.py index 3bf33fc..b9ba8f4 100644 --- a/app/tests/test_config_read_env.py +++ b/app/tests/test_config_read_env.py @@ -44,7 +44,7 @@ def test_extend_key(): assert conf == {'': 'testuser'} def test_read_env_config(): - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None} for _ in patch_getenv(): diff --git a/app/tests/test_config_read_json.py b/app/tests/test_config_read_json.py index 696a529..e1d4c6a 100644 --- a/app/tests/test_config_read_json.py +++ b/app/tests/test_config_read_json.py @@ -84,7 +84,7 @@ def ConfigTomlEmpty(): def test_no_config(ConfigDefault): test_buffer.rd = "" # empty buffer, no json - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadJson() err = Config.get_error() @@ -96,7 +96,7 @@ def test_no_config(ConfigDefault): def test_no_file(ConfigDefault): test_buffer.rd = "" # empty buffer, no json - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadJson("_no__file__no_") err = Config.get_error() @@ -108,7 +108,7 @@ def test_no_file(ConfigDefault): def test_invalid_filename(ConfigDefault): test_buffer.rd = "" # empty buffer, no json - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadJson(None) err = Config.get_error() @@ -340,7 +340,7 @@ def test_cnv6(): def test_empty_config(ConfigDefault): test_buffer.rd = "{}" # empty json - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadJson() err = Config.get_error() @@ -401,7 +401,7 @@ def test_full_config(ConfigComplete): ] } """ - Config.init(ConfigReadToml("app/config/default_config.toml")) + Config.init(ConfigReadToml("app/src/cnf/default_config.toml")) for _ in patch_open(): ConfigReadJson() err = Config.get_error() diff --git a/ha_addons/Makefile b/ha_addons/Makefile index b0faba8..bbfe982 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -25,6 +25,7 @@ TEMPL=templates SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\ $(wildcard $(SRC_PROXY)/*.ini)\ $(wildcard $(SRC_PROXY)/cnf/*.py)\ + $(wildcard $(SRC_PROXY)/cnf/*.toml)\ $(wildcard $(SRC_PROXY)/gen3/*.py)\ $(wildcard $(SRC_PROXY)/gen3plus/*.py) CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml) From f5d760e2f067f6ed0683e3dfa0e7ea405291982d Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 24 Dec 2024 14:14:56 +0100 Subject: [PATCH 03/23] Change wiki paths --- README.md | 12 ++++++------ app/src/cnf/default_config.toml | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 408c08b..186c828 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ No special configuration is required for the Docker container if it is built and On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files. -A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#docker-compose-environment-variables). +A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#docker-compose-environment-variables). ## Proxy Configuration @@ -168,7 +168,7 @@ You find more details here: ### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview ### ### Here you will find a description of all configuration options: -### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details +### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml ### ### The configration uses the TOML format, which aims to be easy to read due to ### obvious semantics. You find more details here: https://toml.io/en/v1.0.0 @@ -184,7 +184,7 @@ You find more details here: ## required credentials. As the proxy does not currently support an encrypted connection ## to the MQTT broker, it is strongly recommended that you do not use a public broker. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account ## mqtt.host = 'mqtt' # URL or IP address of the mqtt broker @@ -201,7 +201,7 @@ mqtt.passwd = '' ## values match the HA default configuration. If you need to change these or want to use ## a different MQTT client, you can adjust the prefixes of the MQTT topics below. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant ## ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates @@ -219,7 +219,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox ## inverters. This connection is only required if you want send data to the TSUN cloud ## to use the TSUN APPs or receive firmware updates. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only ## tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates @@ -235,7 +235,7 @@ tsun.port = 5005 ## inverters. This connection is only required if you want send data to the TSUN cloud ## to use the TSUN APPs or receive firmware updates. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#solarman-cloud-for-gen3plus-inverter-only ## solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates solarman.host = 'iot.talent-monitoring.com' diff --git a/app/src/cnf/default_config.toml b/app/src/cnf/default_config.toml index f4b9364..6c9ba77 100644 --- a/app/src/cnf/default_config.toml +++ b/app/src/cnf/default_config.toml @@ -15,7 +15,7 @@ ### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview ### ### Here you will find a description of all configuration options: -### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details +### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml ### ### The configration uses the TOML format, which aims to be easy to read due to ### obvious semantics. You find more details here: https://toml.io/en/v1.0.0 @@ -31,7 +31,7 @@ ## required credentials. As the proxy does not currently support an encrypted connection ## to the MQTT broker, it is strongly recommended that you do not use a public broker. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account ## mqtt.host = 'mqtt' # URL or IP address of the mqtt broker @@ -48,7 +48,7 @@ mqtt.passwd = '' ## values match the HA default configuration. If you need to change these or want to use ## a different MQTT client, you can adjust the prefixes of the MQTT topics below. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant ## ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates @@ -66,7 +66,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox ## inverters. This connection is only required if you want send data to the TSUN cloud ## to use the TSUN APPs or receive firmware updates. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only ## tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates @@ -82,7 +82,7 @@ tsun.port = 5005 ## inverters. This connection is only required if you want send data to the TSUN cloud ## to use the TSUN APPs or receive firmware updates. ## -## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only +## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#solarman-cloud-for-gen3plus-inverter-only ## solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates From 42fe33bacf04d6337173568385031414f1d53b01 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 11 Feb 2025 00:08:57 +0100 Subject: [PATCH 04/23] add initial DCU support --- CHANGELOG.md | 3 +++ app/src/gen3plus/infos_g3p.py | 33 +++++++++++++++++++++----- app/src/gen3plus/solarman_emu.py | 2 +- app/src/gen3plus/solarman_v5.py | 8 +++---- app/tests/test_infos_g3p.py | 40 ++++++++++++++++---------------- 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb09b5..a52e212 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 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) ## [0.12.1] - 2025-01-13 diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 417487a..0a6264f 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -1,5 +1,6 @@ from typing import Generator +from itertools import chain from infos import Infos, Register, ProxyMode, Fmt @@ -32,7 +33,8 @@ class RegisterMap: 0x4102008e: {'reg': None, 'fmt': ' suggested area string from the config file''' # iterate over RegisterMap.map and get the register values - for row in RegisterMap.map.values(): + for _, row in chain(RegisterMap.map.items(), + RegisterMap.map_02b0.items(), + RegisterMap.map_3026.items()): info_id = row['reg'] if self.__hide_topic(row): res = self.ha_remove(info_id, node_id, snr) # noqa: E501 @@ -153,13 +173,14 @@ class InfosG3P(Infos): if res: yield res - def parse(self, buf, msg_type: int, rcv_ftype: int, node_id: str = '') \ + def parse(self, buf, msg_type: int, rcv_ftype: int, + sensor: int = 0, node_id: str = '') \ -> Generator[tuple[str, bool], None, None]: '''parse a data sequence received from the inverter and stores the values in Infos.db buf: buffer of the sequence to parse''' - for idx, row in RegisterMap.map.items(): + for idx, row in RegisterSel.get(sensor).items(): addr = idx & 0xffff ftype = (idx >> 16) & 0xff mtype = (idx >> 24) & 0xff @@ -183,9 +204,9 @@ class InfosG3P(Infos): self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}' f' : {result}{unit}') - def build(self, len, msg_type: int, rcv_ftype: int): + def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0): buf = bytearray(len) - for idx, row in RegisterMap.map.items(): + for idx, row in RegisterSel.get(sensor).items(): addr = idx & 0xffff ftype = (idx >> 16) & 0xff mtype = (idx >> 24) & 0xff diff --git a/app/src/gen3plus/solarman_emu.py b/app/src/gen3plus/solarman_emu.py index 66035bb..7462388 100644 --- a/app/src/gen3plus/solarman_emu.py +++ b/app/src/gen3plus/solarman_emu.py @@ -103,7 +103,7 @@ class SolarmanEmu(SolarmanBase): self.data_timer.start(self.data_up_inv) _len = 420 ftype = 1 - build_msg = self.db.build(_len, 0x42, ftype) + build_msg = self.db.build(_len, 0x42, ftype, 0x02b0) self._build_header(0x4210) self.ifc.tx_add( diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 463c043..837b78c 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -516,11 +516,11 @@ class SolarmanV5(SolarmanBase): logger.info(f'Model: {model}') self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model) - def __process_data(self, ftype, ts): + def __process_data(self, ftype, ts, sensor=0): inv_update = False msg_type = self.control >> 8 - for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype, - self.node_id): + for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, + ftype, sensor, self.node_id): if update: if key == 'inverter': inv_update = True @@ -581,7 +581,7 @@ class SolarmanV5(SolarmanBase): else: ts = None - self.__process_data(ftype, ts) + self.__process_data(ftype, ts, sensor) self.__forward_msg() self.__send_ack_rsp(0x1210, ftype) self.new_state_up() diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index e0cac05..7da5924 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -105,7 +105,7 @@ def test_parse_4210(inverter_data: bytes): i = InfosG3P(client_mode=False) i.db.clear() - for key, update in i.parse (inverter_data, 0x42, 1): + for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0): pass # side effect is calling generator i.parse() assert json.dumps(i.db) == json.dumps({ @@ -127,10 +127,10 @@ 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): + for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0): pass # side effect is calling generator i.parse() - build_msg = i.build(len(inverter_data), 0x42, 1) + build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0) for i in range(11, 31): build_msg[i] = inverter_data[i] assert inverter_data == build_msg @@ -286,53 +286,53 @@ def test_build_ha_conf4(): def test_exception_and_calc(inverter_data: bytes): # patch table to convert temperature from °F to °C - ofs = RegisterMap.map[0x420100d8]['offset'] - RegisterMap.map[0x420100d8]['quotient'] = 1.8 - RegisterMap.map[0x420100d8]['offset'] = -32/1.8 + ofs = RegisterMap.map_02b0[0x420100d8]['offset'] + RegisterMap.map_02b0[0x420100d8]['quotient'] = 1.8 + RegisterMap.map_02b0[0x420100d8]['offset'] = -32/1.8 # map PV1_VOLTAGE to invalid register - RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2 + RegisterMap.map_02b0[0x420100e0]['reg'] = Register.TEST_REG2 # set invalid maping entry for OUTPUT_POWER (string instead of dict type) - backup = RegisterMap.map[0x420100de] - RegisterMap.map[0x420100de] = 'invalid_entry' + backup = RegisterMap.map_02b0[0x420100de] + RegisterMap.map_02b0[0x420100de] = 'invalid_entry' i = InfosG3P(client_mode=False) i.db.clear() - for key, update in i.parse (inverter_data, 0x42, 1): + for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0): 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) + build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0) 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'] + del RegisterMap.map_02b0[0x420100d8]['quotient'] + del RegisterMap.map_02b0[0x420100d8]['offset'] i.db.clear() - for key, update in i.parse (inverter_data, 0x42, 1): + for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0): 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) + build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0) 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 + RegisterMap.map_02b0[0x420100d8]['offset'] = ofs + RegisterMap.map_02b0[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping + RegisterMap.map_02b0[0x420100de] = backup # reset mapping # test orginial table i.db.clear() - for key, update in i.parse (inverter_data, 0x42, 1): + for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0): 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) + build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0) assert build_msg[32:-1] == inverter_data[32:-1] From 4df36e267233ec52ce1aebd3464fc685c7d77765 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 11 Feb 2025 20:20:48 +0100 Subject: [PATCH 05/23] revert AddOn base docker image to version 17.1.0 --- CHANGELOG.md | 1 + ha_addons/ha_addon/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52e212..d424a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- revert AddOn base docker image to version 17.1.0 - add initial DCU support - update AddOn base docker image to version 17.1.2 - update aiohttp to version 3.11.12 diff --git a/ha_addons/ha_addon/Dockerfile b/ha_addons/ha_addon/Dockerfile index 9160b73..b4c20fc 100755 --- a/ha_addons/ha_addon/Dockerfile +++ b/ha_addons/ha_addon/Dockerfile @@ -13,7 +13,7 @@ # 1 Build Base Image # ###################### -ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.1.3" +ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.1.0" # hadolint ignore=DL3006 FROM $BUILD_FROM AS base From 5f0a35d55bbc5b0411e68e65a5a5f93063d706ca Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 11 Feb 2025 20:45:01 +0100 Subject: [PATCH 06/23] Update AddOn base docker image to version 17.1.3 and python3 to 3.12.9-r0 --- CHANGELOG.md | 2 +- ha_addons/ha_addon/Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d424a46..07d5a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] -- revert AddOn base docker image to version 17.1.0 +- Update AddOn base docker image to version 17.1.3 and 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 diff --git a/ha_addons/ha_addon/Dockerfile b/ha_addons/ha_addon/Dockerfile index b4c20fc..e924844 100755 --- a/ha_addons/ha_addon/Dockerfile +++ b/ha_addons/ha_addon/Dockerfile @@ -13,12 +13,12 @@ # 1 Build Base Image # ###################### -ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.1.0" +ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.1.3" # hadolint ignore=DL3006 FROM $BUILD_FROM AS base # Installiere Python, pip und virtuelle Umgebungstools -RUN apk add --no-cache python3=3.12.8-r1 py3-pip=24.3.1-r0 && \ +RUN apk add --no-cache python3=3.12.9-r0 py3-pip=24.3.1-r0 && \ python -m venv /opt/venv && \ . /opt/venv/bin/activate From a257f09d4cac4bd0869baf5c8379aed04547bb88 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 11 Feb 2025 20:45:54 +0100 Subject: [PATCH 07/23] add ghcr logout for the clean target --- ha_addons/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/ha_addons/Makefile b/ha_addons/Makefile index edd6a5f..5e51a3a 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -64,6 +64,7 @@ clean: 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 From ec3af69e624b70afb7bc64085ad2406458f3f1d9 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 23 Feb 2025 14:17:57 +0100 Subject: [PATCH 08/23] S allius/issue288 (#289) * remove apostrophes from fmt strings - thanks to @onkelenno for the suggestion * improve the logger initializing - don't overwrite the logging.ini settings if the env variable LOG_LVL isn't well defined - Thanks to @onkelenno for the idea to improve * set default argument for LOG_LVL to INFO in docker files * adapt unit test --- CHANGELOG.md | 2 ++ app/Dockerfile | 2 +- app/src/logging.ini | 8 ++++---- app/src/server.py | 36 +++++++++++++++++------------------ app/tests/test_server.py | 14 +++++++++++--- ha_addons/ha_addon/Dockerfile | 2 ++ 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d5a82..abc9f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- 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 - add initial DCU support - update AddOn base docker image to version 17.1.2 diff --git a/app/Dockerfile b/app/Dockerfile index df85b81..b9c5362 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -30,7 +30,7 @@ ARG SERVICE_NAME ARG VERSION ARG UID ARG GID -ARG LOG_LVL +ARG LOG_LVL=INFO ARG environment ENV SERVICE_NAME=$SERVICE_NAME diff --git a/app/src/logging.ini b/app/src/logging.ini index 88f15ef..6be3905 100644 --- a/app/src/logging.ini +++ b/app/src/logging.ini @@ -67,10 +67,10 @@ formatter=file_formatter args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups) [formatter_console_formatter] -format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s' -datefmt='%Y-%m-%d %H:%M:%S +format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s +datefmt=%Y-%m-%d %H:%M:%S [formatter_file_formatter] -format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s' -datefmt='%Y-%m-%d %H:%M:%S +format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s +datefmt=%Y-%m-%d %H:%M:%S diff --git a/app/src/server.py b/app/src/server.py index abc9aae..6056eb9 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -117,19 +117,19 @@ async def handle_shutdown(loop, web_task): loop.stop() -def get_log_level() -> int: +def get_log_level() -> int | None: '''checks if LOG_LVL is set in the environment and returns the corresponding logging.LOG_LEVEL''' - log_level = os.getenv('LOG_LVL', 'INFO') + switch = { + 'DEBUG': logging.DEBUG, + 'WARN': logging.WARNING, + 'INFO': logging.INFO, + 'ERROR': logging.ERROR, + } + log_level = os.getenv('LOG_LVL', None) logging.info(f"LOG_LVL : {log_level}") - if log_level == 'DEBUG': - log_level = logging.DEBUG - elif log_level == 'WARN': - log_level = logging.WARNING - else: - log_level = logging.INFO - return log_level + return switch.get(log_level, None) def main(): # pragma: no cover @@ -172,15 +172,15 @@ def main(): # pragma: no cover logging.info(f"log_backups: {args.log_backups} days") log_level = get_log_level() logging.info('******') - - # set lowest-severity for 'root', 'msg', 'conn' and 'data' logger - logging.getLogger().setLevel(log_level) - logging.getLogger('msg').setLevel(log_level) - logging.getLogger('conn').setLevel(log_level) - logging.getLogger('data').setLevel(log_level) - logging.getLogger('tracer').setLevel(log_level) - logging.getLogger('asyncio').setLevel(log_level) - # logging.getLogger('mqtt').setLevel(log_level) + if log_level: + # set lowest-severity for 'root', 'msg', 'conn' and 'data' logger + logging.getLogger().setLevel(log_level) + logging.getLogger('msg').setLevel(log_level) + logging.getLogger('conn').setLevel(log_level) + logging.getLogger('data').setLevel(log_level) + logging.getLogger('tracer').setLevel(log_level) + logging.getLogger('asyncio').setLevel(log_level) + # logging.getLogger('mqtt').setLevel(log_level) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) diff --git a/app/tests/test_server.py b/app/tests/test_server.py index 367bf5b..020c8c9 100644 --- a/app/tests/test_server.py +++ b/app/tests/test_server.py @@ -7,18 +7,26 @@ from server import get_log_level def test_get_log_level(): - with patch.dict(os.environ, {'LOG_LVL': ''}): + with patch.dict(os.environ, {}): log_lvl = get_log_level() - assert log_lvl == logging.INFO + assert log_lvl == None with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}): log_lvl = get_log_level() assert log_lvl == logging.DEBUG + with patch.dict(os.environ, {'LOG_LVL': 'INFO'}): + log_lvl = get_log_level() + assert log_lvl == logging.INFO + with patch.dict(os.environ, {'LOG_LVL': 'WARN'}): log_lvl = get_log_level() assert log_lvl == logging.WARNING + with patch.dict(os.environ, {'LOG_LVL': 'ERROR'}): + log_lvl = get_log_level() + assert log_lvl == logging.ERROR + with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}): log_lvl = get_log_level() - assert log_lvl == logging.INFO + assert log_lvl == None diff --git a/ha_addons/ha_addon/Dockerfile b/ha_addons/ha_addon/Dockerfile index 78d4dcd..a9468cb 100755 --- a/ha_addons/ha_addon/Dockerfile +++ b/ha_addons/ha_addon/Dockerfile @@ -47,6 +47,8 @@ FROM base AS runtime ARG SERVICE_NAME ARG VERSION +ARG LOG_LVL=INFO +ENV LOG_LVL=$LOG_LVL ENV SERVICE_NAME=${SERVICE_NAME} 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 09/23] 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) From 3f3ed1b14febba121539412415d04d58756c5e0f Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:11:32 +0100 Subject: [PATCH 10/23] add watchdog for Add-ons (#291) --- CHANGELOG.md | 1 + ha_addons/templates/config.jinja | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dee3ed..347924e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add watchdog for Add-ons - 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.2.1 diff --git a/ha_addons/templates/config.jinja b/ha_addons/templates/config.jinja index cc47edc..b4f6a43 100755 --- a/ha_addons/templates/config.jinja +++ b/ha_addons/templates/config.jinja @@ -24,8 +24,7 @@ ports: 5005/tcp: 5005 10000/tcp: 10000 -# FIXME: we disabled the watchdog due to exceptions in the ha supervisor. See: https://github.com/s-allius/tsun-gen3-proxy/issues/249 -# watchdog: "http://[HOST]:[PORT:8127]/-/healthy" +watchdog: "http://[HOST]:[PORT:8127]/-/healthy" # Definition of parameters in the configuration tab of the addon # parameters are available within the container as /data/options.json From 8a2ca3ab9ac98a3b5329e754cb854b073cf57c2a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 27 Feb 2025 22:43:07 +0100 Subject: [PATCH 11/23] fix the build target --- ha_addons/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ha_addons/Makefile b/ha_addons/Makefile index 235370d..44e5b78 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -25,7 +25,7 @@ INST_BASE=../../ha-addons/ha-addons TEMPL=templates # help variable STAGE determine the target to build -dev: STAGE=dev +STAGE=dev debug : STAGE=debug rc : STAGE=rc rel : STAGE=rel @@ -39,6 +39,7 @@ 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/) +build: local_add_on dev debug: local_add_on @echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) From 06ceb02f0d7a9b300aa795dda6c97d475e082178 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 27 Feb 2025 22:50:24 +0100 Subject: [PATCH 12/23] ignore apparmor.txt --- ha_addons/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ha_addons/.gitignore b/ha_addons/.gitignore index 1e162f6..8a014ce 100644 --- a/ha_addons/.gitignore +++ b/ha_addons/.gitignore @@ -1,2 +1,3 @@ .data.json -config.yaml \ No newline at end of file +config.yaml +apparmor.txt \ No newline at end of file From 10b4a84701164db3d8a2fb53380a6edb63df95ac Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:05:36 +0100 Subject: [PATCH 13/23] allow `R47`serial numbers for GEN3 inverters (#302) --- CHANGELOG.md | 1 + ha_addons/ha_addon/translations/de.yaml | 2 +- ha_addons/ha_addon/translations/en.yaml | 2 +- ha_addons/templates/config.jinja | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347924e..4fd0faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- allow `R47`serial numbers for GEN3 inverters - add watchdog for Add-ons - 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) diff --git a/ha_addons/ha_addon/translations/de.yaml b/ha_addons/ha_addon/translations/de.yaml index 36ee8cd..0455987 100755 --- a/ha_addons/ha_addon/translations/de.yaml +++ b/ha_addons/ha_addon/translations/de.yaml @@ -10,7 +10,7 @@ configuration: Weitere wechselrichterspezifische Parameter (z.B. Polling Mode) können im Konfigurationsblock gesetzt werden. - Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` und die der GEN3PLUS + Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` oder `R47` und die der GEN3PLUS Wechselrichter mit `Y17`oder `Y47`! Siehe Beispielkonfiguration im Dokumentations-Tab diff --git a/ha_addons/ha_addon/translations/en.yaml b/ha_addons/ha_addon/translations/en.yaml index 7ffb6f3..82a25aa 100755 --- a/ha_addons/ha_addon/translations/en.yaml +++ b/ha_addons/ha_addon/translations/en.yaml @@ -9,7 +9,7 @@ configuration: to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set in the configuration block. - The serial numbers of all GEN3 inverters start with `R17` and that of the GEN3PLUS + The serial numbers of all GEN3 inverters start with `R17` or `R47` and that of the GEN3PLUS inverters with ‘Y17’ or ‘Y47’! For reference see example configuration in Documentation Tab diff --git a/ha_addons/templates/config.jinja b/ha_addons/templates/config.jinja index b4f6a43..f17a758 100755 --- a/ha_addons/templates/config.jinja +++ b/ha_addons/templates/config.jinja @@ -32,7 +32,7 @@ watchdog: "http://[HOST]:[PORT:8127]/-/healthy" schema: inverters: - - serial: match(^(R17|Y17|Y47).{13}$) + - serial: match(^(R17|R47|Y17|Y47).{13}$) monitor_sn: int? node_id: str suggested_area: str From be60f9ea1e126978e1af4bd83588a56d81b0d7ea Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 2 Mar 2025 21:09:03 +0100 Subject: [PATCH 14/23] calculate power values for DCU (#303) * calculate power values for DCU * refactor code --- app/src/gen3plus/infos_g3p.py | 77 +++++++++++++++++++------- app/src/infos.py | 40 +++++++++----- app/tests/test_infos_g3p.py | 88 ++++++++++++++++++++++++++---- system_tests/test_tcp_socket_v2.py | 2 +- 4 files changed, 160 insertions(+), 47 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index b42b2be..55fdb6a 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -5,6 +5,21 @@ from itertools import chain from infos import Infos, Register, ProxyMode, Fmt +class RegisterFunc: + @staticmethod + def prod_sum(info: Infos, arr: dict) -> None | int: + result = 0 + for sum in arr: + prod = 1 + for factor in sum: + val = info.get_db_value(factor) + if val is None: + return None + prod = prod * val + result += prod + return result + + class RegisterMap: # make the class read/only by using __slots__ __slots__ = () @@ -121,11 +136,11 @@ class RegisterMap: 0x42010034: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage 0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current 0x42010038: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501 - 0x4201003a: {'reg': Register.BATT_3a, 'fmt': '!h'}, # noqa: E501 - 0x4201003c: {'reg': Register.BATT_3c, 'fmt': '!h'}, # noqa: E501 - 0x4201003e: {'reg': Register.BATT_3e, 'fmt': '!h'}, # noqa: E501 - 0x42010040: {'reg': Register.BATT_40, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 - 0x42010042: {'reg': Register.BATT_42, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x4201003a: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 + 0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 + 0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010044: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent 0x42010046: {'reg': Register.BATT_46, 'fmt': '!h'}, # noqa: E501 0x42010048: {'reg': Register.BATT_48, 'fmt': '!h'}, # noqa: E501 @@ -136,13 +151,22 @@ class RegisterMap: 0x42010066: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501 0x42010068: {'reg': Register.BATT_68, 'fmt': '!h'}, # noqa: E501 0x4201006a: {'reg': Register.BATT_6a, 'fmt': '!h'}, # noqa: E501 - 0x4201006c: {'reg': Register.BATT_6c, 'fmt': '!h'}, # noqa: E501 - 0x4201006e: {'reg': Register.BATT_6e, 'fmt': '!h'}, # noqa: E501 + 0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010070: {'reg': Register.BATT_70, 'fmt': '!h'}, # noqa: E501 0x42010072: {'reg': Register.BATT_72, 'fmt': '!h'}, # noqa: E501 0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501 0x42010076: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 0x42010078: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 + 'calc': { + 1: {'reg': Register.BATT_PV_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501 + 'params': [[Register.BATT_PV1_VOLT, Register.BATT_PV1_CUR], + [Register.BATT_PV2_VOLT, Register.BATT_PV2_CUR]]}, + 2: {'reg': Register.BATT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501 + 'params': [[Register.BATT_VOLT, Register.BATT_CUR]]}, + 3: {'reg': Register.BATT_OUT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501 + 'params': [[Register.BATT_OUT_VOLT, Register.BATT_OUT_CUR]]}, + } } @@ -191,13 +215,20 @@ class InfosG3P(Infos): # iterate over RegisterMap.map and get the register values sensor = self.get_db_value(Register.SENSOR_LIST) if "3026" == sensor: - items = RegisterMap.map_3026.items() + reg_map = RegisterMap.map_3026 elif "02b0" == sensor: - items = RegisterMap.map_02b0.items() + reg_map = RegisterMap.map_02b0 else: - items = {} + reg_map = {} + items = reg_map.items() + if 'calc' in reg_map: + virt = reg_map['calc'].items() + else: + virt = {} - for _, row in chain(RegisterMap.map.items(), items): + for idx, row in chain(RegisterMap.map.items(), items, virt): + if 'calc' == idx: + continue info_id = row['reg'] if self.__hide_topic(row): res = self.ha_remove(info_id, node_id, snr) # noqa: E501 @@ -213,7 +244,10 @@ class InfosG3P(Infos): stores the values in Infos.db buf: buffer of the sequence to parse''' - for idx, row in RegisterSel.get(sensor).items(): + reg_map = RegisterSel.get(sensor) + for idx, row in reg_map.items(): + if 'calc' == idx: + continue addr = idx & 0xffff ftype = (idx >> 16) & 0xff mtype = (idx >> 24) & 0xff @@ -223,16 +257,19 @@ class InfosG3P(Infos): continue info_id = row['reg'] result = Fmt.get_value(buf, addr, row) + yield from self.__update_val(node_id, info_id, result) - keys, level, unit, must_incr = self._key_obj(info_id) - - if keys: - name, update = self.update_db(keys, must_incr, result) - yield keys[0], update - else: - name = str(f'info-id.0x{addr:x}') - update = False + if 'calc' in reg_map: + for row in reg_map['calc'].values(): + info_id = row['reg'] + result = row['func'](self, row['params']) + yield from self.__update_val(node_id, info_id, result) + def __update_val(self, node_id, info_id, result): + keys, level, unit, must_incr = self._key_obj(info_id) + if keys: + name, update = self.update_db(keys, must_incr, result) + yield keys[0], update if update: self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}' f' : {result}{unit}') diff --git a/app/src/infos.py b/app/src/infos.py index 2212d78..2e752d8 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -127,10 +127,10 @@ class Register(Enum): BATT_PV2_CUR = 1003 BATT_38 = 1004 BATT_3a = 1005 - BATT_3c = 1006 - BATT_3e = 1007 - BATT_40 = 1010 - BATT_42 = 1011 + BATT_STATUS_1 = 1006 + BATT_STATUS_2 = 1007 + BATT_VOLT = 1010 + BATT_CUR = 1011 BATT_SOC = 1012 BATT_46 = 1013 BATT_48 = 1014 @@ -140,13 +140,17 @@ class Register(Enum): BATT_66 = 1018 BATT_68 = 1019 BATT_6a = 1020 - BATT_6c = 1021 - BATT_6e = 1022 + BATT_OUT_VOLT = 1021 + BATT_OUT_CUR = 1022 BATT_70 = 1023 BATT_72 = 1024 BATT_74 = 1025 BATT_76 = 1026 BATT_78 = 1027 + BATT_PV_PWR = 1040 + BATT_PWR = 1041 + BATT_OUT_PWR = 1042 + VALUE_1 = 9000 TEST_REG1 = 10000 TEST_REG2 = 10001 @@ -157,7 +161,10 @@ class Fmt: def get_value(buf: bytes, idx: int, row: dict): '''Get a value from buf and interpret as in row defined''' fmt = row['fmt'] - res = struct.unpack_from(fmt, buf, idx) + try: + res = struct.unpack_from(fmt, buf, idx) + except Exception: + return None result = res[0] if isinstance(result, (bytearray, bytes)): result = result.decode().split('\x00')[0] @@ -572,12 +579,12 @@ class Infos: 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_3a: {'name': ['batterie', 'Reg_3a'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_3a_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_STATUS_1: {'name': ['batterie', 'Status_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_STATUS_2: {'name': ['batterie', 'Status_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_VOLT: {'name': ['batterie', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_bat_', 'fmt': FMT_FLOAT, 'name': 'Bat Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CUR: {'name': ['batterie', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_bat_', 'fmt': FMT_FLOAT, 'name': 'Bat Current', 'icon': GAUGE, '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 (SOC)', '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 @@ -587,13 +594,16 @@ class Infos: 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_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Out Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Out Current', 'icon': GAUGE, '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 + Register.BATT_PV_PWR: {'name': ['batterie', 'PV_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'pv_power_', 'fmt': FMT_INT, 'name': 'PV Power'}}, # noqa: E501 + Register.BATT_PWR: {'name': ['batterie', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_', 'fmt': FMT_INT, 'name': 'Batterie Power'}}, # noqa: E501 + Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Out Power'}}, # noqa: E501 } @property diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index f49cb50..366743d 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -70,6 +70,28 @@ def inverter_data(): # 0x4210 ftype: 0x01 msg += b'\x00\x00\x00\x00' return msg +@pytest.fixture +def batterie_data(): # 0x4210 ftype: 0x01 + msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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\x70\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' + return msg + +@pytest.fixture +def batterie_data2(): # 0x4210 ftype: 0x01 + msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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\x70\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' + return msg def test_default_db(): i = InfosG3P(client_mode=False) @@ -101,7 +123,7 @@ def test_build_4110(str_test_ip, device_data: bytes): build_msg[i] = device_data[i] assert device_data == build_msg -def test_parse_4210(inverter_data: bytes): +def test_parse_4210_02b0(inverter_data: bytes): i = InfosG3P(client_mode=False) i.db.clear() @@ -123,6 +145,46 @@ def test_parse_4210(inverter_data: bytes): "other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024, "Prod_Compliance_Type": 6} }) +def test_parse_4210_3026(batterie_data: bytes): + i = InfosG3P(client_mode=False) + i.db.clear() + + for key, update in i.parse (batterie_data, 0x42, 1, 0x3026): + pass # side effect is calling generator i.parse() + + assert json.dumps(i.db) == json.dumps({ + "controller": {"Sensor_List": "3026", "Power_On_Time": 4684}, + "inverter": {"Serial_Number": "4101240701490314"}, + "batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12}, + "pv2": {"Voltage": 33.72, "Current": 0.0}, + "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, + "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, + "Reg_68": 15, "Reg_6a": 15, + "out": {"Voltage": 0.14, "Current": 0.0, "Power": 0.0}, + "Reg_70": 0, "Reg_72": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513, + "PV_Power": 37.9232, "Power": -1.0268000000000002}, + }) + +def test_parse_4210_3026_incomplete(batterie_data2: bytes): + i = InfosG3P(client_mode=False) + i.db.clear() + + for key, update in i.parse (batterie_data2, 0x42, 1, 0x3026): + pass # side effect is calling generator i.parse() + + assert json.dumps(i.db) == json.dumps({ + "controller": {"Sensor_List": "3026", "Power_On_Time": 4684}, + "inverter": {"Serial_Number": "4101240701490314"}, + "batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12}, + "pv2": {"Voltage": 33.72, "Current": 0.0}, + "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, + "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, + "Reg_68": 15, "Reg_6a": 15, + "out": {"Voltage": 0.14, "Current": None, "Power": None}, + "Reg_70": None, "Reg_72": None, "Reg_74": None, "Reg_76": None, "Reg_78": None, + "PV_Power": 37.9232, "Power": -1.0268000000000002}, + }) + def test_build_4210(inverter_data: bytes): i = InfosG3P(client_mode=False) i.db.clear() @@ -294,25 +356,29 @@ def test_build_ha_conf5(): 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 + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Out Power", "stat_t": "tsun/garagendach/batterie", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{ (value_json['out']['Power'] | int)}}", "unit_of_meas": "W", "dev": {"name": "Batterie", "sa": "Batterie", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["batterie_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 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 == 'volt_pv1_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv1_123", "val_tpl": "{{ (value_json['pv1']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "batterie_123", "ids": ["bat_inp_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + elif id == 'volt_pv2_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv2_123", "val_tpl": "{{ (value_json['pv2']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "batterie_123", "ids": ["bat_inp_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 elif id == 'signal_123': assert comp == 'sensor' assert d_json == json.dumps({}) tests +=1 elif id == 'inv_count_456': assert False + else: + print(id) - assert tests==1 + assert tests==4 def test_exception_and_calc(inverter_data: bytes): diff --git a/system_tests/test_tcp_socket_v2.py b/system_tests/test_tcp_socket_v2.py index 4bf904b..94816fd 100644 --- a/system_tests/test_tcp_socket_v2.py +++ b/system_tests/test_tcp_socket_v2.py @@ -151,7 +151,7 @@ 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'\x0d\x3a\x00\x0a\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' From 88cb01f6131afcde60b117c8afa71ec6e52fed98 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:47:37 +0100 Subject: [PATCH 15/23] add Modbus polling mode for DCU1000 (#305) * add Modbus scanning mode * fix modbus polling for DCU 1000 * add modbus register for DCU 1000 * calculate meta values from modbus regs * update changelog * reduce code duplication * refactor modbus_scan * add additional unit tests --- CHANGELOG.md | 2 + app/src/cnf/config.py | 10 ++ app/src/gen3/talent.py | 20 ++-- app/src/gen3plus/infos_g3p.py | 18 +++- app/src/gen3plus/solarman_v5.py | 57 +++++++--- app/src/messages.py | 54 +++++++++- app/src/modbus.py | 28 +++++ app/tests/test_solarman.py | 179 ++++++++++++++++++++++++++++++++ app/tests/test_talent.py | 50 +++++++++ 9 files changed, 386 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd0faa..9c5f8c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292) +- add Modbus scanning mode - allow `R47`serial numbers for GEN3 inverters - add watchdog for Add-ons - Respect logging.ini file, if LOG_ENV isn't set well [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288) diff --git a/app/src/cnf/config.py b/app/src/cnf/config.py index 1bc59a9..88a0ab0 100644 --- a/app/src/cnf/config.py +++ b/app/src/cnf/config.py @@ -93,6 +93,11 @@ class Config(): Optional('forward', default=False): Use(bool), }, Optional('modbus_polling', default=True): Use(bool), + Optional('modbus_scanning'): { + 'start': Use(int), + Optional('step', default=0x400): Use(int), + Optional('bytes', default=0x10): Use(int), + }, Optional('suggested_area', default=""): Use(str), Optional('sensor_list', default=0): Use(int), Optional('pv1'): { @@ -136,6 +141,11 @@ class Config(): Optional('forward', default=False): Use(bool), }, Optional('modbus_polling', default=True): Use(bool), + Optional('modbus_scanning'): { + 'start': Use(int), + Optional('step', default=0x400): Use(int), + Optional('bytes', default=0x10): Use(int), + }, Optional('suggested_area', default=""): Use(str), Optional('sensor_list', default=0): Use(int), Optional('pv1'): { diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 73fdae7..29df94c 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -98,13 +98,9 @@ class Talent(Message): if serial_no in inverters: inv = inverters[serial_no] - self.node_id = inv['node_id'] - self.sug_area = inv['suggested_area'] - self.modbus_polling = inv['modbus_polling'] - logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 + self._set_config_parms(inv) self.db.set_pv_module_details(inv) - if self.mb: - self.mb.set_node_id(self.node_id) + logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 else: self.node_id = '' self.sug_area = '' @@ -175,12 +171,17 @@ class Talent(Message): def mb_timout_cb(self, exp_cnt): self.mb_timer.start(self.mb_timeout) + if self.mb_scan: + self._send_modbus_scan() + return if 2 == (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.INV_ADDR, Modbus.READ_REGS, 0x2000, + 96, logging.DEBUG) else: - self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG) + self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000, + 48, logging.DEBUG) def _init_new_client_conn(self) -> bool: contact_name = self.contact_name @@ -554,6 +555,9 @@ class Talent(Message): logger.warning('Unknown Message') self.inc_counter('Unknown_Msg') return + if (self.mb_scan): + modbus_msg_len = self.data_len - hdr_len + self._dump_modbus_scan(data, hdr_len, modbus_msg_len) for key, update, _ in self.mb.recv_resp(self.db, data[ hdr_len:]): diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 55fdb6a..cd6d040 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -257,21 +257,31 @@ class InfosG3P(Infos): continue info_id = row['reg'] result = Fmt.get_value(buf, addr, row) - yield from self.__update_val(node_id, info_id, result) + yield from self.__update_val(node_id, "GEN3PLUS", info_id, result) + yield from self.calc(sensor, node_id) + def calc(self, sensor: int = 0, node_id: str = '') \ + -> Generator[tuple[str, bool], None, None]: + '''calculate meta values from the + stored values in Infos.db + + sensor: sensor_list number + node_id: id-string for the node''' + + reg_map = RegisterSel.get(sensor) if 'calc' in reg_map: for row in reg_map['calc'].values(): info_id = row['reg'] result = row['func'](self, row['params']) - yield from self.__update_val(node_id, info_id, result) + yield from self.__update_val(node_id, "CALC", info_id, result) - def __update_val(self, node_id, info_id, result): + def __update_val(self, node_id, source: str, info_id, result): keys, level, unit, must_incr = self._key_obj(info_id) if keys: name, update = self.update_db(keys, must_incr, result) yield keys[0], update if update: - self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}' + self.tracer.log(level, f'[{node_id}] {source}: {name}' f' : {result}{unit}') def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0): diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 4fcb71b..d279cae 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -322,6 +322,8 @@ class SolarmanV5(SolarmanBase): self.at_acl = g3p_cnf['at_acl'] self.sensor_list = 0 + self.mb_regs = [{'addr': 0x3000, 'len': 48}, + {'addr': 0x2000, 'len': 96}] ''' Our puplic methods @@ -357,7 +359,16 @@ class SolarmanV5(SolarmanBase): self.new_data['controller'] = True self.state = State.up - self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG) + + if self.mb_scan: + self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS, + self.mb_start_reg, self.mb_bytes, + logging.INFO) + else: + self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, + self.mb_regs[0]['addr'], + self.mb_regs[0]['len'], logging.DEBUG) + self.mb_timer.start(self.mb_timeout) def new_state_up(self): @@ -377,16 +388,16 @@ 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, key) + 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 @@ -474,12 +482,18 @@ class SolarmanV5(SolarmanBase): def mb_timout_cb(self, exp_cnt): self.mb_timer.start(self.mb_timeout) + if self.mb_scan: + self._send_modbus_scan() + else: + self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, + self.mb_regs[0]['addr'], + self.mb_regs[0]['len'], logging.INFO) - 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.INFO) + if 1 == (exp_cnt % 30) and len(self.mb_regs) > 1: + # logging.info("Regular Modbus Status request") + self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, + self.mb_regs[1]['addr'], + self.mb_regs[1]['len'], logging.INFO) def at_cmd_forbidden(self, cmd: str, connection: str) -> bool: return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \ @@ -672,16 +686,25 @@ class SolarmanV5(SolarmanBase): return self.__forward_msg() - def __parse_modbus_rsp(self, data): + def __parse_modbus_rsp(self, data, modbus_msg_len): inv_update = False self.modbus_elms = 0 + if (self.mb_scan): + self._dump_modbus_scan(data, 14, modbus_msg_len) + + ts = self._timestamp() for key, update, _ in self.mb.recv_resp(self.db, data[14:]): self.modbus_elms += 1 if update: if key == 'inverter': inv_update = True - self._set_mqtt_timestamp(key, self._timestamp()) + self._set_mqtt_timestamp(key, ts) self.new_data[key] = True + for key, update in self.db.calc(self.sensor_list, self.node_id): + if update: + self._set_mqtt_timestamp(key, ts) + self.new_data[key] = True + return inv_update def __modbus_command_rsp(self, data): @@ -691,7 +714,7 @@ class SolarmanV5(SolarmanBase): # logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}') if valid == 1 and modbus_msg_len > 4: # logger.info(f'first byte modbus:{data[14]}') - inv_update = self.__parse_modbus_rsp(data) + inv_update = self.__parse_modbus_rsp(data, modbus_msg_len) if inv_update: self.__build_model_name() diff --git a/app/src/messages.py b/app/src/messages.py index eecfc80..7efaa86 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -117,6 +117,11 @@ class Message(ProtocolIfc): self.mb_first_timeout = self.MB_START_TIMEOUT '''timer value for next Modbus polling request''' self.modbus_polling = False + self.mb_start_reg = 0 + self.mb_step = 0 + self.mb_bytes = 0 + self.mb_inv_no = 1 + self.mb_scan = False @property def node_id(self): @@ -135,6 +140,25 @@ class Message(ProtocolIfc): # to our _recv_buffer return # pragma: no cover + def _set_config_parms(self, inv: dict): + '''init connection with params from the configuration''' + self.node_id = inv['node_id'] + self.sug_area = inv['suggested_area'] + self.modbus_polling = inv['modbus_polling'] + if 'modbus_scanning' in inv: + scan = inv['modbus_scanning'] + self.mb_scan = True + self.mb_start_reg = scan['start'] + self.mb_step = scan['step'] + self.mb_bytes = scan['bytes'] + # if 'client_mode' in self.db and \ + # self.db.client_mode: + self.mb_start_reg = scan['start'] + # else: + # self.mb_start_reg = scan['start'] - scan['step'] + if self.mb: + self.mb.set_node_id(self.node_id) + def _set_mqtt_timestamp(self, key, ts: float | None): if key not in self.new_data or \ not self.new_data[key]: @@ -160,15 +184,39 @@ class Message(ProtocolIfc): to = self.MAX_DEF_IDLE_TIME return to - def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None: + def _send_modbus_cmd(self, dev_id, func, addr, val, log_lvl) -> None: if self.state != State.up: logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,' ' as the state is not UP') return - self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl) + self.mb.build_msg(dev_id, func, addr, val, log_lvl) async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None: - self._send_modbus_cmd(func, addr, val, log_lvl) + self._send_modbus_cmd(Modbus.INV_ADDR, func, addr, val, log_lvl) + + def _send_modbus_scan(self): + self.mb_start_reg += self.mb_step + if self.mb_start_reg > 0xffff: + self.mb_start_reg = self.mb_start_reg & 0xffff + self.mb_inv_no += 1 + logging.info(f"Next Round: inv:{self.mb_inv_no}" + f" reg:{self.mb_start_reg:04x}") + if (self.mb_start_reg & 0xfffc) % 0x80 == 0: + logging.info(f"[{self.node_id}] Scan info: " + f"inv:{self.mb_inv_no}" + f" reg:{self.mb_start_reg:04x}") + self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS, + self.mb_start_reg, self.mb_bytes, + logging.INFO) + + def _dump_modbus_scan(self, data, hdr_len, modbus_msg_len): + if (data[hdr_len] == self.mb_inv_no and + data[hdr_len+1] == Modbus.READ_REGS): + logging.info(f'[{self.node_id}] Valid MODBUS data ' + f'(reg: 0x{self.mb.last_reg:04x}):') + hex_dump_memory(logging.INFO, 'Valid MODBUS data ' + f'(reg: 0x{self.mb.last_reg:04x}):', + data[hdr_len:], modbus_msg_len) ''' Our puplic methods diff --git a/app/src/modbus.py b/app/src/modbus.py index 5c64086..b8244b5 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -37,6 +37,34 @@ class Modbus(): __crc_tab = [] mb_reg_mapping = { + 0x0000: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501 + 0x0008: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage + 0x0009: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current + 0x000a: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage + 0x000b: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current + 0x000c: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501 + 0x000d: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x000e: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 + 0x000f: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 + 0x0010: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x0011: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x0012: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent + # 0x0013: {'reg': Register.BATT_46, 'fmt': '!h'}, # noqa: E501 + # 0x0014: {'reg': Register.BATT_48, 'fmt': '!h'}, # noqa: E501 + # 0x0015: {'reg': Register.BATT_4a, 'fmt': '!h'}, # noqa: E501 + # 0x0016: {'reg': Register.BATT_4c, 'fmt': '!h'}, # noqa: E501 + # 0x0017: {'reg': Register.BATT_4e, 'fmt': '!h'}, # noqa: E501 + # 0x0023: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501 + # 0x0024: {'reg': Register.BATT_68, 'fmt': '!h'}, # noqa: E501 + # 0x0025: {'reg': Register.BATT_6a, 'fmt': '!h'}, # noqa: E501 + 0x0026: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x0027: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + # 0x0028: {'reg': Register.BATT_70, 'fmt': '!h'}, # noqa: E501 + # 0x0029: {'reg': Register.BATT_72, 'fmt': '!h'}, # noqa: E501 + # 0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501 + # 0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 + # 0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 + 0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501 0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501 0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'}, diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index f379489..a4bbbfc 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -643,6 +643,19 @@ def msg_modbus_rsp(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def msg_modbus_rsp_inv_id2(): # 0x1510 + msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01' + msg += total() + msg += hb() + msg += b'\x0a\xe2\xfa\x33\x02\x03\x28\x40\x10\x08\xd8' + msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02' + msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04' + msg += b'\x00\x01\x00\x00\x2a\xaa' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def msg_modbus_invalid(): # 0x1510 msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00' @@ -678,6 +691,22 @@ def msg_unknown_cmd_rsp(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def dcu_modbus_rsp(): # 0x1510 + msg = b'\xa5\x6d\x00\x10\x15\x03\x03' +get_dcu_sn() +b'\x02\x01' + msg += total() + msg += hb() + msg += b'\x4d\x0d\x84\x34\x01\x03\x5a\x34\x31\x30\x31' + msg += b'\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34\x00\x32\x00\x00' + msg += b'\x00\x32\x00\x00\x00\x00\x10\x7b\x00\x02\x00\x02\x14\x9b\xfe\xfd' + msg += b'\x25\x28\x0c\xe1\x0c\xde\x0c\xe1\x0c\xe1\x0c\xe0\x0c\xe1\x0c\xe3' + msg += b'\x0c\xdf\x0c\xe0\x0c\xe2\x0c\xe1\x0c\xe1\x0c\xe2\x0c\xe2\x0c\xe3' + msg += b'\x0c\xdf\x00\x14\x00\x14\x00\x13\x0f\x94\x01\x4a\x00\x01\x00\x15' + msg += b'\x00\x00\x02\x05\x02\x01\x14\xab' + msg += correct_checksum(msg) + 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' @@ -749,6 +778,14 @@ def config_no_tsun_inv1(): 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': 0}}} +@pytest.fixture +def config_tsun_scan(): + Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xff80, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}} + +@pytest.fixture +def config_tsun_scan_dcu(): + Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, '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}}} @@ -1859,6 +1896,79 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp assert next(m.mb_timer.exp_count) == 4 m.close() +@pytest.mark.asyncio +async def test_modbus_scaning(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp_msg, msg_modbus_rsp, msg_modbus_rsp_inv_id2): + _ = config_tsun_scan + assert asyncio.get_running_loop() + + m = MemoryStream(heartbeat_ind_msg, (0x15,0x56,0)) + m.append_msg(msg_modbus_rsp) + m.append_msg(msg_modbus_rsp_inv_id2) + assert m.mb_scan == False + assert asyncio.get_running_loop() == m.mb_timer.loop + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + assert m.mb_timer.tim == None + m.read() # read complete msg, and dispatch msg + assert m.mb_scan == True + assert m.mb_start_reg == 0xff80 + assert m.mb_step == 0x40 + assert m.mb_bytes == 0x14 + assert asyncio.get_running_loop() == m.mb_timer.loop + + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.snr == 2070233889 + assert m.control == 0x4710 + + assert m.msg_recvd[0]['control']==0x4710 + assert m.msg_recvd[0]['seq']=='84:11' + assert m.msg_recvd[0]['data_len']==0x1 + + assert m.ifc.tx_fifo.get()==heartbeat_rsp_msg + assert m.ifc.fwd_fifo.get()==heartbeat_ind_msg + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + + m.ifc.tx_clear() # clear send buffer for next test + assert isclose(m.mb_timeout, 0.5) + assert next(m.mb_timer.exp_count) == 0 + + await asyncio.sleep(0.5) + assert m.sent_pdu==b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x01\x03\xff\xc0\x00\x14\x75\xed\x33\x15' + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.msg_recvd[1]['control']==0x1510 + assert m.msg_recvd[1]['seq']=='03:03' + assert m.msg_recvd[1]['data_len']==0x3b + assert m.mb.last_addr == 1 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0xffc0 # mb_start_reg + mb_step + assert m.mb.last_len == 20 + assert m.mb.err == 0 + + await asyncio.sleep(0.5) + assert m.sent_pdu==b'\xa5\x17\x00\x10E\x04\x03!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x02\x03\x00\x00\x00\x14\x45\xf6\xbf\x15' + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 3 + assert m.msg_recvd[2]['control']==0x1510 + assert m.msg_recvd[2]['seq']=='03:03' + assert m.msg_recvd[2]['data_len']==0x3b + assert m.mb.last_addr == 2 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step + assert m.mb.last_len == 20 + assert m.mb.err == 0 + + assert next(m.mb_timer.exp_count) == 3 + m.close() + @pytest.mark.asyncio async def test_start_client_mode(config_tsun_inv1, str_test_ip): _ = config_tsun_inv1 @@ -1891,6 +2001,75 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip): assert next(m.mb_timer.exp_count) == 3 m.close() +@pytest.mark.asyncio +async def test_start_client_mode_scan(config_tsun_scan_dcu, str_test_ip, dcu_modbus_rsp): + _ = config_tsun_scan_dcu + assert asyncio.get_running_loop() + m = MemoryStream(dcu_modbus_rsp, (131,0,)) + m.append_msg(dcu_modbus_rsp) + assert m.state == State.init + assert m.no_forwarding == False + assert m.mb_timer.tim == None + assert asyncio.get_running_loop() == m.mb_timer.loop + await m.send_start_cmd(get_dcu_sn_int(), str_test_ip, False, m.mb_first_timeout) + assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x95\x15') + assert m.mb_scan == True + m.mb_step = 0 + assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip + assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5) + assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120 + + assert m.state == State.up + assert m.no_forwarding == True + + assert m.ifc.tx_fifo.get()==b'' + assert isclose(m.mb_timeout, 0.5) + + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.msg_recvd[0]['control']==0x1510 + assert m.msg_recvd[0]['seq']=='03:03' + assert m.msg_recvd[0]['data_len']==109 + assert m.mb.last_addr == 1 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step + assert m.mb.last_len == 45 + assert m.mb.err == 0 + + assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225) + assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604) + assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0) + assert m.new_data['batterie'] == True + m.new_data['batterie'] = False + + await asyncio.sleep(0.5) + assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x04\x03 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x9b\x15') + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.msg_recvd[1]['control']==0x1510 + assert m.msg_recvd[1]['seq']=='03:03' + assert m.msg_recvd[1]['data_len']==109 + assert m.mb.last_addr == 1 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step + assert m.mb.last_len == 45 + assert m.mb.err == 0 + + assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225) + assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604) + assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0) + assert m.new_data['batterie'] == False + + assert next(m.mb_timer.exp_count) == 1 + + m.close() + def test_timeout(config_tsun_inv1): _ = config_tsun_inv1 m = MemoryStream(b'') diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index b5d6e36..2d6029c 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -2184,6 +2184,56 @@ async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind): assert next(m.mb_timer.exp_count) == 4 m.close() +@pytest.mark.asyncio +async def test_modbus_scaning(config_tsun_inv1, msg_inverter_ind, msg_modbus_rsp21): + _ = config_tsun_inv1 + assert asyncio.get_running_loop() + + m = MemoryStream(msg_inverter_ind, (0x8f,0)) + m.append_msg(msg_modbus_rsp21) + m.mb_scan = True + m.mb_start_reg = 0x4560 + m.mb_bytes = 0x14 + assert asyncio.get_running_loop() == m.mb_timer.loop + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + assert m.mb_timer.tim == None + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.id_str == b"R170000000000001" + assert m.unique_id == 'R170000000000001' + assert m.msg_recvd[0]['ctrl']==145 + assert m.msg_recvd[0]['msg_id']==4 + assert m.msg_recvd[0]['header_len']==23 + assert m.msg_recvd[0]['data_len']==120 + assert m.ifc.fwd_fifo.get()==msg_inverter_ind + assert m.ifc.tx_fifo.get()==b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + + m.ifc.tx_clear() # clear send buffer for next test + assert isclose(m.mb_timeout, 0.5) + assert next(m.mb_timer.exp_count) == 0 + + await asyncio.sleep(0.5) + assert m.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x03\x45\x60\x00\x14\x50\xd7' + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.msg_recvd[1]['ctrl']==145 + assert m.msg_recvd[1]['msg_id']==119 + assert m.msg_recvd[1]['header_len']==23 + assert m.msg_recvd[1]['data_len']==50 + assert m.mb.last_addr == 1 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0x4560 + assert m.mb.last_len == 20 + assert m.mb.err == 0 + + assert next(m.mb_timer.exp_count) == 2 + m.close() + def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf): _ = config_tsun_allow_all m = MemoryStream(broken_recv_buf, (0,)) From 3489e8997ddd29cdcab5f6e6bc490336ae324298 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sat, 15 Mar 2025 13:52:49 +0100 Subject: [PATCH 16/23] fix MQTT paket transmitting (#309) --- app/src/gen3plus/solarman_v5.py | 7 ++-- app/tests/test_solarman.py | 58 +++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index d279cae..56e081b 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -5,6 +5,7 @@ import asyncio from itertools import chain from datetime import datetime +from proxy import Proxy from async_ifc import AsyncIfc from messages import hex_dump_memory, Message, State from cnf.config import Config @@ -511,7 +512,7 @@ class SolarmanV5(SolarmanBase): node_id = self.node_id key = 'at_resp' logger.info(f'{key}: {data_json}') - await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501 + await Proxy.mqtt.publish(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501 return self.forward_at_cmd_resp = False @@ -655,7 +656,7 @@ class SolarmanV5(SolarmanBase): def publish_mqtt(self, key, data): # pragma: no cover asyncio.ensure_future( - self.mqtt.publish(key, data)) + Proxy.mqtt.publish(key, data)) def get_cmd_rsp_log_lvl(self) -> int: ftype = self.ifc.rx_peek()[self.header_len] @@ -679,7 +680,7 @@ class SolarmanV5(SolarmanBase): node_id = self.node_id key = 'at_resp' logger.info(f'{key}: {data_json}') - self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501 + self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501 return elif ftype == self.MB_RTU_CMD: self.__modbus_command_rsp(data) diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index a4bbbfc..9b7d5ed 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -11,6 +11,7 @@ from cnf.config import Config from infos import Infos, Register from modbus import Modbus from messages import State, Message +from proxy import Proxy pytest_plugins = ('pytest_asyncio',) @@ -24,6 +25,8 @@ heartbeat = 60 class Mqtt(): def __init__(self): + self.clear() + def clear(self): self.key = '' self.data = '' @@ -50,7 +53,6 @@ class MemoryStream(SolarmanV5): self.mb_timeout = 0.5 self.sent_pdu = b'' self.ifc.tx_fifo.reg_trigger(self.write_cb) - self.mqtt = Mqtt() self.__msg = msg self.__msg_len = len(msg) self.__chunks = chunks @@ -62,7 +64,6 @@ class MemoryStream(SolarmanV5): self.db.stat['proxy']['AT_Command'] = 0 self.db.stat['proxy']['AT_Command_Blocked'] = 0 self.test_exception_async_write = False - self.entity_prfx = '' self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}} self.key = '' self.data = '' @@ -85,8 +86,8 @@ class MemoryStream(SolarmanV5): self.__chunk_idx = 0 def publish_mqtt(self, key, data): - self.key = key - self.data = data + Proxy.mqtt.key = key + Proxy.mqtt.data = data def _read(self) -> int: copied_bytes = 0 @@ -768,7 +769,17 @@ def dcu_data_rsp_msg(): # 0x1210 @pytest.fixture def config_tsun_allow_all(): - Config.act_config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}} + Config.act_config = { + 'ha':{ + 'auto_conf_prefix': 'homeassistant', + 'discovery_prefix': 'homeassistant', + 'entity_prefix': 'tsun', + 'proxy_node_id': 'test_1', + 'proxy_unique_id': '' + }, + 'solarman':{'enabled': True}, 'inverters':{'allow_all':True}} + Proxy.class_init() + Proxy.mqtt = Mqtt() # set dummy mqtt instance @pytest.fixture def config_no_tsun_inv1(): @@ -776,7 +787,17 @@ 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': 0}}} + Config.act_config = { + 'ha':{ + 'auto_conf_prefix': 'homeassistant', + 'discovery_prefix': 'homeassistant', + 'entity_prefix': 'tsun', + 'proxy_node_id': 'test_1', + 'proxy_unique_id': '' + }, + 'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}} + Proxy.class_init() + Proxy.mqtt = Mqtt() @pytest.fixture def config_tsun_scan(): @@ -1465,8 +1486,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv assert m.ifc.fwd_fifo.get()==b'' assert m.sent_pdu == b'' assert str(m.seq) == '01:01' - assert m.mqtt.key == '' - assert m.mqtt.data == "" + assert Proxy.mqtt.key == '' + assert Proxy.mqtt.data == "" m.append_msg(inverter_ind_msg) m.read() # read inverter ind @@ -1482,8 +1503,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv m.sent_pdu = bytearray() assert str(m.seq) == '02:03' - assert m.mqtt.key == '' - assert m.mqtt.data == "" + assert Proxy.mqtt.key == '' + assert Proxy.mqtt.data == "" m.append_msg(at_command_rsp_msg) m.read() # read at resp @@ -1492,8 +1513,9 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv assert m.ifc.rx_get()==b'' assert m.ifc.tx_fifo.get()==b'' assert m.ifc.fwd_fifo.get()==b'' - assert m.key == 'at_resp' - assert m.data == "+ok" + assert Proxy.mqtt.key == 'tsun/at_resp' + assert Proxy.mqtt.data == "+ok" + Proxy.mqtt.clear() # clear last test result m.sent_pdu = bytearray() m.test_exception_async_write = True @@ -1505,8 +1527,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv assert m.sent_pdu == b'' assert str(m.seq) == '03:04' assert m.forward_at_cmd_resp == False - assert m.mqtt.key == '' - assert m.mqtt.data == "" + assert Proxy.mqtt.key == '' + assert Proxy.mqtt.data == "" m.close() @pytest.mark.asyncio @@ -1523,8 +1545,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_ assert m.ifc.tx_fifo.get()==b'' assert m.ifc.fwd_fifo.get()==b'' assert str(m.seq) == '01:01' - assert m.mqtt.key == '' - assert m.mqtt.data == "" + assert Proxy.mqtt.key == '' + assert Proxy.mqtt.data == "" m.append_msg(inverter_ind_msg) m.read() @@ -1540,8 +1562,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_ assert m.ifc.fwd_fifo.get()==b'' assert str(m.seq) == '02:02' assert m.forward_at_cmd_resp == False - assert m.mqtt.key == 'at_resp' - assert m.mqtt.data == "'AT+WEBU' is forbidden" + assert Proxy.mqtt.key == 'tsun/at_resp' + assert Proxy.mqtt.data == "'AT+WEBU' is forbidden" m.close() def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg): From ecd21e46fbfbd52a32dd0ca039bcdf1d9e075f35 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 15 Mar 2025 17:16:54 +0100 Subject: [PATCH 17/23] add modbus scanner config for HA Add-ons --- app/tests/test_config.py | 15 +++++++++++++-- app/tests/test_config_read_json.py | 9 +++++++++ ha_addons/templates/config.jinja | 3 +++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/tests/test_config.py b/app/tests/test_config.py index aa0cd52..b97a4e8 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -152,6 +152,16 @@ def ConfigComplete(): 'pv4': {'manufacturer': 'man4', 'type': 'type4'}, 'suggested_area': 'Garage2', + 'sensor_list': 688}, + 'Y170000000000002': {'modbus_polling': False, + 'modbus_scanning': { + 'bytes': 16, + 'start': 2048, + 'step': 1024 + }, + 'monitor_sn': 2000000001, + 'node_id': 'PV-Garage3/', + 'suggested_area': 'Garage3', 'sensor_list': 688} }, 'batteries': { @@ -224,8 +234,9 @@ def test_full_config(ConfigComplete): }, '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'}}, + 'Y170000000000002': {'modbus_polling': False, 'monitor_sn': 2000000001, 'node_id': 'PV-Garage3/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage3', 'modbus_scanning': {'start': 2048, 'step': 1024, 'bytes': 16}} + } } try: validated = Config.conf_schema.validate(cnf) diff --git a/app/tests/test_config_read_json.py b/app/tests/test_config_read_json.py index 647885a..68c5b60 100644 --- a/app/tests/test_config_read_json.py +++ b/app/tests/test_config_read_json.py @@ -380,6 +380,15 @@ def test_full_config(ConfigComplete): "pv4.manufacturer": "man4", "pv4.type": "type4", "sensor_list": 688 + }, + { + "serial": "Y170000000000002", + "monitor_sn": 2000000001, + "modbus_polling": false, + "modbus_scanning.start": 2048, + "node_id": "PV-Garage3", + "suggested_area": "Garage3", + "sensor_list": 688 } ], "batteries": [ diff --git a/ha_addons/templates/config.jinja b/ha_addons/templates/config.jinja index f17a758..796d0c2 100755 --- a/ha_addons/templates/config.jinja +++ b/ha_addons/templates/config.jinja @@ -40,6 +40,9 @@ 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? + modbus_scanning.start: int(0,65535)? + modbus_scanning.step: int(0,65535)? + modbus_scanning.bytes: int(1,80)? pv1.manufacturer: str? pv1.type: str? pv2.manufacturer: str? From 955657fd8760cac1d966be37978d5c9f0e71ee59 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:11:03 +0100 Subject: [PATCH 18/23] add first costumer apparmor definition (#296) * add first costumer apparmor definition * add initial apparmor support --- CHANGELOG.md | 2 ++ ha_addons/Makefile | 13 +++++++- ha_addons/templates/apparmor.jinja | 52 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 ha_addons/templates/apparmor.jinja diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5f8c2..29d8d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add initial apparmor support [#293](https://github.com/s-allius/tsun-gen3-proxy/issues/293) - add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292) - add Modbus scanning mode - allow `R47`serial numbers for GEN3 inverters - add watchdog for Add-ons +- add first costumer apparmor definition - 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.2.1 diff --git a/ha_addons/Makefile b/ha_addons/Makefile index 44e5b78..34dbb85 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -66,7 +66,7 @@ clean: # 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 +local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt # collect source files SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\ @@ -100,6 +100,9 @@ $(DST)/requirements.txt : $(SRC)/requirements.txt $(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json $(JINJA) --strict -D AppVersion=$(VERSION) -D BuildID=$(BUILD_ID) --format=json $^ -o $@ +$(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json + $(JINJA) --strict --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 @@ -115,31 +118,37 @@ FORCE : ; repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml rootfs/run.sh repro_root = CHANGELOG.md LICENSE.md repro_templates = config.yaml +repro_apparmor = apparmor.txt repro_subdirs = translations rootfs repro_vers = debug dev rc rel repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file))) +repro_all_apparmor := $(foreach dir,$(repro_vers), $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file))) debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \ $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \ + $(foreach file,$(repro_apparmor),$(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_apparmor),$(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_apparmor),$(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_apparmor),$(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)) @@ -149,6 +158,8 @@ $(repro_all_subdirs) : $(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_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json + $(JINJA) --strict $< $(filter %.json,$^) -o $@ $(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../% cp $< $@ diff --git a/ha_addons/templates/apparmor.jinja b/ha_addons/templates/apparmor.jinja new file mode 100644 index 0000000..25ac8e0 --- /dev/null +++ b/ha_addons/templates/apparmor.jinja @@ -0,0 +1,52 @@ +#include + +profile {{slug}} flags=(attach_disconnected,mediate_deleted) { + #include + + # Capabilities + file, + signal (send) set=(kill,term,int,hup,cont), + + # S6-Overlay + /init ix, + /bin/** ix, + /usr/bin/** ix, + /run/{s6,s6-rc*,service}/** ix, + /package/** ix, + /command/** ix, + /etc/services.d/** rwix, + /etc/cont-init.d/** rwix, + /etc/cont-finish.d/** rwix, + /run/{,**} rwk, + /dev/tty rw, + + # Bashio + /usr/lib/bashio/** ix, + /tmp/** rwk, + + # Access to options.json and other files within your addon + /data/** rw, + + # Start new profile for service + /usr/bin/myprogram cx -> myprogram, + + profile myprogram flags=(attach_disconnected,mediate_deleted) { + #include + + # Receive signals from S6-Overlay + signal (receive) peer=*_{{slug}}, + + # Access to options.json and other files within your addon + /data/** rw, + + # Access to mapped volumes specified in config.json + /share/** rw, + + # Access required for service functionality + /usr/bin/myprogram r, + /bin/bash rix, + /bin/echo ix, + /etc/passwd r, + /dev/tty rw, + } +} \ No newline at end of file From e0777dca8e29199b1fbcfc6b022d8433ff6f598b Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:49:01 +0100 Subject: [PATCH 19/23] Add support for MS-3000 inverter (#299) * split register map into multiple maps * add base support reg mapping 0x01900000 * fix shadowed builtin * detect reg mapping for sensor automatically * add device and test regs for MS-3000 * add more register mappings * fix unit tests * add more MS-3000 registers * build modell string for TSUN MS-3000 * add MS3000 unit test * remove obsolete method __set_config_parms * fix start addr of modbus scans - in server mode the start addr must be reduced by mb_step * add tests for sensor_list of ms-3000 inverters * MS-3000: add integer test register * DCU-1000: add Out Status register * add integer test and batterie out register * fix Sonar Qube finding * DCU-1000: add temp sensors --- CHANGELOG.md | 1 + app/src/gen3/infos_g3.py | 147 ++++++++++++--- app/src/gen3/talent.py | 43 ++++- app/src/gen3plus/infos_g3p.py | 14 +- app/src/infos.py | 117 +++++++++++- app/src/messages.py | 10 +- app/tests/test_infos_g3.py | 320 ++++++++++++++++++++++++++++++-- app/tests/test_infos_g3p.py | 12 +- app/tests/test_solarman.py | 8 +- app/tests/test_talent.py | 307 +++++++++++++++++++++++++++++- system_tests/test_tcp_socket.py | 164 ++++++++++++++++ 11 files changed, 1063 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d8d32..33799e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add initial support for TSUN MS-3000 - add initial apparmor support [#293](https://github.com/s-allius/tsun-gen3-proxy/issues/293) - add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292) - add Modbus scanning mode diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index efa220c..50440ac 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -2,26 +2,14 @@ import struct import logging from typing import Generator +from itertools import chain from infos import Infos, Register class RegisterMap: __slots__ = () - map = { - 0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION}, - 0x000927c0: {'reg': Register.CHIP_TYPE}, - 0x00092f90: {'reg': Register.CHIP_MODEL}, - 0x00094ae8: {'reg': Register.MAC_ADDR}, - 0x00095a88: {'reg': Register.TRACE_URL}, - 0x00095aec: {'reg': Register.LOGGER_URL}, - 0x0000000a: {'reg': Register.PRODUCT_NAME}, - 0x00000014: {'reg': Register.MANUFACTURER}, - 0x0000001e: {'reg': Register.VERSION}, - 0x00000028: {'reg': Register.SERIAL_NUMBER}, - 0x00000032: {'reg': Register.EQUIPMENT_MODEL}, - 0x00013880: {'reg': Register.NO_INPUTS}, 0xffffff00: {'reg': Register.INVERTER_CNT}, 0xffffff01: {'reg': Register.UNKNOWN_SNR}, 0xffffff02: {'reg': Register.UNKNOWN_MSG}, @@ -33,6 +21,100 @@ class RegisterMap: 0xffffff08: {'reg': Register.POLLING_INTERVAL}, 0xfffffffe: {'reg': Register.TEST_REG1}, 0xffffffff: {'reg': Register.TEST_REG2}, + } + map_0e100000 = { + 0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION}, + 0x000927c0: {'reg': Register.CHIP_TYPE}, + 0x00092f90: {'reg': Register.CHIP_MODEL}, + 0x00094ae8: {'reg': Register.MAC_ADDR}, + 0x00095a88: {'reg': Register.TRACE_URL}, + 0x00095aec: {'reg': Register.LOGGER_URL}, + 0x000cfc38: {'reg': Register.CONNECT_COUNT}, + 0x000c3500: {'reg': Register.SIGNAL_STRENGTH}, + 0x000c96a8: {'reg': Register.POWER_ON_TIME}, + 0x000d0020: {'reg': Register.COLLECT_INTERVAL}, + 0x000cf850: {'reg': Register.DATA_UP_INTERVAL}, + 0x000c7f38: {'reg': Register.COMMUNICATION_TYPE}, + } + map_01900000 = { + 0x0000000a: {'reg': Register.PRODUCT_NAME}, + 0x00000014: {'reg': Register.MANUFACTURER}, + 0x0000001e: {'reg': Register.VERSION}, + 0x00000046: {'reg': Register.SERIAL_NUMBER}, + 0x0000005A: {'reg': Register.EQUIPMENT_MODEL}, + 0x00000064: {'reg': Register.INVERTER_STATUS}, + 0x00000190: {'reg': Register.EVENT_ALARM}, + 0x000001f4: {'reg': Register.EVENT_FAULT}, + 0x00000258: {'reg': Register.EVENT_BF1}, + 0x000002bc: {'reg': Register.EVENT_BF2}, + 0x00000320: {'reg': Register.TEST_IVAL_1}, + 0x000003e8: {'reg': Register.TEST_VAL_0}, + 0x0000044c: {'reg': Register.TEST_VAL_1}, # DC 1 Inpput Voltage *10 + 0x000004b0: {'reg': Register.TEST_VAL_2}, + 0x00000514: {'reg': Register.GRID_VOLTAGE}, # Grid Voltage + 0x00000578: {'reg': Register.GRID_CURRENT}, # Grid Current + 0x000005dc: {'reg': Register.TEST_VAL_3}, + 0x00000640: {'reg': Register.GRID_FREQUENCY}, + 0x000006a4: {'reg': Register.TEST_IVAL_2}, + 0x00000708: {'reg': Register.TEST_IVAL_3}, + 0x0000076c: {'reg': Register.TEST_IVAL_4}, + 0x000007d0: {'reg': Register.TEST_VAL_4}, # DC 2 Input Voltage *10 + 0x00000834: {'reg': Register.MAX_DESIGNED_POWER}, + 0x00000898: {'reg': Register.OUTPUT_POWER}, # Grid Power + 0x000008fc: {'reg': Register.DAILY_GENERATION}, # Daily Generation + 0x00000960: {'reg': Register.TOTAL_GENERATION}, # Total Genration + 0x000009c4: {'reg': Register.TEST_IVAL_5}, + 0x00000a28: {'reg': Register.TEST_VAL_10}, # Isolationsimpedanz Rx + 0x00000a8c: {'reg': Register.TEST_VAL_11}, # Isolationsimpedanz Ry + 0x00000af0: {'reg': Register.TEST_IVAL_6}, + 0x000001324: {'reg': Register.PV1_VOLTAGE}, # PV1 Voltage + 0x000001388: {'reg': Register.PV1_CURRENT}, # PV1 Current + 0x0000013ec: {'reg': Register.PV1_POWER}, # PV1 Power + 0x000001450: {'reg': Register.TEST_VAL_5}, + 0x0000015e0: {'reg': Register.PV2_VOLTAGE}, # PV2 Voltage + 0x000001644: {'reg': Register.PV2_CURRENT}, # PV2 Current + 0x0000016a8: {'reg': Register.PV2_POWER}, # PV2 Power + 0x00000170c: {'reg': Register.TEST_VAL_6}, + 0x00000189c: {'reg': Register.PV3_VOLTAGE}, + 0x000001900: {'reg': Register.PV3_CURRENT}, + 0x000001964: {'reg': Register.PV3_POWER}, + 0x0000019c8: {'reg': Register.TEST_VAL_7}, + 0x000001c20: {'reg': Register.TEST_VAL_14}, + 0x000001c84: {'reg': Register.TEST_VAL_15}, + 0x000001ce8: {'reg': Register.TEST_VAL_16}, # DC 1 Voltage + 0x000001d4c: {'reg': Register.TEST_VAL_17}, + 0x000001db0: {'reg': Register.TEST_VAL_18}, + 0x000001e14: {'reg': Register.TEST_IVAL_8}, + 0x000001e78: {'reg': Register.PV4_VOLTAGE}, + 0x000001edc: {'reg': Register.PV4_CURRENT}, + 0x000001f40: {'reg': Register.PV4_POWER}, + 0x000001fa4: {'reg': Register.TEST_VAL_8}, + 0x0000020c9: {'reg': Register.TEST_IVAL_9}, + 0x0000020db: {'reg': Register.TEST_IVAL_10}, + + 0x000002134: {'reg': Register.PV5_VOLTAGE}, + 0x000002198: {'reg': Register.PV5_CURRENT}, + 0x0000021fc: {'reg': Register.PV5_POWER}, + # 0x000002260: {'reg': Register.TEST_VAL_13}, + 0x0000023f0: {'reg': Register.PV6_VOLTAGE}, + 0x000002454: {'reg': Register.PV6_CURRENT}, + 0x0000024b8: {'reg': Register.PV6_POWER}, + # 0x00000251c: {'reg': Register.TEST_VAL_14}, + 0x000002774: {'reg': Register.TEST_VAL_24}, + 0x0000027d8: {'reg': Register.TEST_VAL_25}, + 0x00000283c: {'reg': Register.TEST_VAL_26}, # DC 2 Voltage + 0x0000028a0: {'reg': Register.TEST_VAL_27}, + 0x000002904: {'reg': Register.TEST_VAL_28}, + 0x000002968: {'reg': Register.TEST_IVAL_11}, + 0x0000029cc: {'reg': Register.TEST_IVAL_12}, + } + map_01900001 = { + 0x0000000a: {'reg': Register.PRODUCT_NAME}, + 0x00000014: {'reg': Register.MANUFACTURER}, + 0x0000001e: {'reg': Register.VERSION}, + 0x00000028: {'reg': Register.SERIAL_NUMBER}, + 0x00000032: {'reg': Register.EQUIPMENT_MODEL}, + 0x00013880: {'reg': Register.NO_INPUTS}, 0x00000640: {'reg': Register.OUTPUT_POWER}, 0x000005dc: {'reg': Register.RATED_POWER}, 0x00000514: {'reg': Register.INVERTER_TEMP}, @@ -61,12 +143,7 @@ class RegisterMap: 0x000003e8: {'reg': Register.GRID_VOLTAGE}, 0x0000044c: {'reg': Register.GRID_CURRENT}, 0x000004b0: {'reg': Register.GRID_FREQUENCY}, - 0x000cfc38: {'reg': Register.CONNECT_COUNT}, - 0x000c3500: {'reg': Register.SIGNAL_STRENGTH}, - 0x000c96a8: {'reg': Register.POWER_ON_TIME}, - 0x000d0020: {'reg': Register.COLLECT_INTERVAL}, - 0x000cf850: {'reg': Register.DATA_UP_INTERVAL}, - 0x000c7f38: {'reg': Register.COMMUNICATION_TYPE}, + 0x00000190: {'reg': Register.EVENT_ALARM}, 0x000001f4: {'reg': Register.EVENT_FAULT}, 0x00000258: {'reg': Register.EVENT_BF1}, @@ -86,6 +163,18 @@ class RegisterMap: } +class RegisterSel: + __sensor_map = { + 0x0e100000: RegisterMap.map_0e100000, + 0x01900000: RegisterMap.map_01900000, + 0x01900001: RegisterMap.map_01900001, + } + + @classmethod + def get(cls, sensor: int): + return cls.__sensor_map.get(sensor, RegisterMap.map) + + class InfosG3(Infos): __slots__ = () @@ -101,18 +190,27 @@ class InfosG3(Infos): entity strings sug_area:str ==> suggested area string from the config file''' # iterate over RegisterMap.map and get the register values - for row in RegisterMap.map.values(): + sensor = self.get_db_value(Register.SENSOR_LIST) + if "01900000" == sensor: + items = RegisterMap.map_01900000.items() + elif "01900001" == sensor: + items = RegisterMap.map_01900001.items() + else: + items = {} + + for _, row in chain(RegisterMap.map_0e100000.items(), items): reg = row['reg'] res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501 if res: yield res - def parse(self, buf, ind=0, node_id: str = '') -> \ + def parse(self, buf, ind=0, sensor: int = 0, node_id: str = '') -> \ Generator[tuple[str, bool], None, None]: '''parse a data sequence received from the inverter and stores the values in Infos.db buf: buffer of the sequence to parse''' + reg_map = RegisterSel.get(sensor) result = struct.unpack_from('!l', buf, ind) elms = result[0] i = 0 @@ -120,11 +218,11 @@ class InfosG3(Infos): while i < elms: result = struct.unpack_from('!lB', buf, ind) addr = result[0] - if addr not in RegisterMap.map: + if addr not in reg_map: row = None info_id = -1 else: - row = RegisterMap.map[addr] + row = reg_map[addr] info_id = row['reg'] data_type = result[1] ind += 5 @@ -192,3 +290,6 @@ class InfosG3(Infos): if update: self.tracer.log(level, f'[{node_id}] GEN3: {name} :' f' {result}{unit}') + + logging.log(level, f'[{node_id}] GEN3: {name} :' + f' {result}{unit}') diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 29df94c..e799a36 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -75,6 +75,7 @@ class Talent(Message): 0x87: self.get_modbus_log_lvl, 0x04: logging.INFO, } + self.sensor_list = 0 ''' Our puplic methods @@ -443,7 +444,7 @@ class Talent(Message): logger.debug(f'time: {timestamp:08x}') # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( # "%Y-%m-%d %H:%M:%S")}') - return msg_hdr_len, timestamp + return msg_hdr_len, data_id, timestamp def msg_collector_data(self): if self.ctrl.is_ind(): @@ -480,21 +481,51 @@ class Talent(Message): self.forward() - def __process_data(self, ignore_replay: bool): - msg_hdr_len, ts = self.parse_msg_header() - if ignore_replay: + def __build_model_name(self): + db = self.db + model = db.get_db_value(Register.EQUIPMENT_MODEL, None) + if model: + return + max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0) + if max_pow == 3000: + model = f'TSOL-MS{max_pow}' + self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model) + self.db.set_db_def_value(Register.MANUFACTURER, 'TSUN') + self.db.set_db_def_value(Register.NO_INPUTS, 4) + + def __process_data(self, inv_data: bool): + msg_hdr_len, data_id, ts = self.parse_msg_header() + if inv_data: + # handle register mapping + if 0 == self.sensor_list: + self.sensor_list = data_id + self.db.set_db_def_value(Register.SENSOR_LIST, + f"{self.sensor_list:08x}") + logging.debug(f"Use sensor-list: {self.sensor_list:#08x}" + f" for '{self.unique_id}'") + if data_id != self.sensor_list: + logging.warning(f'Unexpected Sensor-List:{data_id:08x}' + f' (!={self.sensor_list:08x})') + # ignore replays for inverter data age = self._utc() - self._utcfromts(ts) age = age/(3600*24) logger.debug(f"Age: {age} days") - if age > 1: + if age > 1: # is a replay? return + inv_update = False + for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len - + msg_hdr_len, self.node_id): + + msg_hdr_len, data_id, self.node_id): if update: + if key == 'inverter': + inv_update = True self._set_mqtt_timestamp(key, self._utcfromts(ts)) self.new_data[key] = True + if inv_update: + self.__build_model_name() + def msg_ota_update(self): if self.ctrl.is_req(): self.inc_counter('OTA_Start_Msg') diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index cd6d040..75f48d6 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -137,8 +137,8 @@ class RegisterMap: 0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current 0x42010038: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501 0x4201003a: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 - 0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 - 0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 + 0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 MPTT-1 Status? + 0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 MPTT-2 Status? 0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010044: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent @@ -149,13 +149,13 @@ class RegisterMap: 0x4201004e: {'reg': Register.BATT_4e, 'fmt': '!h'}, # noqa: E501 0x42010066: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501 - 0x42010068: {'reg': Register.BATT_68, 'fmt': '!h'}, # noqa: E501 - 0x4201006a: {'reg': Register.BATT_6a, 'fmt': '!h'}, # noqa: E501 + 0x42010068: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501 batterie temp 1 + 0x4201006a: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501 batterie temp 2 0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 - 0x42010070: {'reg': Register.BATT_70, 'fmt': '!h'}, # noqa: E501 - 0x42010072: {'reg': Register.BATT_72, 'fmt': '!h'}, # noqa: E501 - 0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501 + 0x42010070: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501, state of output value 0 or 1 + 0x42010072: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501 controller temp + 0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501, control input 0..2048 0x42010076: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 0x42010078: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 'calc': { diff --git a/app/src/infos.py b/app/src/infos.py index 2e752d8..13f56e7 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -138,12 +138,12 @@ class Register(Enum): BATT_4c = 1016 BATT_4e = 1017 BATT_66 = 1018 - BATT_68 = 1019 - BATT_6a = 1020 + BATT_TEMP_1 = 1019 + BATT_TEMP_2 = 1020 BATT_OUT_VOLT = 1021 BATT_OUT_CUR = 1022 - BATT_70 = 1023 - BATT_72 = 1024 + BATT_OUT_STATUS = 1023 + BATT_TEMP_3 = 1024 BATT_74 = 1025 BATT_76 = 1026 BATT_78 = 1027 @@ -151,6 +151,53 @@ class Register(Enum): BATT_PWR = 1041 BATT_OUT_PWR = 1042 + TEST_VAL_0 = 2000 + TEST_VAL_1 = 2001 + TEST_VAL_2 = 2002 + TEST_VAL_3 = 2003 + TEST_VAL_4 = 2004 + TEST_VAL_5 = 2005 + TEST_VAL_6 = 2006 + TEST_VAL_7 = 2007 + TEST_VAL_8 = 2008 + TEST_VAL_9 = 2009 + TEST_VAL_10 = 2010 + TEST_VAL_11 = 2011 + TEST_VAL_12 = 2012 + TEST_VAL_13 = 2013 + TEST_VAL_14 = 2014 + TEST_VAL_15 = 2015 + TEST_VAL_16 = 2016 + TEST_VAL_17 = 2017 + TEST_VAL_18 = 2018 + TEST_VAL_19 = 2019 + TEST_VAL_20 = 2020 + TEST_VAL_21 = 2021 + TEST_VAL_22 = 2022 + TEST_VAL_23 = 2023 + TEST_VAL_24 = 2024 + TEST_VAL_25 = 2025 + TEST_VAL_26 = 2026 + TEST_VAL_27 = 2027 + TEST_VAL_28 = 2028 + TEST_VAL_29 = 2029 + TEST_VAL_30 = 2030 + TEST_VAL_31 = 2031 + TEST_VAL_32 = 2032 + + TEST_IVAL_1 = 2041 + TEST_IVAL_2 = 2042 + TEST_IVAL_3 = 2043 + TEST_IVAL_4 = 2044 + TEST_IVAL_5 = 2045 + TEST_IVAL_6 = 2046 + TEST_IVAL_7 = 2047 + TEST_IVAL_8 = 2048 + TEST_IVAL_9 = 2049 + TEST_IVAL_10 = 2050 + TEST_IVAL_11 = 2051 + TEST_IVAL_12 = 2052 + VALUE_1 = 9000 TEST_REG1 = 10000 TEST_REG2 = 10001 @@ -263,6 +310,7 @@ class Infos: LIGHTNING = 'mdi:lightning-bolt' COUNTER = 'mdi:counter' GAUGE = 'mdi:gauge' + POWER = 'mdi:power' SOLAR_POWER_VAR = 'mdi:solar-power-variant' SOLAR_POWER = 'mdi:solar-power' WIFI = 'mdi:wifi' @@ -316,6 +364,7 @@ class Infos: __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 __work_mode_val_tpl = "{%set mode = ['Normal-Mode', 'Aging-Mode', 'ATE-Mode', 'Shielding GFDI', 'DTU-Mode'] %}{{mode[value_json['Work_Mode']|int(0)]|default(value_json['Work_Mode'])}}" # noqa: E501 __status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501 + __out_status_type_val_tpl = "{%set out_status = ['Off', 'On'] %}{{out_status[value_json['out']['Out_Status']|int(0)]|default(value_json['out']['Out_Status'])}}" # noqa: E501 __rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501 __designed_power_val_tpl = ''' {% if 'Max_Designed_Power' in value_json and @@ -466,7 +515,7 @@ class Infos: Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': 'mdi:power', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': POWER, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.INPUT_COEFFICIENT: {'name': ['inverter', 'Input_Coefficient'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'input_coef_', 'val_tpl': __input_coef_val_tpl, 'name': 'Input Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 @@ -514,7 +563,7 @@ class Infos: Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501 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.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': 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 @@ -592,18 +641,66 @@ class Infos: # 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_TEMP_1: {'name': ['batterie', 'Temp_1'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_1_', 'fmt': FMT_INT, 'name': 'Temperature-1'}}, # noqa: E501 + Register.BATT_TEMP_2: {'name': ['batterie', 'Temp_2'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_2_', 'fmt': FMT_INT, 'name': 'Temperature-2'}}, # noqa: E501 Register.BATT_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Out Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Out Current', 'icon': GAUGE, '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_OUT_STATUS: {'name': ['batterie', 'out', 'Out_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'out_status_', 'name': 'Output Status', 'val_tpl': __out_status_type_val_tpl, 'icon': POWER}}, # noqa: E501 + Register.BATT_TEMP_3: {'name': ['batterie', 'Controller_Temp'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_3_', 'fmt': FMT_INT, 'name': 'Ctrl Temperature'}}, # 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 Register.BATT_PV_PWR: {'name': ['batterie', 'PV_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'pv_power_', 'fmt': FMT_INT, 'name': 'PV Power'}}, # noqa: E501 Register.BATT_PWR: {'name': ['batterie', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_', 'fmt': FMT_INT, 'name': 'Batterie Power'}}, # noqa: E501 Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Out Power'}}, # noqa: E501 + + Register.TEST_VAL_0: {'name': ['input', 'Val_0'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_0_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_1: {'name': ['input', 'Val_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_2: {'name': ['input', 'Val_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_3: {'name': ['input', 'Val_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_4: {'name': ['input', 'Val_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_5: {'name': ['input', 'Val_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_6: {'name': ['input', 'Val_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_7: {'name': ['input', 'Val_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_8: {'name': ['input', 'Val_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_9: {'name': ['input', 'Val_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_10: {'name': ['input', 'Val_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_11: {'name': ['input', 'Val_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_12: {'name': ['input', 'Val_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_13: {'name': ['input', 'Val_13'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_13_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_14: {'name': ['input', 'Val_14'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_14_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_15: {'name': ['input', 'Val_15'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_15_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_16: {'name': ['input', 'Val_16'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_16_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_17: {'name': ['input', 'Val_17'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_17_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_18: {'name': ['input', 'Val_18'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_18_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_19: {'name': ['input', 'Val_19'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_19_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_20: {'name': ['input', 'Val_20'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_20_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_21: {'name': ['input', 'Val_21'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_21_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_22: {'name': ['input', 'Val_22'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_22_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_23: {'name': ['input', 'Val_23'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_23_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_24: {'name': ['input', 'Val_24'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_24_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_25: {'name': ['input', 'Val_25'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_25_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_26: {'name': ['input', 'Val_26'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_26_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_27: {'name': ['input', 'Val_27'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_27_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_28: {'name': ['input', 'Val_28'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_28_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_29: {'name': ['input', 'Val_29'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_29_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_30: {'name': ['input', 'Val_30'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_30_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_31: {'name': ['input', 'Val_31'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_31_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_VAL_32: {'name': ['input', 'Val_32'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_32_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + + Register.TEST_IVAL_1: {'name': ['input', 'iVal_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_2: {'name': ['input', 'iVal_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_3: {'name': ['input', 'iVal_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_4: {'name': ['input', 'iVal_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_5: {'name': ['input', 'iVal_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_6: {'name': ['input', 'iVal_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_7: {'name': ['input', 'iVal_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_8: {'name': ['input', 'iVal_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_9: {'name': ['input', 'iVal_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_10: {'name': ['input', 'iVal_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_11: {'name': ['input', 'iVal_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.TEST_IVAL_12: {'name': ['input', 'iVal_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 } @property diff --git a/app/src/messages.py b/app/src/messages.py index 7efaa86..e067df6 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -151,11 +151,11 @@ class Message(ProtocolIfc): self.mb_start_reg = scan['start'] self.mb_step = scan['step'] self.mb_bytes = scan['bytes'] - # if 'client_mode' in self.db and \ - # self.db.client_mode: - self.mb_start_reg = scan['start'] - # else: - # self.mb_start_reg = scan['start'] - scan['step'] + if 'client_mode' in inv: + self.mb_start_reg = scan['start'] + else: + self.mb_start_reg = scan['start'] - scan['step'] + self.mb_start_reg &= 0xffff if self.mb: self.mb.set_node_id(self.node_id) diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py index da3eaed..d0425e8 100644 --- a/app/tests/test_infos_g3.py +++ b/app/tests/test_infos_g3.py @@ -93,6 +93,86 @@ def contr2_data_seq(): # Get Time Request message msg += b'\x00\x00\x00' return msg +@pytest.fixture +def contr3_data_seq(): # Get Time Request message + msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' # | ..^.....9..+.T.R + msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x32\x2e\x30\x31\x2e\x31\x33\x00' # | SW_400_V2.01.13. + msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' # | .'.T.Raymon../.T + msg += b'\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88' # | .RSW-1-10001..Z. + msg += b'\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f' # | T.t.raymoniot.co + msg += b'\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61' # | m..Z.T.logger.ta + msg += b'\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e' # | lent-monitoring. + msg += b'\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | com../.T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32\xe8\x54\x10\xff' # | ...........2.T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .6.T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff\xff\xff\xff\xff' # | .......:.T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3e\xa0\x54' # | .............>.T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J + msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT.............. + msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i + msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................ + msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I.. + msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I + msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I....... + msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I..... + msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI... + msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I. + msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........ + msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I...... + msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I.... + msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I.. + msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L......... + msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI... + msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I. + msg += b'\x00\x00\xff' + return msg + @pytest.fixture def inv_data_seq(): # Data indication from the controller msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28' @@ -140,6 +220,153 @@ def inv_data_seq2(): # Data indication from the controller msg += b'\x53\x00\x00' return msg +@pytest.fixture +def inv_data_seq3(): # Inverter indication from MS-2000 + + msg = b'\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS.. + msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S.... + msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S.... + msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S..... + msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I.... + msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS.. + msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S.... + msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^ + msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S. + msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS... + msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS..... + msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS + msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I.. + msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h + msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC. + msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F + msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L..... + msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH... + msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S..... + msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3... + msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F....... + msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H. + msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA.... + msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S... + msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S..... + msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S.. + msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS.... + msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....S.....?S.... + msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B + msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS. + msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS... + msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS..... + msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F..... + msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB.. + msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S... + msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S..... + msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS..... + msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#..... + msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB.... + msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F... + msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3 + msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F> + msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... . + msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S. + msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S... + msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S.... + msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S + msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S.. + msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S.... + msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... . + msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S. + msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F. + msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"` + msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#( + msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S. + msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S... + msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....# + msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S + msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S.. + msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S.... + msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#. + msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F. + msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$. + msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F...... + msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H + msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS. + msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS... + msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....& + msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS + msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS.. + msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US.... + msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X + msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S. + msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F. + msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(L.... + msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{ + msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L + msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S + msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S.. + msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S.... + msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6. + msg += b'\x53\x00\x66' # | S.f' + return msg + @pytest.fixture def inv_data_new(): # Data indication from DSP V5.0.17 msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00' @@ -254,7 +481,7 @@ def inv_data_seq2_zero(): # Data indication from the controller def test_parse_control(contr_data_seq): i = InfosG3() - for key, result in i.parse (contr_data_seq): + for key, result in i.parse (contr_data_seq, sensor=0x0e100000): pass # side effect in calling i.parse() assert json.dumps(i.db) == json.dumps( @@ -262,15 +489,23 @@ def test_parse_control(contr_data_seq): def test_parse_control2(contr2_data_seq): i = InfosG3() - for key, result in i.parse (contr2_data_seq): + for key, result in i.parse (contr2_data_seq, sensor=0x0e100000): pass # side effect in calling i.parse() assert json.dumps(i.db) == json.dumps( {"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}}) +def test_parse_control3(contr3_data_seq): + i = InfosG3() + for key, result in i.parse (contr3_data_seq, sensor=0x0e100000): + pass # side effect in calling i.parse() + + assert json.dumps(i.db) == json.dumps( +{"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}}) + def test_parse_inverter(inv_data_seq): i = InfosG3() - for key, result in i.parse (inv_data_seq): + for key, result in i.parse (inv_data_seq, sensor=0x01900001): pass # side effect in calling i.parse() assert json.dumps(i.db) == json.dumps( @@ -278,10 +513,10 @@ def test_parse_inverter(inv_data_seq): def test_parse_cont_and_invert(contr_data_seq, inv_data_seq): i = InfosG3() - for key, result in i.parse (contr_data_seq): + for key, result in i.parse (contr_data_seq, sensor=0x0e100000): pass # side effect in calling i.parse() - for key, result in i.parse (inv_data_seq): + for key, result in i.parse (inv_data_seq, sensor=0x01900001): pass # side effect in calling i.parse() assert json.dumps(i.db) == json.dumps( @@ -289,10 +524,29 @@ def test_parse_cont_and_invert(contr_data_seq, inv_data_seq): "collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}, "inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}}) +def test_parse_cont_and_invert2(contr3_data_seq, inv_data_seq3): + i = InfosG3() + for key, result in i.parse (contr3_data_seq, sensor=0x0e100000): + pass # side effect in calling i.parse() + + for key, result in i.parse (inv_data_seq3, sensor=0x01900000): + pass # side effect in calling i.parse() + + assert json.dumps(i.db) == json.dumps( + { +"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}, +"env": {"Inverter_Status": 0}, +"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0}, +"input": {"iVal_1": 1, "Val_0": 655.28, "Val_1": 327.4, "Val_2": 0.0, "Val_3": 0.0, "iVal_2": 3, "iVal_3": 12, "iVal_4": 80, "Val_4": 327.4, "iVal_5": 0, "Val_10": 30.25, "Val_11": 1.35, "iVal_6": 12, "pv1": {"Voltage": 78.6, "Current": 0.0, "Power": 0.0}, "Val_5": 110.0, "pv2": {"Voltage": 58.1, "Current": 0.0, "Power": 0.0}, "Val_6": 110.0, "pv3": {"Voltage": 58.8, "Current": 0.0, "Power": 0.0}, "Val_7": 110.0, "Val_14": 0.01, "Val_15": 0.0, "Val_16": 33.0, "Val_17": 0.0, "Val_18": 0.0, "iVal_8": 2, "pv4": {"Voltage": 17.4, "Current": 0.02, "Power": 0.3}, "Val_8": 4.8, "iVal_10": 1, "pv5": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv6": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "Val_24": 0.0, "Val_25": 0.0, "Val_26": 32.99, "Val_27": 0.0, "Val_28": 0.0, "iVal_11": 2, "iVal_12": 3}, +"grid": {"Voltage": 238.5, "Current": 0.05, "Frequency": 50.0, "Output_Power": 0.0}, +"inverter": {"Max_Designed_Power": 3000}, +"total": {"Total_Generation": 29.86} + }) def test_build_ha_conf1(contr_data_seq): i = InfosG3() i.static_init() # initialize counter + i.set_db_def_value(Register.SENSOR_LIST, "01900001") tests = 0 for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'): @@ -328,6 +582,7 @@ def test_build_ha_conf1(contr_data_seq): def test_build_ha_conf2(contr_data_seq): i = InfosG3() i.static_init() # initialize counter + i.set_db_def_value(Register.SENSOR_LIST, "01900001") tests = 0 for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): @@ -352,11 +607,12 @@ def test_build_ha_conf2(contr_data_seq): def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2): i = InfosG3() - for key, result in i.parse (contr_data_seq): + i.set_db_def_value(Register.SENSOR_LIST, "01900001") + for key, result in i.parse (contr_data_seq, sensor=0x0e100000): pass # side effect in calling i.parse() - for key, result in i.parse (inv_data_seq): + for key, result in i.parse (inv_data_seq, sensor=0x01900001): pass # side effect in calling i.parse() - for key, result in i.parse (inv_data_seq2): + for key, result in i.parse (inv_data_seq2, sensor=0x01900001): pass # side effect in calling i.parse() tests = 0 @@ -390,9 +646,10 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2): def test_build_ha_conf4(contr_data_seq, inv_data_seq): i = InfosG3() - for key, result in i.parse (contr_data_seq): + i.set_db_def_value(Register.SENSOR_LIST, "01900001") + for key, result in i.parse (contr_data_seq, sensor=0x0e100000): pass # side effect in calling i.parse() - for key, result in i.parse (inv_data_seq): + for key, result in i.parse (inv_data_seq, sensor=0x01900001): pass # side effect in calling i.parse() i.set_db_def_value(Register.MAC_ADDR, "00a057123456") @@ -414,10 +671,37 @@ def test_build_ha_conf4(contr_data_seq, inv_data_seq): tests +=1 assert tests==1 +def test_build_ha_conf5(contr3_data_seq, inv_data_seq3): + i = InfosG3() + i.set_db_def_value(Register.SENSOR_LIST, "01900000") + for key, result in i.parse (contr3_data_seq, sensor=0x0e100000): + pass # side effect in calling i.parse() + for key, result in i.parse (inv_data_seq3, sensor=0x01900000): + pass # side effect in calling i.parse() + i.set_db_def_value(Register.MAC_ADDR, "00a057123456") + + tests = 0 + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'): + if id == 'signal_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:56"]]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + assert tests==1 + + i.set_db_def_value(Register.MAC_ADDR, "00:a0:57:12:34:57") + + tests = 0 + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'): + if id == 'signal_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:57"]]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + assert tests==1 + def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero): i = InfosG3() tests = 0 - for key, update in i.parse (inv_data_seq2): + for key, update in i.parse (inv_data_seq2, sensor=0x01900001): if key == 'total' or key == 'inverter' or key == 'env': assert update == True tests +=1 @@ -426,7 +710,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero): assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23}) tests = 0 - for key, update in i.parse (inv_data_seq2): + for key, update in i.parse (inv_data_seq2, sensor=0x01900001): if key == 'total' or key == 'env': assert update == False tests +=1 @@ -438,7 +722,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero): assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": -1, "Input_Coefficient": -0.1, "Output_Coefficient": 100.0, "No_Inputs": 2}) tests = 0 - for key, update in i.parse (inv_data_seq2_zero): + for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001): if key == 'total': assert update == False tests +=1 @@ -453,7 +737,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero): def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero): i = InfosG3() tests = 0 - for key, update in i.parse (inv_data_seq2_zero): + for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001): if key == 'total': assert update == False tests +=1 @@ -467,7 +751,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero): assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0}) tests = 0 - for key, update in i.parse (inv_data_seq2_zero): + for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001): if key == 'total' or key == 'env': assert update == False tests +=1 @@ -478,7 +762,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero): assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0}) tests = 0 - for key, update in i.parse (inv_data_seq2): + for key, update in i.parse (inv_data_seq2, sensor=0x01900001): if key == 'total' or key == 'env': tests +=1 @@ -489,7 +773,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero): def test_new_data_types(inv_data_new): i = InfosG3() tests = 0 - for key, update in i.parse (inv_data_new): + for key, update in i.parse (inv_data_new, sensor=0x01900001): if key == 'events': tests +=1 elif key == 'inverter': @@ -514,7 +798,7 @@ def test_invalid_data_type(invalid_data_seq): assert val == 0 - for key, result in i.parse (invalid_data_seq): + for key, result in i.parse (invalid_data_seq, sensor=0x01900001): pass # side effect in calling i.parse() assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}}) diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 366743d..4cfefcf 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -159,9 +159,9 @@ def test_parse_4210_3026(batterie_data: bytes): "pv2": {"Voltage": 33.72, "Current": 0.0}, "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, - "Reg_68": 15, "Reg_6a": 15, - "out": {"Voltage": 0.14, "Current": 0.0, "Power": 0.0}, - "Reg_70": 0, "Reg_72": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513, + "Temp_1": 15, "Temp_2": 15, + "out": {"Voltage": 0.14, "Current": 0.0, "Out_Status": 0, "Power": 0.0}, + "Controller_Temp": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513, "PV_Power": 37.9232, "Power": -1.0268000000000002}, }) @@ -179,9 +179,9 @@ def test_parse_4210_3026_incomplete(batterie_data2: bytes): "pv2": {"Voltage": 33.72, "Current": 0.0}, "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, - "Reg_68": 15, "Reg_6a": 15, - "out": {"Voltage": 0.14, "Current": None, "Power": None}, - "Reg_70": None, "Reg_72": None, "Reg_74": None, "Reg_76": None, "Reg_78": None, + "Temp_1": 15, "Temp_2": 15, + "out": {"Voltage": 0.14, "Current": None, "Out_Status": None, "Power": None}, + "Controller_Temp": None, "Reg_74": None, "Reg_76": None, "Reg_78": None, "PV_Power": 37.9232, "Power": -1.0268000000000002}, }) diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 9b7d5ed..a19286c 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -801,11 +801,11 @@ def config_tsun_inv1(): @pytest.fixture def config_tsun_scan(): - Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xff80, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}} + Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xffc0, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}} @pytest.fixture def config_tsun_scan_dcu(): - Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'suggested_area':'roof', 'sensor_list': 0}}} + Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'client_mode': {'host': '192.168.1.1.'}, 'suggested_area':'roof', 'sensor_list': 0}}} @pytest.fixture def config_tsun_dcu1(): @@ -2034,6 +2034,10 @@ async def test_start_client_mode_scan(config_tsun_scan_dcu, str_test_ip, dcu_mod assert m.mb_timer.tim == None assert asyncio.get_running_loop() == m.mb_timer.loop await m.send_start_cmd(get_dcu_sn_int(), str_test_ip, False, m.mb_first_timeout) + assert m.mb_start_reg == 0x0000 + assert m.mb_step == 0x100 + assert m.mb_bytes == 0x2d + assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x95\x15') assert m.mb_scan == True m.mb_step = 0 diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 2d6029c..2e7a4f0 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -7,6 +7,7 @@ from cnf.config import Config from infos import Infos, Register from modbus import Modbus from messages import State +from mock import patch pytest_plugins = ('pytest_asyncio',) @@ -468,15 +469,15 @@ def config_tsun_allow_all(): @pytest.fixture def config_no_tsun_inv1(): - Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} + Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': True, 'suggested_area':'roof'}}} @pytest.fixture def config_tsun_inv1(): - Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} + Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0x01900001, 'modbus_polling': True, 'suggested_area':'roof'}}} @pytest.fixture def config_no_modbus_poll(): - Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}} + Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': False, 'suggested_area':'roof'}}} @pytest.fixture def msg_ota_req(): # Over the air update request from tsun cloud @@ -817,6 +818,236 @@ def multiple_recv_buf(): # There are three message in the buffer, but the second msg += b'\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' # | 0....T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J + msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT.............. + msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i + msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................ + msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I.. + msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I + msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I....... + msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I..... + msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI... + msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I. + msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........ + msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I...... + msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I.... + msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I.. + msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L......... + msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI... + msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I. + msg += b'\x00\x00\xff' + return msg + +@pytest.fixture +def msg_inverter_ms3000_ind(): # Data indication from the controller + msg = b'\x00\x00\x08\xff\x10R170000000000001\x91\x04\x01\x90\x00\x00\x10R170000000000001' + msg += b'\x01\x00\x00\x01' + msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS.. + msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S.... + msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S.... + msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S..... + msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I.... + msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS.. + msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S.... + msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^ + msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S. + msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS... + msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS..... + msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS + msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I.. + msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h + msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC. + msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F + msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L..... + msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH... + msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S..... + msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3... + msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F....... + msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H. + msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA.... + msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S... + msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S..... + msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S.. + msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS.... + msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....S.....?S.... + msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B + msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS. + msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS... + msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS..... + msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F..... + msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB.. + msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S... + msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S..... + msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS..... + msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#..... + msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB.... + msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F... + msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3 + msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F> + msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... . + msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S. + msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S... + msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S.... + msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S + msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S.. + msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S.... + msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... . + msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S. + msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F. + msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"` + msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#( + msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S. + msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S... + msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....# + msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S + msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S.. + msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S.... + msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#. + msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F. + msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$. + msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F...... + msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H + msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS. + msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS... + msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....& + msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS + msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS.. + msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US.... + msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X + msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S. + msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F. + msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(L.... + msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{ + msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L + msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S + msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S.. + msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S.... + msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6. + msg += b'\x53\x00\x66' # | S.f' + return msg + def test_read_message(msg_contact_info): Config.act_config = {'tsun':{'enabled': True}} m = MemoryStream(msg_contact_info, (0,)) @@ -1570,6 +1801,64 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack): m.close() assert m.db.get_db_value(Register.INVERTER_STATUS) == 0 +def test_msg_inv_ind4(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack): + '''Check sonar_lists of MS-3000 inverter''' + _ = config_tsun_inv1 + tracer.setLevel(logging.DEBUG) + with patch.object(logging, 'warning') as spy: + m = MemoryStream(msg_inverter_ms3000_ind, (0,)) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Invalid_Data_Type'] = 0 + m.read() # read complete msg, and dispatch msg + spy.assert_not_called() + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Invalid_Data_Type'] == 0 + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.id_str == b"R170000000000001" + assert m.unique_id == 'R170000000000001' + assert int(m.ctrl)==145 + assert m.msg_id==4 + assert m.header_len==23 + assert m.data_len==2284 + m.ts_offset = 0 + m._update_header(m.ifc.fwd_fifo.peek()) + assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind + assert m.ifc.tx_fifo.get()==msg_inverter_ack + assert m.db.get_db_value(Register.INVERTER_STATUS) == 0 + assert m.db.get_db_value(Register.TS_GRID) == 1739866976 + m.db.db['grid'] = {'Output_Power': 100} + m.close() + +def test_msg_inv_ind5(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack): + '''Check that unexpected sonar_lists will log a warning''' + _ = config_tsun_inv1 + tracer.setLevel(logging.DEBUG) + with patch.object(logging, 'warning') as spy: + m = MemoryStream(msg_inverter_ms3000_ind, (0,)) + m.sensor_list = 0x01900002 # change the expected sensor_list + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Invalid_Data_Type'] = 0 + m.read() # read complete msg, and dispatch msg + spy.assert_called() + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Invalid_Data_Type'] == 0 + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.id_str == b"R170000000000001" + assert m.unique_id == 'R170000000000001' + assert int(m.ctrl)==145 + assert m.msg_id==4 + assert m.header_len==23 + assert m.data_len==2284 + m.ts_offset = 0 + m._update_header(m.ifc.fwd_fifo.peek()) + assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind + assert m.ifc.tx_fifo.get()==msg_inverter_ack + assert m.db.get_db_value(Register.INVERTER_STATUS) == 0 + assert m.db.get_db_value(Register.TS_GRID) == 1739866976 + m.db.db['grid'] = {'Output_Power': 100} + m.close() def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack): _ = config_tsun_inv1 @@ -1614,6 +1903,18 @@ def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid): assert m.db.stat['proxy']['Unknown_Ctrl'] == 1 m.close() +def test_build_modell_3000(config_tsun_allow_all, msg_inverter_ms3000_ind): + _ = config_tsun_allow_all + m = MemoryStream(msg_inverter_ms3000_ind, (0,)) + assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) + assert None == m.db.get_db_value(Register.RATED_POWER, None) + assert None == m.db.get_db_value(Register.INVERTER_TEMP, None) + m.read() # read complete msg, and dispatch msg + assert 3000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) + assert 0 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 'TSOL-MS3000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) + m.close() + def test_msg_ota_req(config_tsun_inv1, msg_ota_req): _ = config_tsun_inv1 m = MemoryStream(msg_ota_req, (0,), False) diff --git a/system_tests/test_tcp_socket.py b/system_tests/test_tcp_socket.py index e663577..9c459b7 100644 --- a/system_tests/test_tcp_socket.py +++ b/system_tests/test_tcp_socket.py @@ -93,6 +93,155 @@ def msg_inverter_ind(): # Data indication from the inverter msg += b'\x53\x00\x00' return msg +@pytest.fixture +def msg_inverter_ind2(): # Data indication from the inverter + msg = b'\x00\x00\x08\xff\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x00\x10'+get_inv_no() + msg += b'\x01\x00\x00\x01' + msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS.. + msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S.... + msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S.... + msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S..... + msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I.... + msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS.. + msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S.... + msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^ + msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S. + msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS... + msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS..... + msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS + msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I.. + msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h + msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC. + msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F + msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L..... + msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH... + msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S..... + msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3... + msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F....... + msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H. + msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA.... + msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S... + msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S..... + msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S.. + msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS.... + msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....S.....?S.... + msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B + msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS. + msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS... + msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS..... + msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F..... + msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB.. + msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S... + msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S..... + msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS..... + msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#..... + msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB.... + msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F... + msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3 + msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F> + msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... . + msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S. + msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S... + msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S.... + msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S + msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S.. + msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S.... + msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... . + msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S. + msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F. + msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"` + msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#( + msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S. + msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S... + msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....# + msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S + msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S.. + msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S.... + msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#. + msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F. + msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$. + msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F...... + msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H + msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS. + msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS... + msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....& + msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS + msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS.. + msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US.... + msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X + msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S. + msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F. + msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(L.... + msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{ + msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L + msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S + msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S.. + msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S.... + msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6. + msg += b'\x53\x00\x66' # | S.f' + return msg + + @pytest.fixture def msg_ota_update_req(): # Over the air update request from talent cloud msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35' @@ -234,3 +383,18 @@ def test_ota_req(client_connection, msg_ota_update_req): _ = s.recv(1024) except TimeoutError: pass + +def test_send_inv_data2(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inverter_ind2): + s = client_connection + try: + s.sendall(msg_timestamp_req) + _ = s.recv(1024) + except TimeoutError: + pass + # time.sleep(32.5) + # assert data == msg_timestamp_resp + try: + s.sendall(msg_inverter_ind2) + _ = s.recv(1024) + except TimeoutError: + pass From 7cf9e98c7f79f466b56a64bc92e8dcb6b4eddbf2 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 16 Mar 2025 19:34:34 +0100 Subject: [PATCH 20/23] Merge branch 'renovate/python-3.x' of https://github.com/s-allius/tsun-gen3-proxy into renovate/python-3.x --- .github/workflows/python-app.yml | 4 ++-- CHANGELOG.md | 1 + README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index fae9304..f738b28 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -37,10 +37,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up Python 3.12 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/CHANGELOG.md b/CHANGELOG.md index 33799e4..31ac310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- update dependency python to 3.13 - add initial support for TSUN MS-3000 - add initial apparmor support [#293](https://github.com/s-allius/tsun-gen3-proxy/issues/293) - add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292) diff --git a/README.md b/README.md index 186c828..73ebe5a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

integration

License: BSD-3-Clause - Supported Python versions + Supported Python versions Supported aiomqtt versions Supported aiocron versions Supported toml versions From dff8934b82136b922dd84d66b501fcb7a9b5c5fa Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:10:10 +0100 Subject: [PATCH 21/23] Dcu1000 (#312) * set equipment model dor DCU1000 devices * DCU1000: add temp sensor and mppt states * DCU1000: add total generation * add more DCU1000 registers for MODBUS polling * improve names of batterie measurements * add more diagnostic registers * adapt unit tests * move uml files into subfolder * add sensors for batterie cell voltages * swap On and Off for MPPT status --- app/{ => docu}/proxy.svg | 0 app/{ => docu}/proxy.yuml | 0 app/{ => docu}/proxy_2.svg | 0 app/{ => docu}/proxy_2.yuml | 0 app/{ => docu}/proxy_3.svg | 0 app/{ => docu}/proxy_3.yuml | 0 app/src/gen3plus/infos_g3p.py | 31 +++++++---- app/src/gen3plus/solarman_v5.py | 5 +- app/src/infos.py | 92 ++++++++++++++++++++------------- app/src/modbus.py | 39 +++++++++----- app/tests/test_infos_g3p.py | 16 +++--- 11 files changed, 116 insertions(+), 67 deletions(-) rename app/{ => docu}/proxy.svg (100%) rename app/{ => docu}/proxy.yuml (100%) rename app/{ => docu}/proxy_2.svg (100%) rename app/{ => docu}/proxy_2.yuml (100%) rename app/{ => docu}/proxy_3.svg (100%) rename app/{ => docu}/proxy_3.yuml (100%) diff --git a/app/proxy.svg b/app/docu/proxy.svg similarity index 100% rename from app/proxy.svg rename to app/docu/proxy.svg diff --git a/app/proxy.yuml b/app/docu/proxy.yuml similarity index 100% rename from app/proxy.yuml rename to app/docu/proxy.yuml diff --git a/app/proxy_2.svg b/app/docu/proxy_2.svg similarity index 100% rename from app/proxy_2.svg rename to app/docu/proxy_2.svg diff --git a/app/proxy_2.yuml b/app/docu/proxy_2.yuml similarity index 100% rename from app/proxy_2.yuml rename to app/docu/proxy_2.yuml diff --git a/app/proxy_3.svg b/app/docu/proxy_3.svg similarity index 100% rename from app/proxy_3.svg rename to app/docu/proxy_3.svg diff --git a/app/proxy_3.yuml b/app/docu/proxy_3.yuml similarity index 100% rename from app/proxy_3.yuml rename to app/docu/proxy_3.yuml diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 75f48d6..4cbe08c 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -136,25 +136,36 @@ class RegisterMap: 0x42010034: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage 0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current 0x42010038: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501 - 0x4201003a: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x4201003a: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 MPTT-1 Status? 0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 MPTT-2 Status? 0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010044: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent - 0x42010046: {'reg': Register.BATT_46, 'fmt': '!h'}, # noqa: E501 - 0x42010048: {'reg': Register.BATT_48, 'fmt': '!h'}, # noqa: E501 - 0x4201004a: {'reg': Register.BATT_4a, 'fmt': '!h'}, # noqa: E501 - 0x4201004c: {'reg': Register.BATT_4c, 'fmt': '!h'}, # noqa: E501 - 0x4201004e: {'reg': Register.BATT_4e, 'fmt': '!h'}, # noqa: E501 + 0x42010046: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010048: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201004a: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201004c: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201004e: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010050: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010052: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010054: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010056: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010058: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201005a: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201005c: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x4201005e: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010060: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010062: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x42010064: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 - 0x42010066: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501 - 0x42010068: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501 batterie temp 1 - 0x4201006a: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501 batterie temp 2 + 0x42010066: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501 + 0x42010068: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501 + 0x4201006a: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501 0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x42010070: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501, state of output value 0 or 1 - 0x42010072: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501 controller temp + 0x42010072: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501 controller temp 0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501, control input 0..2048 0x42010076: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 0x42010078: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 56e081b..90ac323 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -392,10 +392,13 @@ class SolarmanV5(SolarmanBase): def _set_config_parms(self, inv: dict, serial_no: str = ""): '''init connection with params from the configuration''' super()._set_config_parms(inv) + snr = serial_no[:3] + if '410' == snr: + self.db.set_db_def_value(Register.EQUIPMENT_MODEL, + 'TSOL-DC1000') self.sensor_list = inv['sensor_list'] if 0 == self.sensor_list: - snr = serial_no[:3] if '410' == snr: self.sensor_list = 0x3026 self.mb_regs = [{'addr': 0x0000, 'len': 45}] diff --git a/app/src/infos.py b/app/src/infos.py index 13f56e7..3bc6a4c 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -126,27 +126,38 @@ class Register(Enum): BATT_PV2_VOLT = 1002 BATT_PV2_CUR = 1003 BATT_38 = 1004 - BATT_3a = 1005 + BATT_TOTAL_GEN = 1005 BATT_STATUS_1 = 1006 BATT_STATUS_2 = 1007 BATT_VOLT = 1010 BATT_CUR = 1011 BATT_SOC = 1012 - BATT_46 = 1013 - BATT_48 = 1014 - BATT_4a = 1015 - BATT_4c = 1016 - BATT_4e = 1017 - BATT_66 = 1018 - BATT_TEMP_1 = 1019 - BATT_TEMP_2 = 1020 - BATT_OUT_VOLT = 1021 - BATT_OUT_CUR = 1022 - BATT_OUT_STATUS = 1023 - BATT_TEMP_3 = 1024 - BATT_74 = 1025 - BATT_76 = 1026 - BATT_78 = 1027 + BATT_CELL1_VOLT = 1013 + BATT_CELL2_VOLT = 1014 + BATT_CELL3_VOLT = 1015 + BATT_CELL4_VOLT = 1016 + BATT_CELL5_VOLT = 1017 + BATT_CELL6_VOLT = 1018 + BATT_CELL7_VOLT = 1019 + BATT_CELL8_VOLT = 1020 + BATT_CELL9_VOLT = 1021 + BATT_CELL10_VOLT = 1022 + BATT_CELL11_VOLT = 1023 + BATT_CELL12_VOLT = 1024 + BATT_CELL13_VOLT = 1025 + BATT_CELL14_VOLT = 1026 + BATT_CELL15_VOLT = 1027 + BATT_CELL16_VOLT = 1028 + BATT_TEMP_1 = 1029 + BATT_TEMP_2 = 1030 + BATT_TEMP_3 = 1031 + BATT_OUT_VOLT = 1032 + BATT_OUT_CUR = 1033 + BATT_OUT_STATUS = 1034 + BATT_TEMP_4 = 1035 + BATT_74 = 1036 + BATT_76 = 1037 + BATT_78 = 1038 BATT_PV_PWR = 1040 BATT_PWR = 1041 BATT_OUT_PWR = 1042 @@ -364,6 +375,8 @@ class Infos: __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 __work_mode_val_tpl = "{%set mode = ['Normal-Mode', 'Aging-Mode', 'ATE-Mode', 'Shielding GFDI', 'DTU-Mode'] %}{{mode[value_json['Work_Mode']|int(0)]|default(value_json['Work_Mode'])}}" # noqa: E501 __status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501 + __mppt1_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_1']|int(0)]|default(value_json['Status_1'])}}" # noqa: E501 + __mppt2_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_2']|int(0)]|default(value_json['Status_2'])}}" # noqa: E501 __out_status_type_val_tpl = "{%set out_status = ['Off', 'On'] %}{{out_status[value_json['out']['Out_Status']|int(0)]|default(value_json['out']['Out_Status'])}}" # noqa: E501 __rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501 __designed_power_val_tpl = ''' @@ -628,32 +641,41 @@ class Infos: 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': 'kWh', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_3a_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.BATT_STATUS_1: {'name': ['batterie', 'Status_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.BATT_STATUS_2: {'name': ['batterie', 'Status_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.BATT_VOLT: {'name': ['batterie', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_bat_', 'fmt': FMT_FLOAT, 'name': 'Bat Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.BATT_CUR: {'name': ['batterie', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_bat_', 'fmt': FMT_FLOAT, 'name': 'Bat Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_TOTAL_GEN: {'name': ['batterie', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'batterie', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501 + Register.BATT_STATUS_1: {'name': ['batterie', 'Status_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status1_', 'name': 'MPPT-1 Status', 'val_tpl': __mppt1_status_type_val_tpl, 'icon': POWER}}, # noqa: E501 + Register.BATT_STATUS_2: {'name': ['batterie', 'Status_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status2_', 'name': 'MPPT-2 Status', 'val_tpl': __mppt2_status_type_val_tpl, 'icon': POWER}}, # noqa: E501 + Register.BATT_VOLT: {'name': ['batterie', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CUR: {'name': ['batterie', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Current', 'icon': GAUGE, '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 (SOC)', '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_TEMP_1: {'name': ['batterie', 'Temp_1'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_1_', 'fmt': FMT_INT, 'name': 'Temperature-1'}}, # noqa: E501 - Register.BATT_TEMP_2: {'name': ['batterie', 'Temp_2'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_2_', 'fmt': FMT_INT, 'name': 'Temperature-2'}}, # noqa: E501 - Register.BATT_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Out Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Out Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL1_VOLT: {'name': ['batterie', 'Cell', 'Volt1'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell1_', 'val_tpl': "{{ (value_json['Cell']['Volt1'] | float)}}", 'name': 'Cell-01 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL3_VOLT: {'name': ['batterie', 'Cell', 'Volt3'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell3_', 'val_tpl': "{{ (value_json['Cell']['Volt2'] | float)}}", 'name': 'Cell-03 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL4_VOLT: {'name': ['batterie', 'Cell', 'Volt4'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell4_', 'val_tpl': "{{ (value_json['Cell']['Volt3'] | float)}}", 'name': 'Cell-04 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL2_VOLT: {'name': ['batterie', 'Cell', 'Volt2'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell2_', 'val_tpl': "{{ (value_json['Cell']['Volt4'] | float)}}", 'name': 'Cell-02 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL5_VOLT: {'name': ['batterie', 'Cell', 'Volt5'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell5_', 'val_tpl': "{{ (value_json['Cell']['Volt5'] | float)}}", 'name': 'Cell-05 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL6_VOLT: {'name': ['batterie', 'Cell', 'Volt6'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell6_', 'val_tpl': "{{ (value_json['Cell']['Volt6'] | float)}}", 'name': 'Cell-06 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL7_VOLT: {'name': ['batterie', 'Cell', 'Volt7'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell7_', 'val_tpl': "{{ (value_json['Cell']['Volt7'] | float)}}", 'name': 'Cell-07 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL8_VOLT: {'name': ['batterie', 'Cell', 'Volt8'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell8_', 'val_tpl': "{{ (value_json['Cell']['Volt8'] | float)}}", 'name': 'Cell-08 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL9_VOLT: {'name': ['batterie', 'Cell', 'Volt9'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell9_', 'val_tpl': "{{ (value_json['Cell']['Volt9'] | float)}}", 'name': 'Cell-09 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL10_VOLT: {'name': ['batterie', 'Cell', 'Volt10'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell10_', 'val_tpl': "{{ (value_json['Cell']['Volt10'] | float)}}", 'name': 'Cell-10 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL11_VOLT: {'name': ['batterie', 'Cell', 'Volt11'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell11_', 'val_tpl': "{{ (value_json['Cell']['Volt11'] | float)}}", 'name': 'Cell-11 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL12_VOLT: {'name': ['batterie', 'Cell', 'Volt12'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell12_', 'val_tpl': "{{ (value_json['Cell']['Volt12'] | float)}}", 'name': 'Cell-12 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL13_VOLT: {'name': ['batterie', 'Cell', 'Volt13'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell13_', 'val_tpl': "{{ (value_json['Cell']['Volt13'] | float)}}", 'name': 'Cell-13 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL14_VOLT: {'name': ['batterie', 'Cell', 'Volt14'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell14_', 'val_tpl': "{{ (value_json['Cell']['Volt14'] | float)}}", 'name': 'Cell-14 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL15_VOLT: {'name': ['batterie', 'Cell', 'Volt15'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell15_', 'val_tpl': "{{ (value_json['Cell']['Volt15'] | float)}}", 'name': 'Cell-15 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_CELL16_VOLT: {'name': ['batterie', 'Cell', 'Volt16'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell16_', 'val_tpl': "{{ (value_json['Cell']['Volt16'] | float)}}", 'name': 'Cell-16 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_TEMP_1: {'name': ['batterie', 'Temp_1'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_1_', 'fmt': FMT_INT, 'name': 'Batterie Temp-1', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_TEMP_2: {'name': ['batterie', 'Temp_2'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_2_', 'fmt': FMT_INT, 'name': 'Batterie Temp-2', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_TEMP_3: {'name': ['batterie', 'Temp_3'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_3_', 'fmt': FMT_INT, 'name': 'Batterie Temp-3', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Output Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Output Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.BATT_OUT_STATUS: {'name': ['batterie', 'out', 'Out_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'out_status_', 'name': 'Output Status', 'val_tpl': __out_status_type_val_tpl, 'icon': POWER}}, # noqa: E501 - Register.BATT_TEMP_3: {'name': ['batterie', 'Controller_Temp'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_3_', 'fmt': FMT_INT, 'name': 'Ctrl Temperature'}}, # noqa: E501 + Register.BATT_TEMP_4: {'name': ['batterie', 'Controller_Temp'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_4_', 'fmt': FMT_INT, 'name': 'Ctrl Temperature'}}, # 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 Register.BATT_PV_PWR: {'name': ['batterie', 'PV_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'pv_power_', 'fmt': FMT_INT, 'name': 'PV Power'}}, # noqa: E501 Register.BATT_PWR: {'name': ['batterie', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_', 'fmt': FMT_INT, 'name': 'Batterie Power'}}, # noqa: E501 - Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Out Power'}}, # noqa: E501 + Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Output Power'}}, # noqa: E501 Register.TEST_VAL_0: {'name': ['input', 'Val_0'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_0_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.TEST_VAL_1: {'name': ['input', 'Val_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501 diff --git a/app/src/modbus.py b/app/src/modbus.py index b8244b5..c63b545 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -43,27 +43,38 @@ class Modbus(): 0x000a: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage 0x000b: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current 0x000c: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501 - 0x000d: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 + 0x000d: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x000e: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 0x000f: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 0x0010: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x0011: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x0012: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent - # 0x0013: {'reg': Register.BATT_46, 'fmt': '!h'}, # noqa: E501 - # 0x0014: {'reg': Register.BATT_48, 'fmt': '!h'}, # noqa: E501 - # 0x0015: {'reg': Register.BATT_4a, 'fmt': '!h'}, # noqa: E501 - # 0x0016: {'reg': Register.BATT_4c, 'fmt': '!h'}, # noqa: E501 - # 0x0017: {'reg': Register.BATT_4e, 'fmt': '!h'}, # noqa: E501 - # 0x0023: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501 - # 0x0024: {'reg': Register.BATT_68, 'fmt': '!h'}, # noqa: E501 - # 0x0025: {'reg': Register.BATT_6a, 'fmt': '!h'}, # noqa: E501 + 0x0013: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0014: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0015: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0016: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0017: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0018: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0019: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001a: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001b: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001c: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001d: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001e: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x001f: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0020: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0021: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0022: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501 + 0x0023: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501 + 0x0024: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501 + 0x0025: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501 0x0026: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 0x0027: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 - # 0x0028: {'reg': Register.BATT_70, 'fmt': '!h'}, # noqa: E501 - # 0x0029: {'reg': Register.BATT_72, 'fmt': '!h'}, # noqa: E501 - # 0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501 - # 0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 - # 0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 + 0x0028: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501 + 0x0029: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501 + 0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501 + 0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501 + 0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501 0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501 0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501 diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 4cfefcf..87271d8 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -157,9 +157,10 @@ def test_parse_4210_3026(batterie_data: bytes): "inverter": {"Serial_Number": "4101240701490314"}, "batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12}, "pv2": {"Voltage": 33.72, "Current": 0.0}, - "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, - "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, - "Temp_1": 15, "Temp_2": 15, + "Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0, + "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, + "Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21}, + "Temp_1": 15, "Temp_2": 15, "Temp_3": 15, "out": {"Voltage": 0.14, "Current": 0.0, "Out_Status": 0, "Power": 0.0}, "Controller_Temp": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513, "PV_Power": 37.9232, "Power": -1.0268000000000002}, @@ -177,9 +178,10 @@ def test_parse_4210_3026_incomplete(batterie_data2: bytes): "inverter": {"Serial_Number": "4101240701490314"}, "batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12}, "pv2": {"Voltage": 33.72, "Current": 0.0}, - "Reg_38": 0, "Reg_3a": 20.8, "Status_1": 0, "Status_2": 0, - "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, "Reg_66": 15, - "Temp_1": 15, "Temp_2": 15, + "Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0, + "Voltage": 51.34, "Current": -0.02, "SOC": 10.0, + "Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21}, + "Temp_1": 15, "Temp_2": 15, "Temp_3": 15, "out": {"Voltage": 0.14, "Current": None, "Out_Status": None, "Power": None}, "Controller_Temp": None, "Reg_74": None, "Reg_76": None, "Reg_78": None, "PV_Power": 37.9232, "Power": -1.0268000000000002}, @@ -357,7 +359,7 @@ def test_build_ha_conf5(): if id == 'out_power_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Out Power", "stat_t": "tsun/garagendach/batterie", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{ (value_json['out']['Power'] | int)}}", "unit_of_meas": "W", "dev": {"name": "Batterie", "sa": "Batterie", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["batterie_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Output Power", "stat_t": "tsun/garagendach/batterie", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{ (value_json['out']['Power'] | int)}}", "unit_of_meas": "W", "dev": {"name": "Batterie", "sa": "Batterie", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["batterie_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 elif id == 'daily_gen_123': assert False From 32d7711ab755fe3f30539be5310975c06b3804e7 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Wed, 26 Mar 2025 18:47:09 +0100 Subject: [PATCH 22/23] S allius/issue321 (#325) * support frame type no 8 for AT+ responses --- app/src/gen3plus/solarman_v5.py | 6 ++++-- app/tests/test_solarman.py | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 90ac323..9b4bee8 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -247,6 +247,7 @@ class SolarmanBase(Message): class SolarmanV5(SolarmanBase): AT_CMD = 1 MB_RTU_CMD = 2 + AT_CMD_RSP = 8 MB_CLIENT_DATA_UP = 30 '''Data up time in client mode''' HDR_FMT = ' Date: Wed, 26 Mar 2025 18:56:01 +0100 Subject: [PATCH 23/23] S allius/issue320 (#324) * at unit test for 0x4510 msg with frametype 5 --- app/tests/test_solarman.py | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 4a58f41..e9a28b1 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -633,6 +633,15 @@ def msg_modbus_cmd_fwd(): msg += b'\x15' return msg +@pytest.fixture +def msg_modbus_cmd_seq(): + msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x05\x26\x30\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x06\x01\x00\x01' + msg += b'\x03\xe8' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def msg_modbus_cmd_crc_err(): msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02' @@ -655,6 +664,19 @@ def msg_modbus_rsp(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def msg_modbus_interim_rsp(): # 0x0510 + msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01' + msg += total() + msg += hb() + msg += b'\x0a\xe2\xfa\x33\x01\x03\x28\x40\x10\x08\xd8' + msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02' + msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04' + msg += b'\x00\x01\x00\x00\x6c\x68' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def msg_modbus_rsp_inv_id2(): # 0x1510 msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01' @@ -1714,6 +1736,34 @@ def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd): assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() +def test_msg_modbus_req_seq(config_tsun_inv1, msg_modbus_cmd_seq): + _ = config_tsun_inv1 + m = MemoryStream(b'') + m.snr = get_sn_int() + m.sensor_list = 0x2b0 + m.state = State.up + c = m.createClientStream(msg_modbus_cmd_seq) + + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['AT_Command'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 0 + m.db.stat['proxy']['Invalid_Msg_Format'] = 0 + c.read() # read complete msg, and dispatch msg + assert not c.header_valid # must be invalid, since msg was handled and buffer flushed + assert c.msg_count == 1 + assert c.control == 0x4510 + assert str(c.seq) == '03:02' + assert c.header_len==11 + assert c.data_len==23 + assert c.ifc.fwd_fifo.get()==msg_modbus_cmd_seq + assert c.ifc.tx_fifo.get()==b'' + assert m.sent_pdu == b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['AT_Command'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err): _ = config_tsun_inv1 m = MemoryStream(b'')