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 2da0b28..31ac310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ 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) +- 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 +- addon: add date and time to dev container version +- Update AddOn python3 to 3.12.9-r0 +- add initial DCU support +- 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 - addon: bump base image version to v17.1.0 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 diff --git a/app/.version b/app/.version index aac2dac..51de330 100644 --- a/app/.version +++ b/app/.version @@ -1 +1 @@ -0.12.1 \ No newline at end of file +0.13.0 \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile index 84ffe88..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 @@ -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/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/cnf/config.py b/app/src/cnf/config.py index a8f16db..88a0ab0 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 + '/' @@ -92,8 +93,13 @@ 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=0x2b0): Use(int), + Optional('sensor_list', default=0): Use(int), Optional('pv1'): { Optional('type'): Use(str), Optional('manufacturer'): Use(str), @@ -119,6 +125,38 @@ 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('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'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv2'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + } + } } }, ignore_extra_keys=True ) @@ -178,7 +216,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/config/default_config.toml b/app/src/cnf/default_config.toml similarity index 82% rename from app/config/default_config.toml rename to app/src/cnf/default_config.toml index 6c9ba77..ef7518f 100644 --- a/app/config/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/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 73fdae7..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 @@ -98,13 +99,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 +172,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 @@ -442,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(): @@ -479,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') @@ -554,6 +586,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 417487a..4cbe08c 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -1,9 +1,25 @@ from typing import Generator +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__ = () @@ -32,7 +48,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(): + sensor = self.get_db_value(Register.SENSOR_LIST) + if "3026" == sensor: + reg_map = RegisterMap.map_3026 + elif "02b0" == sensor: + reg_map = RegisterMap.map_02b0 + else: + reg_map = {} + items = reg_map.items() + if 'calc' in reg_map: + virt = reg_map['calc'].items() + else: + virt = {} + + 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 @@ -153,13 +248,17 @@ 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(): + 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 @@ -169,23 +268,36 @@ class InfosG3P(Infos): continue info_id = row['reg'] result = Fmt.get_value(buf, addr, row) + yield from self.__update_val(node_id, "GEN3PLUS", info_id, result) + yield from self.calc(sensor, node_id) - keys, level, unit, must_incr = self._key_obj(info_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 - 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 + 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, "CALC", 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): + 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..9b4bee8 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -2,8 +2,10 @@ import struct import logging import time 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 @@ -245,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 = ' {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 +437,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 @@ -459,12 +487,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.DEBUG) - - if 1 == (exp_cnt % 30): - # logging.info("Regular Modbus Status request") - self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG) + 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 \ @@ -482,7 +516,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 @@ -516,11 +550,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 +615,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() @@ -626,7 +660,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] @@ -644,29 +678,39 @@ class SolarmanV5(SolarmanBase): data = self.ifc.rx_peek()[self.header_len: self.header_len+self.data_len] ftype = data[0] - if ftype == self.AT_CMD: + if ftype == self.AT_CMD or \ + ftype == self.AT_CMD_RSP: if not self.forward_at_cmd_resp: data_json = data[14:].decode("utf-8") 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) 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): @@ -676,7 +720,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/infos.py b/app/src/infos.py index bcdb847..3bc6a4c 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -121,6 +121,94 @@ 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_TOTAL_GEN = 1005 + BATT_STATUS_1 = 1006 + BATT_STATUS_2 = 1007 + BATT_VOLT = 1010 + BATT_CUR = 1011 + BATT_SOC = 1012 + 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 + + 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 @@ -131,7 +219,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] @@ -230,6 +321,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' @@ -266,6 +358,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,11 +366,18 @@ 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 __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 = ''' {% if 'Max_Designed_Power' in value_json and @@ -428,7 +528,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 @@ -476,7 +576,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 @@ -518,15 +618,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 +636,93 @@ 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_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_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_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': '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 + 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/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/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/messages.py b/app/src/messages.py index eecfc80..e067df6 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 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) + 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..c63b545 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -37,6 +37,45 @@ 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_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_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_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 0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'}, 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/src/server.py b/app/src/server.py index e7c44af..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 @@ -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}") @@ -170,21 +172,21 @@ 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) # 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..b97a4e8 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, + } } } @@ -139,14 +152,49 @@ 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': { + '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} } } 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'}, + '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 +206,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 +220,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 +229,15 @@ 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'}}, + '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) except Exception: @@ -193,7 +247,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 +270,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 +286,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() @@ -240,6 +294,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 +318,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 +333,7 @@ def test_read_cnf1(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } @@ -279,7 +346,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() @@ -287,6 +354,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 +378,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 +393,7 @@ def test_read_cnf2(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } @@ -322,7 +402,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 +414,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() @@ -342,6 +422,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 +446,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 +461,7 @@ def test_read_cnf4(): 'type': 'RSM40-8-410M'}, 'pv4': {'manufacturer': 'Risen', 'type': 'RSM40-8-410M'}, - 'sensor_list': 688 + 'sensor_list': 0 } } } @@ -377,7 +470,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 +479,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..68c5b60 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() @@ -380,6 +380,29 @@ 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": [ + { + "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, @@ -401,7 +424,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/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 e0cac05..87271d8 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,11 +123,11 @@ 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() - 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({ @@ -123,14 +145,56 @@ 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, "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}, + }) + +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, "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}, + }) + 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 @@ -138,6 +202,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 +277,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,56 +349,89 @@ 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 comp == 'sensor' + 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 + 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==4 + 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] 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/app/tests/test_solarman.py b/app/tests/test_solarman.py index 6f11bec..e9a28b1 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 @@ -130,6 +131,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' @@ -560,6 +567,17 @@ def at_command_rsp_msg(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def at_command_interim_rsp_msg(): # 0x0510 + msg = b'\xa5\x25\x00\x10\x05\x03\x03' +get_sn() +b'\x08\x01' + msg += total() + msg += hb() + msg += b'\x00\x00\x00\x00+ok=10\x2c' + msg += b'start download\x0d\x0a' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def heartbeat_ind_msg(): # 0x4710 msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn() @@ -615,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' @@ -637,6 +664,32 @@ 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' + 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' @@ -672,9 +725,94 @@ 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' + 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}} + 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(): @@ -682,7 +820,29 @@ 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 = { + '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(): + 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}, 'client_mode': {'host': '192.168.1.1.'}, '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 +1123,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,)) @@ -1331,8 +1519,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 @@ -1348,8 +1536,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 @@ -1358,8 +1546,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 @@ -1371,8 +1560,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 @@ -1389,8 +1578,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() @@ -1406,8 +1595,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): @@ -1496,6 +1685,29 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg): assert m.db.stat['proxy']['Modbus_Command'] == 0 m.close() +def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg): + _ = config_tsun_inv1 + m = MemoryStream(at_command_interim_rsp_msg) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 0 + m.db.stat['proxy']['Invalid_Msg_Format'] = 0 + m.db.stat['proxy']['Unknown_Msg'] = 0 + m.forward_at_cmd_resp = True + 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.control == 0x0510 + assert str(m.seq) == '03:03' + assert m.header_len==11 + assert m.data_len==37 + assert m.ifc.fwd_fifo.get()==at_command_interim_rsp_msg + assert m.ifc.tx_fifo.get()==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + assert m.db.stat['proxy']['Unknown_Msg'] == 0 + m.close() + def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd): _ = config_tsun_inv1 m = MemoryStream(b'') @@ -1524,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'') @@ -1762,6 +2002,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 @@ -1794,6 +2107,79 @@ 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.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 + 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..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) @@ -2184,6 +2485,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,)) 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 diff --git a/ha_addons/Makefile b/ha_addons/Makefile index 5b17d60..15b8794 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -8,11 +8,12 @@ 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 @@ -20,12 +21,58 @@ DST_PROXY=$(DST)/home/proxy # base director of the add-on repro for installing the add-on git repros INST_BASE=../../ha-addons +# Template folder for build the config.yaml variants TEMPL=templates +# help variable STAGE determine the target to build +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/) + +build: local_add_on + +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 $(ADDON_PATH)/apparmor.txt + # collect source files 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) @@ -34,49 +81,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 - -# -# 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) @@ -92,40 +98,78 @@ $(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 $@ +$(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 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_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))) -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_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)) $(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 +$(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 $< $@ +$(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 $< $@ @@ -135,5 +179,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/Dockerfile b/ha_addons/ha_addon/Dockerfile index 925b342..6f35ceb 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} diff --git a/ha_addons/ha_addon/translations/de.yaml b/ha_addons/ha_addon/translations/de.yaml index cf48599..0455987 100755 --- a/ha_addons/ha_addon/translations/de.yaml +++ b/ha_addons/ha_addon/translations/de.yaml @@ -10,8 +10,21 @@ 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 - Wechselrichter mir `Y17`oder `47`! + Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` oder `R47` und die der GEN3PLUS + 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..82a25aa 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’! + 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 + 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/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 diff --git a/ha_addons/templates/config.jinja b/ha_addons/templates/config.jinja index 7ffbc1d..796d0c2 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}} @@ -24,17 +24,15 @@ 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 # and should become picked up by the proxy - current workaround as a transfer script -# TODO: check again for multi hierarchie parameters schema: inverters: - - serial: match(^(R17|Y17|Y47).{13}$) + - serial: match(^(R17|R47|Y17|Y47).{13}$) monitor_sn: int? node_id: str suggested_area: str @@ -42,11 +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? - #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 + 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? @@ -62,6 +58,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 +99,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.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 diff --git a/system_tests/test_tcp_socket_v2.py b/system_tests/test_tcp_socket_v2.py index b3521a8..94816fd 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\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' + 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)