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
-
+
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)