From bfea38d9da3d836046c29de2ae7bb360f784a8ab Mon Sep 17 00:00:00 2001
From: Stefan Allius <122395479+s-allius@users.noreply.github.com>
Date: Mon, 16 Sep 2024 00:45:36 +0200
Subject: [PATCH] Dev 0.11 (#190)
* Code Cleanup (#158)
* print coverage report
* create sonar-project property file
* install all py dependencies in one step
* code cleanup
* reduce cognitive complexity
* do not build on *.yml changes
* optimise versionstring handling (#159)
- Reading the version string from the image updates
it even if the image is re-pulled without re-deployment
* fix linter warning
* exclude *.pyi filese
* ignore some rules for tests
* cleanup (#160)
* Sonar qube 3 (#163)
fix SonarQube warnings in modbus.py
* Sonar qube 3 (#164)
* fix SonarQube warnings
* Sonar qube 3 (#165)
* cleanup
* Add support for TSUN Titan inverter
Fixes #161
* fix SonarQube warnings
* fix error
* rename field "config"
* SonarQube reads flake8 output
* don't stop on flake8 errors
* flake8 scan only app/src for SonarQube
* update flake8 run
* ignore flake8 C901
* cleanup
* fix linter warnings
* ignore changed *.yml files
* read sensor list solarman data packets
* catch 'No route to' error and log only in debug mode
* fix unit tests
* add sensor_list configuration
* adapt unit tests
* fix SonarQube warnings
* Sonar qube 3 (#166)
* add unittests for mqtt.py
* add mock
* move test requirements into a file
* fix unit tests
* fix formating
* initial version
* fix SonarQube warning
* Sonar qube 4 (#169)
* add unit test for inverter.py
* fix SonarQube warning
* Sonar qube 5 (#170)
* fix SonarLints warnings
* use random IP adresses for unit tests
* Docker: The description ist missing (#171)
Fixes #167
* S allius/issue167 (#172)
* cleanup
* Sonar qube 6 (#174)
* test class ModbusConn
* Sonar qube 3 (#178)
* add more unit tests
* GEN3: don't crash on overwritten msg in the receive buffer
* improve test coverage und reduce test delays
* reduce cognitive complexity
* fix merge
* fix merge conflikt
* fix merge conflict
* S allius/issue182 (#183)
* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182
* add support for Controller serial no and MAC
* test hardening
* GEN3: add support for new messages of version 3 firmwares
* bump libraries to latest versions
- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5
* improve test coverage
* reduce cognective complexity
* fix target preview
* remove dubbled fixtures
* increase test coverage
* Update README.md (#185)
update badges
* S allius/issue186 (#187)
* Parse more values in Server Mode
Fixes #186
* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode
* fix unit test
* increase test coverage
* S allius/issue186 (#188)
* increase test coverage
* update changelog
* add dokumentation
* change default config
* Update README.md (#189)
Config file is now foldable
---
CHANGELOG.md | 3 +
README.md | 174 ++++++++++++++++----
app/config/default_config.toml | 181 ++++++++++++++++----
app/docker-bake.hcl | 2 +-
app/requirements.txt | 4 +-
app/src/gen3/connection_g3.py | 9 +-
app/src/gen3/infos_g3.py | 168 ++++++++++---------
app/src/gen3/inverter_g3.py | 15 +-
app/src/gen3/talent.py | 77 +++++++++
app/src/gen3plus/infos_g3p.py | 3 +-
app/src/gen3plus/solarman_v5.py | 4 +-
app/src/infos.py | 20 ++-
app/tests/test_config.py | 156 +++++++++++++++++-
app/tests/test_connection_g3.py | 84 ++++++++++
app/tests/test_connection_g3p.py | 89 ++++++++++
app/tests/test_infos_g3.py | 74 ++++++---
app/tests/test_infos_g3p.py | 4 +-
app/tests/test_inverter_g3.py | 235 ++++++++++++++++++++++++++
app/tests/test_inverter_g3p.py | 236 +++++++++++++++++++++++++++
app/tests/test_modbus_tcp.py | 84 +++++++++-
app/tests/test_solarman.py | 77 +++++++++
app/tests/test_talent.py | 272 ++++++++++++++++++++++++++++++-
22 files changed, 1780 insertions(+), 191 deletions(-)
create mode 100644 app/tests/test_connection_g3.py
create mode 100644 app/tests/test_connection_g3p.py
create mode 100644 app/tests/test_inverter_g3.py
create mode 100644 app/tests/test_inverter_g3p.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3db2d75..eacd121 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]
+- Parse more values in Server Mode [#186](https://github.com/s-allius/tsun-gen3-proxy/issues/186)
+- GEN3: add support for new messages of version 3 firmwares [#182](https://github.com/s-allius/tsun-gen3-proxy/issues/182)
+- add support for controller MAC and serial number
- GEN3: don't crash on overwritten msg in the receive buffer
- Reading the version string from the image updates it even if the image is re-pulled without re-deployment
diff --git a/README.md b/README.md
index 16c90f5..7590b4a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-
+
@@ -121,26 +121,63 @@ The proxy can be configured via the file 'config.toml'. When the proxy is starte
The configration uses the TOML format, which aims to be easy to read due to obvious semantics.
You find more details here:
+
+Here is an example of a config.toml file
+
```toml
-# configuration for tsun cloud for 'GEN3' inverters
-tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
-tsun.host = 'logger.talent-monitoring.com'
-tsun.port = 5005
-
-# configuration for solarman cloud for 'GEN3 PLUS' inverters
-solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
-solarman.host = 'iot.talent-monitoring.com'
-solarman.port = 10000
+##########################################################################################
+###
+### T S U N - G E N 3 - P R O X Y
+###
+### from Stefan Allius
+###
+##########################################################################################
+###
+### The readme will give you an overview of the project:
+### https://s-allius.github.io/tsun-gen3-proxy/
+###
+### The proxy supports different operation modes. Select the proper mode
+### which depends on your inverter type and you inverter firmware.
+### Please read:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
+###
+### Here you will find a description of all configuration options:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
+###
+### The configration uses the TOML format, which aims to be easy to read due to
+### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
+###
+##########################################################################################
-# mqtt broker configuration
+##########################################################################################
+##
+## MQTT broker configuration
+##
+## In this block, you must configure the connection to your MQTT broker and specify the
+## required credentials. As the proxy does not currently support an encrypted connection
+## to the MQTT broker, it is strongly recommended that you do not use a public broker.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
+##
+
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883
mqtt.user = ''
mqtt.passwd = ''
-# home-assistant
+##########################################################################################
+##
+## HOME ASSISTANT
+##
+## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
+## values match the HA default configuration. If you need to change these or want to use
+## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
+##
+
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
@@ -148,40 +185,115 @@ ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_i
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
-# microinverters
-inverters.allow_all = false # True: allow inverters, even if we have no inverter mapping
+##########################################################################################
+##
+## GEN3 Proxy Mode Configuration
+##
+## In this block, you can configure an optional connection to the TSUN cloud for GEN3
+## inverters. This connection is only required if you want send data to the TSUN cloud
+## to use the TSUN APPs or receive firmware updates.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
+##
-# inverter mapping, maps a `serial_no* to a `node_id` and defines an optional `suggested_area` for `home-assistant`
-#
-# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
+tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
+tsun.host = 'logger.talent-monitoring.com'
+tsun.port = 5005
+
+
+##########################################################################################
+##
+## GEN3PLUS Proxy Mode Configuration
+##
+## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
+## inverters. This connection is only required if you want send data to the TSUN cloud
+## to use the TSUN APPs or receive firmware updates.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
+##
+solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
+solarman.host = 'iot.talent-monitoring.com'
+solarman.port = 10000
+
+
+##########################################################################################
+###
+### Inverter Definitions
+###
+### The proxy supports the simultaneous operation of several inverters, even of different
+### types. A configuration block must be defined for each inverter, in which all necessary
+### parameters must be specified. These depend on the operation mode used and also differ
+### slightly depending on the inverter type.
+###
+### In addition, the PV modules can be defined at the individual inputs for documentation
+### purposes, whereby these are displayed in Home Assistant.
+###
+### The proxy only accepts connections from known inverters. This can be switched off for
+### test purposes and unknown serial numbers are also accepted.
+###
+
+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
+## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
+## in the configuration block
+##
+## The serial numbers of all GEN3 inverters start with `R17`!
+##
[inverters."R17xxxxxxxxxxxx1"]
-node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
-suggested_area = 'roof' # Optional, suggested installation area for home-assistant
-modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
+node_id = 'inv_1' # MQTT replacement for inverters serial number
+suggested_area = 'roof' # suggested installation place for home-assistant
+modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
-[inverters."R17xxxxxxxxxxxx2"]
-node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
-suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
-modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
-pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
-pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
+
+##########################################################################################
+##
+## 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
+## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
+## can be set in the configuration block
+##
+## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
+## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
+## with the inverter.
+##
[inverters."Y17xxxxxxxxxxxx1"] # This block is also for inverters with a Y47 serial no
-monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
-node_id = 'inv_3' # MQTT replacement for inverters serial number
-suggested_area = 'garage' # suggested installation place for home-assistant
-modbus_polling = false # Enable optional MODBUS polling for GEN3PLUS inverter
+monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
+node_id = 'inv_2' # MQTT replacement for inverters serial number
+suggested_area = 'garage' # 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}
+
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv4 = {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
+### this connection or parameters (e.g. network credentials) can be queried. Filters can
+### then be configured for the AT+ commands from the TSUN Cloud so that only certain
+### accesses are permitted.
+###
+### An overview of all known AT+ commands can be found here:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
+###
+
[gen3plus.at_acl]
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access
tsun.block = []
@@ -190,6 +302,8 @@ mqtt.block = []
```
+
+
## Inverter Configuration
GEN3PLUS inverters offer a web interface that can be used to configure the inverter. This is very practical for sending the data directly to the proxy. On the one hand, the inverter broadcasts its own SSID on 2.4GHz. This can be recognized because it is broadcast with `AP_`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the inverter.
diff --git a/app/config/default_config.toml b/app/config/default_config.toml
index 744835a..57b2baf 100644
--- a/app/config/default_config.toml
+++ b/app/config/default_config.toml
@@ -1,64 +1,177 @@
-# configuration to reach tsun cloud
-tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
-tsun.host = 'logger.talent-monitoring.com'
-tsun.port = 5005
+##########################################################################################
+###
+### T S U N - G E N 3 - P R O X Y
+###
+### from Stefan Allius
+###
+##########################################################################################
+###
+### The readme will give you an overview of the project:
+### https://s-allius.github.io/tsun-gen3-proxy/
+###
+### The proxy supports different operation modes. Select the proper mode
+### which depends on your inverter type and you inverter firmware.
+### Please read:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
+###
+### Here you will find a description of all configuration options:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
+###
+### The configration uses the TOML format, which aims to be easy to read due to
+### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
+###
+##########################################################################################
-# configuration to reach the new tsun cloud for G3 Plus inverters
-solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
-solarman.host = 'iot.talent-monitoring.com'
-solarman.port = 10000
-# mqtt broker configuration
+##########################################################################################
+##
+## MQTT broker configuration
+##
+## In this block, you must configure the connection to your MQTT broker and specify the
+## required credentials. As the proxy does not currently support an encrypted connection
+## to the MQTT broker, it is strongly recommended that you do not use a public broker.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
+##
+
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883
mqtt.user = ''
mqtt.passwd = ''
-# home-assistant
+##########################################################################################
+##
+## HOME ASSISTANT
+##
+## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
+## values match the HA default configuration. If you need to change these or want to use
+## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
+##
+
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
-# microinverters
-inverters.allow_all = true # allow inverters, even if we have no inverter mapping
-# inverter mapping, maps a `serial_no* to a `mqtt_id` and defines an optional `suggested_place` for `home-assistant`
-#
-# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
+##########################################################################################
+##
+## GEN3 Proxy Mode Configuration
+##
+## In this block, you can configure an optional connection to the TSUN cloud for GEN3
+## inverters. This connection is only required if you want send data to the TSUN cloud
+## to use the TSUN APPs or receive firmware updates.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
+##
+
+tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
+tsun.host = 'logger.talent-monitoring.com'
+tsun.port = 5005
+
+
+##########################################################################################
+##
+## GEN3PLUS Proxy Mode Configuration
+##
+## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
+## inverters. This connection is only required if you want send data to the TSUN cloud
+## to use the TSUN APPs or receive firmware updates.
+##
+## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
+##
+
+solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
+solarman.host = 'iot.talent-monitoring.com'
+solarman.port = 10000
+
+
+##########################################################################################
+###
+### Inverter Definitions
+###
+### The proxy supports the simultaneous operation of several inverters, even of different
+### types. A configuration block must be defined for each inverter, in which all necessary
+### parameters must be specified. These depend on the operation mode used and also differ
+### slightly depending on the inverter type.
+###
+### In addition, the PV modules can be defined at the individual inputs for documentation
+### purposes, whereby these are displayed in Home Assistant.
+###
+### The proxy only accepts connections from known inverters. This can be switched off for
+### test purposes and unknown serial numbers are also accepted.
+###
+
+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
+## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
+## in the configuration block
+##
+## The serial numbers of all GEN3 inverters start with `R17`!
+##
+
[inverters."R170000000000001"]
-#node_id = '' # Optional, MQTT replacement for inverters serial number
-#suggested_area = '' # Optional, suggested installation area for home-assistant
-modbus_polling = false # Disable optional MODBUS polling
-#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
-#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
+node_id = '' # MQTT replacement for inverters serial number
+suggested_area = '' # suggested installation area for home-assistant
+modbus_polling = false # Disable optional MODBUS polling
+pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
+pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
-#[inverters."R17xxxxxxxxxxxx2"]
-#node_id = '' # Optional, MQTT replacement for inverters serial number
-#suggested_area = '' # Optional, suggested installation area for home-assistant
-#modbus_polling = false # Disable optional MODBUS polling
-#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
-#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
+
+##########################################################################################
+##
+## 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
+## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
+## can be set in the configuration block
+##
+## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
+## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
+## with the inverter.
+##
[inverters."Y170000000000001"]
-monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
-#node_id = '' # Optional, MQTT replacement for inverters serial number
-#suggested_area = '' # Optional, suggested installation place for home-assistant
-modbus_polling = true # Enable optional MODBUS polling
+monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
+node_id = '' # MQTT replacement for inverters 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}
-#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
-#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
-#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
-#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
+pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
+pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
+pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
+pv4 = {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
+### this connection or parameters (e.g. network credentials) can be queried. Filters can
+### then be configured for the AT+ commands from the TSUN Cloud so that only certain
+### accesses are permitted.
+###
+### An overview of all known AT+ commands can be found here:
+### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
+###
[gen3plus.at_acl]
+# filter for received commands from the internet
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
tsun.block = []
+# filter for received commands from the MQTT broker
mqtt.allow = ['AT+']
mqtt.block = []
diff --git a/app/docker-bake.hcl b/app/docker-bake.hcl
index b6d58d9..56317d2 100644
--- a/app/docker-bake.hcl
+++ b/app/docker-bake.hcl
@@ -78,7 +78,7 @@ target "dev" {
target "preview" {
inherits = ["_common", "_prod"]
- tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"]
+ tags = ["${IMAGE}:preview", "${IMAGE}:${VERSION}"]
}
target "rc" {
diff --git a/app/requirements.txt b/app/requirements.txt
index 2aa2067..6546f18 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -1,4 +1,4 @@
- aiomqtt==2.2.0
+ aiomqtt==2.3.0
schema==0.7.7
aiocron==1.8
- aiohttp==3.10.2
\ No newline at end of file
+ aiohttp==3.10.5
\ No newline at end of file
diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py
index 5aea231..b7e246b 100644
--- a/app/src/gen3/connection_g3.py
+++ b/app/src/gen3/connection_g3.py
@@ -1,7 +1,12 @@
import logging
from asyncio import StreamReader, StreamWriter
-from async_stream import AsyncStream
-from gen3.talent import Talent
+
+if __name__ == "app.src.gen3.connection_g3":
+ from app.src.async_stream import AsyncStream
+ from app.src.gen3.talent import Talent
+else: # pragma: no cover
+ from async_stream import AsyncStream
+ from gen3.talent import Talent
logger = logging.getLogger('conn')
diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py
index 3594f9d..c39bed9 100644
--- a/app/src/gen3/infos_g3.py
+++ b/app/src/gen3/infos_g3.py
@@ -11,81 +11,82 @@ else: # pragma: no cover
class RegisterMap:
map = {
- 0x00092ba8: Register.COLLECTOR_FW_VERSION,
- 0x000927c0: Register.CHIP_TYPE,
- 0x00092f90: Register.CHIP_MODEL,
- 0x00095a88: Register.TRACE_URL,
- 0x00095aec: Register.LOGGER_URL,
- 0x0000000a: Register.PRODUCT_NAME,
- 0x00000014: Register.MANUFACTURER,
- 0x0000001e: Register.VERSION,
- 0x00000028: Register.SERIAL_NUMBER,
- 0x00000032: Register.EQUIPMENT_MODEL,
- 0x00013880: Register.NO_INPUTS,
- 0xffffff00: Register.INVERTER_CNT,
- 0xffffff01: Register.UNKNOWN_SNR,
- 0xffffff02: Register.UNKNOWN_MSG,
- 0xffffff03: Register.INVALID_DATA_TYPE,
- 0xffffff04: Register.INTERNAL_ERROR,
- 0xffffff05: Register.UNKNOWN_CTRL,
- 0xffffff06: Register.OTA_START_MSG,
- 0xffffff07: Register.SW_EXCEPTION,
- 0xffffff08: Register.MAX_DESIGNED_POWER,
- 0xffffff09: Register.OUTPUT_COEFFICIENT,
- 0xffffff0a: Register.INVERTER_STATUS,
- 0xffffff0b: Register.POLLING_INTERVAL,
- 0xfffffffe: Register.TEST_REG1,
- 0xffffffff: Register.TEST_REG2,
- 0x00000640: Register.OUTPUT_POWER,
- 0x000005dc: Register.RATED_POWER,
- 0x00000514: Register.INVERTER_TEMP,
- 0x000006a4: Register.PV1_VOLTAGE,
- 0x00000708: Register.PV1_CURRENT,
- 0x0000076c: Register.PV1_POWER,
- 0x000007d0: Register.PV2_VOLTAGE,
- 0x00000834: Register.PV2_CURRENT,
- 0x00000898: Register.PV2_POWER,
- 0x000008fc: Register.PV3_VOLTAGE,
- 0x00000960: Register.PV3_CURRENT,
- 0x000009c4: Register.PV3_POWER,
- 0x00000a28: Register.PV4_VOLTAGE,
- 0x00000a8c: Register.PV4_CURRENT,
- 0x00000af0: Register.PV4_POWER,
- 0x00000c1c: Register.PV1_DAILY_GENERATION,
- 0x00000c80: Register.PV1_TOTAL_GENERATION,
- 0x00000ce4: Register.PV2_DAILY_GENERATION,
- 0x00000d48: Register.PV2_TOTAL_GENERATION,
- 0x00000dac: Register.PV3_DAILY_GENERATION,
- 0x00000e10: Register.PV3_TOTAL_GENERATION,
- 0x00000e74: Register.PV4_DAILY_GENERATION,
- 0x00000ed8: Register.PV4_TOTAL_GENERATION,
- 0x00000b54: Register.DAILY_GENERATION,
- 0x00000bb8: Register.TOTAL_GENERATION,
- 0x000003e8: Register.GRID_VOLTAGE,
- 0x0000044c: Register.GRID_CURRENT,
- 0x000004b0: Register.GRID_FREQUENCY,
- 0x000cfc38: Register.CONNECT_COUNT,
- 0x000c3500: Register.SIGNAL_STRENGTH,
- 0x000c96a8: Register.POWER_ON_TIME,
- 0x000d0020: Register.COLLECT_INTERVAL,
- 0x000cf850: Register.DATA_UP_INTERVAL,
- 0x000c7f38: Register.COMMUNICATION_TYPE,
- 0x00000191: Register.EVENT_401,
- 0x00000192: Register.EVENT_402,
- 0x00000193: Register.EVENT_403,
- 0x00000194: Register.EVENT_404,
- 0x00000195: Register.EVENT_405,
- 0x00000196: Register.EVENT_406,
- 0x00000197: Register.EVENT_407,
- 0x00000198: Register.EVENT_408,
- 0x00000199: Register.EVENT_409,
- 0x0000019a: Register.EVENT_410,
- 0x0000019b: Register.EVENT_411,
- 0x0000019c: Register.EVENT_412,
- 0x0000019d: Register.EVENT_413,
- 0x0000019e: Register.EVENT_414,
- 0x0000019f: Register.EVENT_415,
- 0x000001a0: Register.EVENT_416,
+ 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},
+ 0xffffff03: {'reg': Register.INVALID_DATA_TYPE},
+ 0xffffff04: {'reg': Register.INTERNAL_ERROR},
+ 0xffffff05: {'reg': Register.UNKNOWN_CTRL},
+ 0xffffff06: {'reg': Register.OTA_START_MSG},
+ 0xffffff07: {'reg': Register.SW_EXCEPTION},
+ 0xffffff08: {'reg': Register.POLLING_INTERVAL},
+ 0xfffffffe: {'reg': Register.TEST_REG1},
+ 0xffffffff: {'reg': Register.TEST_REG2},
+ 0x00000640: {'reg': Register.OUTPUT_POWER},
+ 0x000005dc: {'reg': Register.RATED_POWER},
+ 0x00000514: {'reg': Register.INVERTER_TEMP},
+ 0x000006a4: {'reg': Register.PV1_VOLTAGE},
+ 0x00000708: {'reg': Register.PV1_CURRENT},
+ 0x0000076c: {'reg': Register.PV1_POWER},
+ 0x000007d0: {'reg': Register.PV2_VOLTAGE},
+ 0x00000834: {'reg': Register.PV2_CURRENT},
+ 0x00000898: {'reg': Register.PV2_POWER},
+ 0x000008fc: {'reg': Register.PV3_VOLTAGE},
+ 0x00000960: {'reg': Register.PV3_CURRENT},
+ 0x000009c4: {'reg': Register.PV3_POWER},
+ 0x00000a28: {'reg': Register.PV4_VOLTAGE},
+ 0x00000a8c: {'reg': Register.PV4_CURRENT},
+ 0x00000af0: {'reg': Register.PV4_POWER},
+ 0x00000c1c: {'reg': Register.PV1_DAILY_GENERATION},
+ 0x00000c80: {'reg': Register.PV1_TOTAL_GENERATION},
+ 0x00000ce4: {'reg': Register.PV2_DAILY_GENERATION},
+ 0x00000d48: {'reg': Register.PV2_TOTAL_GENERATION},
+ 0x00000dac: {'reg': Register.PV3_DAILY_GENERATION},
+ 0x00000e10: {'reg': Register.PV3_TOTAL_GENERATION},
+ 0x00000e74: {'reg': Register.PV4_DAILY_GENERATION},
+ 0x00000ed8: {'reg': Register.PV4_TOTAL_GENERATION},
+ 0x00000b54: {'reg': Register.DAILY_GENERATION},
+ 0x00000bb8: {'reg': Register.TOTAL_GENERATION},
+ 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},
+ 0x00000191: {'reg': Register.EVENT_401},
+ 0x00000192: {'reg': Register.EVENT_402},
+ 0x00000193: {'reg': Register.EVENT_403},
+ 0x00000194: {'reg': Register.EVENT_404},
+ 0x00000195: {'reg': Register.EVENT_405},
+ 0x00000196: {'reg': Register.EVENT_406},
+ 0x00000197: {'reg': Register.EVENT_407},
+ 0x00000198: {'reg': Register.EVENT_408},
+ 0x00000199: {'reg': Register.EVENT_409},
+ 0x0000019a: {'reg': Register.EVENT_410},
+ 0x0000019b: {'reg': Register.EVENT_411},
+ 0x0000019c: {'reg': Register.EVENT_412},
+ 0x0000019d: {'reg': Register.EVENT_413},
+ 0x0000019e: {'reg': Register.EVENT_414},
+ 0x0000019f: {'reg': Register.EVENT_415},
+ 0x000001a0: {'reg': Register.EVENT_416},
+ 0x00000064: {'reg': Register.INVERTER_STATUS},
+ 0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
+ 0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
}
@@ -103,7 +104,8 @@ 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 reg in RegisterMap.map.values():
+ for row in RegisterMap.map.values():
+ reg = row['reg']
res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
if res:
yield res
@@ -122,9 +124,11 @@ class InfosG3(Infos):
result = struct.unpack_from('!lB', buf, ind)
addr = result[0]
if addr not in RegisterMap.map:
+ row = None
info_id = -1
else:
- info_id = RegisterMap.map[addr]
+ row = RegisterMap.map[addr]
+ info_id = row['reg']
data_type = result[1]
ind += 5
@@ -170,9 +174,19 @@ class InfosG3(Infos):
" not supported")
return
+ result = self.__modify_val(row, result)
+
yield from self.__store_result(addr, result, info_id, node_id)
i += 1
+ def __modify_val(self, row, result):
+ if row:
+ if 'eval' in row:
+ result = eval(row['eval'])
+ if 'ratio' in row:
+ result = round(result * row['ratio'], 2)
+ return result
+
def __store_result(self, addr, result, info_id, node_id):
keys, level, unit, must_incr = self._key_obj(info_id)
if keys:
diff --git a/app/src/gen3/inverter_g3.py b/app/src/gen3/inverter_g3.py
index 9e2a40a..c365286 100644
--- a/app/src/gen3/inverter_g3.py
+++ b/app/src/gen3/inverter_g3.py
@@ -3,11 +3,18 @@ import traceback
import json
import asyncio
from asyncio import StreamReader, StreamWriter
-from config import Config
-from inverter import Inverter
-from gen3.connection_g3 import ConnectionG3
from aiomqtt import MqttCodeError
-from infos import Infos
+
+if __name__ == "app.src.gen3.inverter_g3":
+ from app.src.config import Config
+ from app.src.inverter import Inverter
+ from app.src.gen3.connection_g3 import ConnectionG3
+ from app.src.infos import Infos
+else: # pragma: no cover
+ from config import Config
+ from inverter import Inverter
+ from gen3.connection_g3 import ConnectionG3
+ from infos import Infos
logger_mqtt = logging.getLogger('mqtt')
diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py
index 015efd9..c225691 100644
--- a/app/src/gen3/talent.py
+++ b/app/src/gen3/talent.py
@@ -56,20 +56,24 @@ class Talent(Message):
0x00: self.msg_contact_info,
0x13: self.msg_ota_update,
0x22: self.msg_get_time,
+ 0x99: self.msg_act_time,
0x71: self.msg_collector_data,
# 0x76:
0x77: self.msg_modbus,
# 0x78:
+ 0x87: self.msg_modbus2,
0x04: self.msg_inverter_data,
}
self.log_lvl = {
0x00: logging.INFO,
0x13: logging.INFO,
0x22: logging.INFO,
+ 0x99: logging.INFO,
0x71: logging.INFO,
# 0x76:
0x77: self.get_modbus_log_lvl,
# 0x78:
+ 0x87: self.get_modbus_log_lvl,
0x04: logging.INFO,
}
self.modbus_elms = 0 # for unit tests
@@ -127,6 +131,7 @@ class Talent(Message):
logger.debug(f'SerialNo {serial_no} not known but accepted!')
self.unique_id = serial_no
+ self.db.set_db_def_value(Register.COLLECTOR_SNR, serial_no)
def read(self) -> float:
'''process all received messages in the _recv_buffer'''
@@ -170,6 +175,25 @@ class Talent(Message):
logger.info(self.__flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
+ def forward_snd(self) -> None:
+ '''add the actual receive msg to the forwarding queue'''
+ tsun = Config.get('tsun')
+ if tsun['enabled']:
+ _len = len(self._send_buffer) - self.send_msg_ofs
+ struct.pack_into('!l', self._send_buffer, self.send_msg_ofs,
+ _len-4)
+
+ buffer = self._send_buffer[self.send_msg_ofs:]
+ buflen = _len
+ self._forward_buffer += buffer[:buflen]
+ hex_dump_memory(logging.INFO, 'Store for forwarding:',
+ buffer, buflen)
+
+ fnc = self.switch.get(self.msg_id, self.msg_unknown)
+ logger.info(self.__flow_str(self.server_side, 'forwrd') +
+ f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
+ self._send_buffer = self._send_buffer[:self.send_msg_ofs]
+
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
@@ -400,6 +424,8 @@ class Talent(Message):
result = struct.unpack_from('!q', self._recv_buffer,
self.header_len)
self.ts_offset = result[0]-ts
+ if self.remote_stream:
+ self.remote_stream.ts_offset = self.ts_offset
logger.debug(f'tsun-time: {int(result[0]):08x}'
f' proxy-time: {ts:08x}'
f' offset: {self.ts_offset}')
@@ -410,6 +436,41 @@ class Talent(Message):
self.forward()
+ def msg_act_time(self):
+ if self.ctrl.is_ind():
+ if self.data_len == 9:
+ self.state = State.up # allow MODBUS cmds
+ if (self.modbus_polling):
+ self.mb_timer.start(self.mb_first_timeout)
+ self.db.set_db_def_value(Register.POLLING_INTERVAL,
+ self.mb_timeout)
+ self.__build_header(0x99)
+ self._send_buffer += b'\x02'
+ self.__finish_send_msg()
+
+ result = struct.unpack_from('!Bq', self._recv_buffer,
+ self.header_len)
+ resp_code = result[0]
+ ts = result[1]+self.ts_offset
+ logger.debug(f'inv-time: {int(result[1]):08x}'
+ f' tsun-time: {ts:08x}'
+ f' offset: {self.ts_offset}')
+ self.__build_header(0x91)
+ self._send_buffer += struct.pack('!Bq', resp_code, ts)
+ self.forward_snd()
+ return
+ elif self.ctrl.is_resp():
+ result = struct.unpack_from('!B', self._recv_buffer,
+ self.header_len)
+ resp_code = result[0]
+ logging.debug(f'TimeActRespCode: {resp_code}')
+ return
+ else:
+ logger.warning(self.TXT_UNKNOWN_CTRL)
+ self.inc_counter('Unknown_Ctrl')
+
+ self.forward()
+
def parse_msg_header(self):
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
@@ -492,6 +553,15 @@ class Talent(Message):
modbus_len = result[1]
return msg_hdr_len, modbus_len
+ def parse_modbus_header2(self):
+
+ msg_hdr_len = 6
+
+ result = struct.unpack_from('!lBBB', self._recv_buffer,
+ self.header_len)
+ modbus_len = result[2]
+ return msg_hdr_len, modbus_len
+
def get_modbus_log_lvl(self) -> int:
if self.ctrl.is_req():
return logging.INFO
@@ -501,6 +571,13 @@ class Talent(Message):
def msg_modbus(self):
hdr_len, _ = self.parse_modbus_header()
+ self.__msg_modbus(hdr_len)
+
+ def msg_modbus2(self):
+ hdr_len, _ = self.parse_modbus_header2()
+ self.__msg_modbus(hdr_len)
+
+ def __msg_modbus(self, hdr_len):
data = self._recv_buffer[self.header_len:
self.header_len+self.data_len]
diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py
index 2d6a2fc..135aa3d 100644
--- a/app/src/gen3plus/infos_g3p.py
+++ b/app/src/gen3plus/infos_g3p.py
@@ -19,6 +19,7 @@ class RegisterMap:
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': ' {inv}')
if (type(inv) is dict and 'monitor_sn' in inv
and inv['monitor_sn'] == snr):
self.__set_config_parms(inv)
self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
+
+ self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
break
else:
self.node_id = ''
diff --git a/app/src/infos.py b/app/src/infos.py
index 2192b5b..e06348e 100644
--- a/app/src/infos.py
+++ b/app/src/infos.py
@@ -16,6 +16,8 @@ class Register(Enum):
CHIP_MODEL = 3
TRACE_URL = 4
LOGGER_URL = 5
+ MAC_ADDR = 6
+ COLLECTOR_SNR = 7
PRODUCT_NAME = 20
MANUFACTURER = 21
VERSION = 22
@@ -188,8 +190,8 @@ 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}, # noqa: E501
- 'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION}, # 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
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'mdl': Register.PV3_MODEL, 'mf': Register.PV3_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501
@@ -222,6 +224,9 @@ class Infos:
Register.CHIP_MODEL: {'name': ['collector', 'Chip_Model'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.TRACE_URL: {'name': ['collector', 'Trace_URL'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
+ Register.MAC_ADDR: {'name': ['collector', 'MAC-Addr'], 'singleton': False, 'level': logging.INFO, 'unit': ''}, # noqa: E501
+ Register.COLLECTOR_SNR: {'name': ['collector', 'Serial_Number'], 'singleton': False, 'level': logging.INFO, 'unit': ''}, # noqa: E501
+
# inverter values used for device registration:
Register.PRODUCT_NAME: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -507,7 +512,7 @@ class Infos:
dev['name'] = device['name']+' - '+sug_area
dev['sa'] = device['name']+' - '+sug_area
self.__add_via_dev(dev, device, key, snr)
- for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
+ for key in ('mdl', 'mf', 'sw', 'hw', 'sn'): # add optional
# values fpr 'modell', 'manufacturer', 'sw version' and
# 'hw version'
if key in device:
@@ -518,8 +523,17 @@ class Infos:
dev['ids'] = [f"{ha['dev']}"]
else:
dev['ids'] = [f"{ha['dev']}_{snr}"]
+ self.__add_connection(dev, device)
return dev
+ def __add_connection(self, dev, device):
+ if 'mac' in device:
+ mac_str = self.dev_value(device['mac'])
+ if mac_str is not None:
+ if 12 == len(mac_str):
+ mac_str = ':'.join(mac_str[i:i+2] for i in range(0, 12, 2))
+ dev['cns'] = [["mac", f"{mac_str}"]]
+
def __add_via_dev(self, dev, device, key, snr):
if 'via' in device: # add the link to the parent device
via = device['via']
diff --git a/app/tests/test_config.py b/app/tests/test_config.py
index 9782567..5ceb1b3 100644
--- a/app/tests/test_config.py
+++ b/app/tests/test_config.py
@@ -30,7 +30,33 @@ def test_default_config():
validated = Config.conf_schema.validate(cnf)
except Exception:
assert False
- assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': False, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 688}}}
+ assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
+ 'inverters': {
+ 'allow_all': False,
+ 'R170000000000001': {
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'modbus_polling': False,
+ 'monitor_sn': 0,
+ 'suggested_area': '',
+ 'sensor_list': 688},
+ 'Y170000000000001': {
+ 'modbus_polling': True,
+ 'monitor_sn': 2000000000,
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv3': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv4': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'suggested_area': '',
+ 'sensor_list': 688}}}
def test_full_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -71,7 +97,37 @@ def test_read_empty():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.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'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
+ 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'},
+ 'inverters': {
+ 'allow_all': False,
+ 'R170000000000001': {
+ 'suggested_area': '',
+ 'modbus_polling': False,
+ 'monitor_sn': 0,
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'sensor_list': 688
+ },
+ 'Y170000000000001': {
+ 'modbus_polling': True,
+ 'monitor_sn': 2000000000,
+ 'suggested_area': '',
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv3': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv4': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'sensor_list': 688
+ }
+ }
+ }
defcnf = TstConfig.def_config.get('solarman')
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
@@ -93,7 +149,37 @@ def test_read_cnf1():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.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'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
+ 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'},
+ 'inverters': {
+ 'allow_all': False,
+ 'R170000000000001': {
+ 'suggested_area': '',
+ 'modbus_polling': False,
+ 'monitor_sn': 0,
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'sensor_list': 688
+ },
+ 'Y170000000000001': {
+ 'modbus_polling': True,
+ 'monitor_sn': 2000000000,
+ 'suggested_area': '',
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv3': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv4': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'sensor_list': 688
+ }
+ }
+ }
cnf = TstConfig.get('solarman')
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
defcnf = TstConfig.def_config.get('solarman')
@@ -106,7 +192,37 @@ def test_read_cnf2():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.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'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
+ 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'},
+ 'inverters': {
+ 'allow_all': False,
+ 'R170000000000001': {
+ 'suggested_area': '',
+ 'modbus_polling': False,
+ 'monitor_sn': 0,
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'sensor_list': 688
+ },
+ 'Y170000000000001': {
+ 'modbus_polling': True,
+ 'monitor_sn': 2000000000,
+ 'suggested_area': '',
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv3': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv4': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'sensor_list': 688
+ }
+ }
+ }
assert True == TstConfig.is_default('solarman')
def test_read_cnf3():
@@ -123,7 +239,37 @@ def test_read_cnf4():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.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'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
+ 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'},
+ 'inverters': {
+ 'allow_all': False,
+ 'R170000000000001': {
+ 'suggested_area': '',
+ 'modbus_polling': False,
+ 'monitor_sn': 0,
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-395M'},
+ 'sensor_list': 688
+ },
+ 'Y170000000000001': {
+ 'modbus_polling': True,
+ 'monitor_sn': 2000000000,
+ 'suggested_area': '',
+ 'node_id': '',
+ 'pv1': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv2': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv3': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'pv4': {'manufacturer': 'Risen',
+ 'type': 'RSM40-8-410M'},
+ 'sensor_list': 688
+ }
+ }
+ }
assert False == TstConfig.is_default('solarman')
def test_read_cnf5():
diff --git a/app/tests/test_connection_g3.py b/app/tests/test_connection_g3.py
new file mode 100644
index 0000000..452bf18
--- /dev/null
+++ b/app/tests/test_connection_g3.py
@@ -0,0 +1,84 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+
+from mock import patch
+from app.src.async_stream import AsyncStream
+from app.src.gen3.connection_g3 import ConnectionG3
+from app.src.gen3.talent import Talent
+
+@pytest.fixture
+def patch_async_init():
+ with patch.object(AsyncStream, '__init__') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_talent_init():
+ with patch.object(Talent, '__init__') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_healthy():
+ with patch.object(AsyncStream, 'healthy') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_async_close():
+ with patch.object(AsyncStream, 'close') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_talent_close():
+ with patch.object(Talent, 'close') as conn:
+ yield conn
+
+class FakeReader():
+ def __init__(self):
+ self.on_recv = asyncio.Event()
+ async def read(self, max_len: int):
+ await self.on_recv.wait()
+ return b''
+ def feed_eof(self):
+ return
+
+
+class FakeWriter():
+ def write(self, buf: bytes):
+ return
+ def get_extra_info(self, sel: str):
+ if sel == 'peername':
+ return 'remote.intern'
+ elif sel == 'sockname':
+ return 'sock:1234'
+ assert False
+ def is_closing(self):
+ return False
+ def close(self):
+ return
+ async def wait_closed(self):
+ return
+
+
+
+def test_method_calls(patch_async_init, patch_talent_init, patch_healthy, patch_async_close, patch_talent_close):
+ spy1 = patch_async_init
+ spy2 = patch_talent_init
+ spy3 = patch_healthy
+ spy4 = patch_async_close
+ spy5 = patch_talent_close
+ reader = FakeReader()
+ writer = FakeWriter()
+ id_str = "id_string"
+ addr = ('proxy.local', 10000)
+ conn = ConnectionG3(reader, writer, addr,
+ remote_stream= None, server_side=True, id_str=id_str)
+ spy1.assert_called_once_with(conn, reader, writer, addr)
+ spy2.assert_called_once_with(conn, True, id_str)
+ conn.healthy()
+
+ spy3.assert_called_once()
+
+ conn.close()
+ spy4.assert_called_once()
+ spy5.assert_called_once()
+
diff --git a/app/tests/test_connection_g3p.py b/app/tests/test_connection_g3p.py
new file mode 100644
index 0000000..67607f1
--- /dev/null
+++ b/app/tests/test_connection_g3p.py
@@ -0,0 +1,89 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+
+from mock import patch
+from app.src.singleton import Singleton
+from app.src.async_stream import AsyncStream
+from app.src.gen3plus.connection_g3p import ConnectionG3P
+from app.src.gen3plus.solarman_v5 import SolarmanV5
+
+@pytest.fixture
+def patch_async_init():
+ with patch.object(AsyncStream, '__init__') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_solarman_init():
+ with patch.object(SolarmanV5, '__init__') as conn:
+ yield conn
+
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ Singleton._instances.clear()
+ yield
+
+@pytest.fixture
+def patch_healthy():
+ with patch.object(AsyncStream, 'healthy') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_async_close():
+ with patch.object(AsyncStream, 'close') as conn:
+ yield conn
+
+@pytest.fixture
+def patch_solarman_close():
+ with patch.object(SolarmanV5, 'close') as conn:
+ yield conn
+
+class FakeReader():
+ def __init__(self):
+ self.on_recv = asyncio.Event()
+ async def read(self, max_len: int):
+ await self.on_recv.wait()
+ return b''
+ def feed_eof(self):
+ return
+
+
+class FakeWriter():
+ def write(self, buf: bytes):
+ return
+ def get_extra_info(self, sel: str):
+ if sel == 'peername':
+ return 'remote.intern'
+ elif sel == 'sockname':
+ return 'sock:1234'
+ assert False
+ def is_closing(self):
+ return False
+ def close(self):
+ return
+ async def wait_closed(self):
+ return
+
+
+
+def test_method_calls(patch_async_init, patch_solarman_init, patch_healthy, patch_async_close, patch_solarman_close):
+ spy1 = patch_async_init
+ spy2 = patch_solarman_init
+ spy3 = patch_healthy
+ spy4 = patch_async_close
+ spy5 = patch_solarman_close
+ reader = FakeReader()
+ writer = FakeWriter()
+ addr = ('proxy.local', 10000)
+ conn = ConnectionG3P(reader, writer, addr,
+ remote_stream= None, server_side=True, client_mode=False)
+ spy1.assert_called_once_with(conn, reader, writer, addr)
+ spy2.assert_called_once_with(conn, True, False)
+ conn.healthy()
+
+ spy3.assert_called_once()
+
+ conn.close()
+ spy4.assert_called_once()
+ spy5.assert_called_once()
+
diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py
index 37a8076..6fad692 100644
--- a/app/tests/test_infos_g3.py
+++ b/app/tests/test_infos_g3.py
@@ -1,7 +1,7 @@
# test_with_pytest.py
-import pytest, json
-from app.src.infos import Register, ClrAtMidnight
-from app.src.gen3.infos_g3 import InfosG3
+import pytest, json, math
+from app.src.infos import Register
+from app.src.gen3.infos_g3 import InfosG3, RegisterMap
@pytest.fixture
def contr_data_seq(): # Get Time Request message
@@ -364,12 +364,12 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
if id == 'out_power_123':
assert comp == 'sensor'
- assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
if id == 'daily_gen_123':
assert comp == 'sensor'
- assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
elif id == 'power_pv1_123':
@@ -388,6 +388,32 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
tests +=1
assert tests==5
+def test_build_ha_conf4(contr_data_seq, inv_data_seq):
+ i = InfosG3()
+ for key, result in i.parse (contr_data_seq):
+ pass # side effect in calling i.parse()
+ for key, result in i.parse (inv_data_seq):
+ 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_V1.00.06", "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_V1.00.06", "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
@@ -395,21 +421,21 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
if key == 'total' or key == 'inverter' or key == 'env':
assert update == True
tests +=1
- assert tests==5
+ assert tests==8
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
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_Temp": 23})
+ 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):
if key == 'total' or key == 'env':
assert update == False
tests +=1
- assert tests==3
+ assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
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_Temp": 23})
- assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
+ assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
+ assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "Max_Designed_Power": -1, "Output_Coefficient": 100.0, "No_Inputs": 2})
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
@@ -417,13 +443,12 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
assert update == False
tests +=1
elif key == 'env':
- assert update == True
tests +=1
- assert tests==3
+ assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "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_Temp": 0})
+ assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3()
@@ -436,10 +461,10 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert update == True
tests +=1
- assert tests==3
+ assert tests==4
assert json.dumps(i.db['total']) == json.dumps({})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "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_Temp": 0})
+ 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):
@@ -447,18 +472,17 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert update == False
tests +=1
- assert tests==3
+ assert tests==4
assert json.dumps(i.db['total']) == json.dumps({})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "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_Temp": 0})
+ 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):
if key == 'total' or key == 'env':
- assert update == True
tests +=1
- assert tests==3
+ assert tests==4
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
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}})
@@ -496,3 +520,15 @@ def test_invalid_data_type(invalid_data_seq):
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
assert val == 1
+
+def test_result_eval(inv_data_seq2: bytes):
+
+ # add eval to convert temperature from °F to °C
+ RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
+
+ i = InfosG3()
+
+ for _, _ in i.parse (inv_data_seq2):
+ pass # side effect is calling generator i.parse()
+ assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
+ del RegisterMap.map[0x00000514]['eval'] # remove eval
diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py
index f7aef4d..c80a6b7 100644
--- a/app/tests/test_infos_g3p.py
+++ b/app/tests/test_infos_g3p.py
@@ -86,7 +86,7 @@ def test_parse_4110(str_test_ip, device_data: bytes):
assert json.dumps(i.db) == json.dumps({
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
- 'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
+ 'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
})
def test_parse_4210(inverter_data: bytes):
@@ -98,7 +98,7 @@ def test_parse_4210(inverter_data: bytes):
assert json.dumps(i.db) == json.dumps({
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
- "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
+ "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
diff --git a/app/tests/test_inverter_g3.py b/app/tests/test_inverter_g3.py
new file mode 100644
index 0000000..017e897
--- /dev/null
+++ b/app/tests/test_inverter_g3.py
@@ -0,0 +1,235 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+
+from mock import patch
+from enum import Enum
+from app.src.infos import Infos
+from app.src.config import Config
+from app.src.inverter import Inverter
+from app.src.singleton import Singleton
+from app.src.gen3.connection_g3 import ConnectionG3
+from app.src.gen3.inverter_g3 import InverterG3
+
+from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
+
+pytest_plugins = ('pytest_asyncio',)
+
+# initialize the proxy statistics
+Infos.static_init()
+
+@pytest.fixture
+def config_conn():
+ Config.act_config = {
+ 'mqtt':{
+ 'host': test_hostname,
+ 'port': test_port,
+ 'user': '',
+ 'passwd': ''
+ },
+ 'ha':{
+ 'auto_conf_prefix': 'homeassistant',
+ 'discovery_prefix': 'homeassistant',
+ 'entity_prefix': 'tsun',
+ 'proxy_node_id': 'test_1',
+ 'proxy_unique_id': ''
+ },
+ 'tsun':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
+ }
+
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ Singleton._instances.clear()
+ yield
+
+@pytest.fixture
+def patch_conn_init():
+ with patch.object(ConnectionG3, '__init__', return_value= None) as conn:
+ yield conn
+
+@pytest.fixture
+def patch_conn_close():
+ with patch.object(ConnectionG3, 'close') as conn:
+ yield conn
+
+class FakeReader():
+ def __init__(self):
+ self.on_recv = asyncio.Event()
+ async def read(self, max_len: int):
+ await self.on_recv.wait()
+ return b''
+ def feed_eof(self):
+ return
+
+
+class FakeWriter():
+ def write(self, buf: bytes):
+ return
+ def get_extra_info(self, sel: str):
+ if sel == 'peername':
+ return 'remote.intern'
+ elif sel == 'sockname':
+ return 'sock:1234'
+ assert False
+ def is_closing(self):
+ return False
+ def close(self):
+ return
+ async def wait_closed(self):
+ return
+
+class TestType(Enum):
+ RD_TEST_0_BYTES = 1
+ RD_TEST_TIMEOUT = 2
+ RD_TEST_EXCEPT = 3
+
+
+test = TestType.RD_TEST_0_BYTES
+
+@pytest.fixture
+def patch_open_connection():
+ async def new_conn(conn):
+ await asyncio.sleep(0)
+ return FakeReader(), FakeWriter()
+
+ def new_open(host: str, port: int):
+ global test
+ if test == TestType.RD_TEST_TIMEOUT:
+ raise ConnectionRefusedError
+ elif test == TestType.RD_TEST_EXCEPT:
+ raise ValueError("Value cannot be negative") # Compliant
+ return new_conn(None)
+
+ with patch.object(asyncio, 'open_connection', new_open) as conn:
+ yield conn
+
+
+def test_method_calls(patch_conn_init, patch_conn_close):
+ spy1 = patch_conn_init
+ spy2 = patch_conn_close
+ reader = FakeReader()
+ writer = FakeWriter()
+ addr = ('proxy.local', 10000)
+ inverter = InverterG3(reader, writer, addr)
+ inverter.l_addr = ''
+ inverter.r_addr = ''
+
+ spy1.assert_called_once()
+ spy1.assert_called_once_with(reader, writer, addr, None, True)
+
+ inverter.close()
+ spy2.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_remote_conn(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
+
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_remote_except(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ global test
+ test = TestType.RD_TEST_TIMEOUT
+
+ inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
+
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream==None
+
+ test = TestType.RD_TEST_EXCEPT
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream==None
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_publish(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
+ inverter._Talent__set_serial_no(serial_no= "123344")
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == False
+
+ inverter.new_data['env'] = True
+ inverter.db.db['env'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['env'] == False
+
+ Infos.new_stat_data['proxy'] = True
+ await inverter.async_publ_mqtt()
+ assert Infos.new_stat_data['proxy'] == False
+
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ _ = patch_mqtt_err
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
+ inverter._Talent__set_serial_no(serial_no= "123344")
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == True
+
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ _ = patch_mqtt_except
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3(FakeReader(), FakeWriter(), ('proxy.local', 10000))
+ inverter._Talent__set_serial_no(serial_no= "123344")
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == True
+
+ inverter.close()
+ spy1.assert_called_once()
diff --git a/app/tests/test_inverter_g3p.py b/app/tests/test_inverter_g3p.py
new file mode 100644
index 0000000..07d1160
--- /dev/null
+++ b/app/tests/test_inverter_g3p.py
@@ -0,0 +1,236 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+
+from mock import patch
+from enum import Enum
+from app.src.infos import Infos
+from app.src.config import Config
+from app.src.inverter import Inverter
+from app.src.singleton import Singleton
+from app.src.gen3plus.connection_g3p import ConnectionG3P
+from app.src.gen3plus.inverter_g3p import InverterG3P
+
+from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
+
+
+pytest_plugins = ('pytest_asyncio',)
+
+# initialize the proxy statistics
+Infos.static_init()
+
+@pytest.fixture
+def config_conn():
+ Config.act_config = {
+ 'mqtt':{
+ 'host': test_hostname,
+ 'port': test_port,
+ 'user': '',
+ 'passwd': ''
+ },
+ 'ha':{
+ 'auto_conf_prefix': 'homeassistant',
+ 'discovery_prefix': 'homeassistant',
+ 'entity_prefix': 'tsun',
+ 'proxy_node_id': 'test_1',
+ 'proxy_unique_id': ''
+ },
+ 'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True}
+ }
+
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ Singleton._instances.clear()
+ yield
+
+@pytest.fixture
+def patch_conn_init():
+ with patch.object(ConnectionG3P, '__init__', return_value= None) as conn:
+ yield conn
+
+@pytest.fixture
+def patch_conn_close():
+ with patch.object(ConnectionG3P, 'close') as conn:
+ yield conn
+
+class FakeReader():
+ def __init__(self):
+ self.on_recv = asyncio.Event()
+ async def read(self, max_len: int):
+ await self.on_recv.wait()
+ return b''
+ def feed_eof(self):
+ return
+
+
+class FakeWriter():
+ def write(self, buf: bytes):
+ return
+ def get_extra_info(self, sel: str):
+ if sel == 'peername':
+ return 'remote.intern'
+ elif sel == 'sockname':
+ return 'sock:1234'
+ assert False
+ def is_closing(self):
+ return False
+ def close(self):
+ return
+ async def wait_closed(self):
+ return
+
+class TestType(Enum):
+ RD_TEST_0_BYTES = 1
+ RD_TEST_TIMEOUT = 2
+ RD_TEST_EXCEPT = 3
+
+
+test = TestType.RD_TEST_0_BYTES
+
+@pytest.fixture
+def patch_open_connection():
+ async def new_conn(conn):
+ await asyncio.sleep(0)
+ return FakeReader(), FakeWriter()
+
+ def new_open(host: str, port: int):
+ global test
+ if test == TestType.RD_TEST_TIMEOUT:
+ raise ConnectionRefusedError
+ elif test == TestType.RD_TEST_EXCEPT:
+ raise ValueError("Value cannot be negative") # Compliant
+ return new_conn(None)
+
+ with patch.object(asyncio, 'open_connection', new_open) as conn:
+ yield conn
+
+
+def test_method_calls(patch_conn_init, patch_conn_close):
+ spy1 = patch_conn_init
+ spy2 = patch_conn_close
+ reader = FakeReader()
+ writer = FakeWriter()
+ addr = ('proxy.local', 10000)
+ inverter = InverterG3P(reader, writer, addr, client_mode=False)
+ inverter.l_addr = ''
+ inverter.r_addr = ''
+
+ spy1.assert_called_once()
+ spy1.assert_called_once_with(reader, writer, addr, None, server_side=True, client_mode=False)
+
+ inverter.close()
+ spy2.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_remote_conn(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
+
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_remote_except(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ global test
+ test = TestType.RD_TEST_TIMEOUT
+
+ inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
+
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream==None
+
+ test = TestType.RD_TEST_EXCEPT
+ await inverter.async_create_remote()
+ await asyncio.sleep(0)
+ assert inverter.remote_stream==None
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_publish(config_conn, patch_open_connection, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
+ inverter._SolarmanV5__set_serial_no(snr= 123344)
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == False
+
+ inverter.new_data['env'] = True
+ inverter.db.db['env'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['env'] == False
+
+ Infos.new_stat_data['proxy'] = True
+ await inverter.async_publ_mqtt()
+ assert Infos.new_stat_data['proxy'] == False
+
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ _ = patch_mqtt_err
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
+ inverter._SolarmanV5__set_serial_no(snr= 123344)
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == True
+
+ inverter.close()
+ spy1.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except, patch_conn_close):
+ _ = config_conn
+ _ = patch_open_connection
+ _ = patch_mqtt_except
+ assert asyncio.get_running_loop()
+
+ spy1 = patch_conn_close
+
+ Inverter.class_init()
+
+ inverter = InverterG3P(FakeReader(), FakeWriter(), ('proxy.local', 10000), client_mode=False)
+ inverter._SolarmanV5__set_serial_no(snr= 123344)
+
+ inverter.new_data['inverter'] = True
+ inverter.db.db['inverter'] = {}
+ await inverter.async_publ_mqtt()
+ assert inverter.new_data['inverter'] == True
+
+ inverter.close()
+ spy1.assert_called_once()
diff --git a/app/tests/test_modbus_tcp.py b/app/tests/test_modbus_tcp.py
index f9ec1ef..f68e031 100644
--- a/app/tests/test_modbus_tcp.py
+++ b/app/tests/test_modbus_tcp.py
@@ -1,10 +1,10 @@
# test_with_pytest.py
import pytest
import asyncio
+from aiomqtt import MqttCodeError
from mock import patch
from enum import Enum
-from enum import Enum
from app.src.singleton import Singleton
from app.src.config import Config
from app.src.infos import Infos
@@ -134,10 +134,20 @@ def patch_no_mqtt():
yield conn
@pytest.fixture
-def patch_no_mqtt():
- with patch.object(Mqtt, 'publish') as conn:
+def patch_mqtt_err():
+ def new_publish(self, key, data):
+ raise MqttCodeError(None)
+
+ with patch.object(Mqtt, 'publish', new_publish) as conn:
yield conn
+@pytest.fixture
+def patch_mqtt_except():
+ def new_publish(self, key, data):
+ raise ValueError("Test")
+
+ with patch.object(Mqtt, 'publish', new_publish) as conn:
+ yield conn
@pytest.mark.asyncio
async def test_modbus_conn(patch_open):
@@ -205,10 +215,6 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
assert 1 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
- # check that the connection is released
- for m in Message:
- if (m.node_id == 'inv_2'):
- assert False
@pytest.mark.asyncio
async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
@@ -242,3 +248,67 @@ async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
assert 2 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
+
+@pytest.mark.asyncio
+async def test_mqtt_err(config_conn, patch_mqtt_err, patch_open):
+ _ = config_conn
+ _ = patch_open
+ _ = patch_mqtt_err
+ global test
+ assert asyncio.get_running_loop()
+ Inverter.class_init()
+ test = TestType.RD_TEST_0_BYTES
+
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
+ ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
+ await asyncio.sleep(0.01)
+ test = 0
+ for m in Message:
+ if (m.node_id == 'inv_2'):
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 1
+ test += 1
+ if test == 1:
+ m.shutdown_started = False
+ m.reader.on_recv.set()
+ await asyncio.sleep(0.1)
+ assert m.state == State.closed
+ await asyncio.sleep(0.1)
+ else:
+ m.shutdown_started = True
+ m.reader.on_recv.set()
+ del m
+
+ await asyncio.sleep(0.01)
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
+
+@pytest.mark.asyncio
+async def test_mqtt_except(config_conn, patch_mqtt_except, patch_open):
+ _ = config_conn
+ _ = patch_open
+ _ = patch_mqtt_except
+ global test
+ assert asyncio.get_running_loop()
+ Inverter.class_init()
+ test = TestType.RD_TEST_0_BYTES
+
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
+ ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
+ await asyncio.sleep(0.01)
+ test = 0
+ for m in Message:
+ if (m.node_id == 'inv_2'):
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 1
+ test += 1
+ if test == 1:
+ m.shutdown_started = False
+ m.reader.on_recv.set()
+ await asyncio.sleep(0.1)
+ assert m.state == State.closed
+ await asyncio.sleep(0.1)
+ else:
+ m.shutdown_started = True
+ m.reader.on_recv.set()
+ del m
+
+ await asyncio.sleep(0.01)
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py
index 521ef9b..80778a2 100644
--- a/app/tests/test_solarman.py
+++ b/app/tests/test_solarman.py
@@ -184,6 +184,35 @@ def device_rsp_msg(): # 0x1110
msg += b'\x15'
return msg
+@pytest.fixture
+def device_ind_msg2(): # 0x4110
+ msg = b'\xa5\xd4\x00\x10\x41\x02\x03' +get_sn() +b'\x02\xba\xd2\x00\x00'
+ msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
+ msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
+ msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
+ msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
+ msg += b'\x02\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\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
+ msg += b'\x6d\x65\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'
+ msg += correct_checksum(msg)
+ msg += b'\x15'
+ return msg
+
+@pytest.fixture
+def device_rsp_msg2(): # 0x1110
+ msg = b'\xa5\x0a\x00\x10\x11\x03\x03' +get_sn() +b'\x02\x01'
+ msg += total()
+ msg += hb()
+ msg += correct_checksum(msg)
+ msg += b'\x15'
+ return msg
+
@pytest.fixture
def invalid_start_byte(): # 0x4110
msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
@@ -901,6 +930,54 @@ def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_in
assert m._send_buffer==b''
m.close()
+def test_read_two_messages3(config_tsun_allow_all, device_ind_msg2, device_rsp_msg2, inverter_ind_msg, inverter_rsp_msg):
+ # test device message received after the inverter masg
+ _ = config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg, (0,))
+ m.append_msg(device_ind_msg2)
+ 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 == 2070233889
+ assert m.unique_id == '2070233889'
+ assert m.msg_recvd[0]['control']==0x4210
+ assert m.msg_recvd[0]['seq']=='02:02'
+ assert m.msg_recvd[0]['data_len']==0x199
+ assert m.msg_recvd[1]['control']==0x4110
+ assert m.msg_recvd[1]['seq']=='03:03'
+ assert m.msg_recvd[1]['data_len']==0xd4
+ assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None)
+ assert 0x02b0 == m.sensor_list
+ assert m._forward_buffer==inverter_ind_msg+device_ind_msg2
+ assert m._send_buffer==inverter_rsp_msg+device_rsp_msg2
+
+ m._send_buffer = bytearray(0) # clear send buffer for next test
+ m._init_new_client_conn()
+ assert m._send_buffer==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,))
+ 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.header_len==11
+ assert m.snr == 2070233889
+ assert m.unique_id == '2070233889'
+ assert m.control == 0x4210
+ assert str(m.seq) == '03:03'
+ assert m.data_len == 0x199
+ assert m._recv_buffer==b''
+ assert m._send_buffer==inverter_rsp_msg_81
+ assert m._forward_buffer==inverter_ind_msg_81
+ assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
+ m.close()
+
def test_unkown_message(config_tsun_inv1, unknown_msg):
_ = config_tsun_inv1
m = MemoryStream(unknown_msg, (0,))
diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py
index 16bb8d8..4ea5b54 100644
--- a/app/tests/test_talent.py
+++ b/app/tests/test_talent.py
@@ -41,6 +41,7 @@ class MemoryStream(Talent):
self.send_msg_ofs = 0
self.test_exception_async_write = False
self.msg_recvd = []
+ self.remote_stream = None
def append_msg(self, msg):
self.__msg += msg
@@ -138,6 +139,26 @@ def msg_time_rsp_inv(): # Get Time Resonse message
def msg_time_invalid(): # Get Time Request message
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22'
+@pytest.fixture
+def msg_act_time(): # Act Time Indication message
+ return b'\x00\x00\x00\x1c\x10R170000000000001\x91\x99\x01\x00\x00\x01\x89\xc6\x53\x4d\x80'
+
+@pytest.fixture
+def msg_act_time_ofs(): # Act Time Indication message withoffset 3600
+ return b'\x00\x00\x00\x1c\x10R170000000000001\x91\x99\x01\x00\x00\x01\x89\xc6\x53\x5b\x90'
+
+@pytest.fixture
+def msg_act_time_ack(): # Act Time Response message
+ return b'\x00\x00\x00\x14\x10R170000000000001\x99\x99\x02'
+
+@pytest.fixture
+def msg_act_time_cmd(): # Act Time Response message
+ return b'\x00\x00\x00\x14\x10R170000000000001\x70\x99\x02'
+
+@pytest.fixture
+def msg_act_time_inv(): # Act Time Indication message withoffset 3600
+ return b'\x00\x00\x00\x1b\x10R170000000000001\x91\x99\x00\x00\x01\x89\xc6\x53\x5b\x90'
+
@pytest.fixture
def msg_controller_ind(): # Data indication from the controller
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
@@ -442,6 +463,26 @@ def msg_modbus_rsp21():
msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
return msg
+@pytest.fixture
+def msg_modbus_cmd_new():
+ msg = b'\x00\x00\x00\x20\x10R170000000000001'
+ msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x03\x30\x00'
+ msg += b'\x00\x30\x4a\xde'
+ return msg
+
+@pytest.fixture
+def msg_modbus_rsp20_new():
+ msg = b'\x00\x00\x00\x7e\x10R170000000000001'
+ msg += b'\x91\x87\x00\x01\xa3\x28\x00\x65\x01\x03\x60'
+ msg += b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ msg += b'\x51\x09\x09\x17\x00\x17\x13\x88\x00\x40\x00\x00\x02\x58\x02\x23'
+ msg += b'\x00\x07\x00\x00\x00\x00\x01\x4f\x00\xab\x02\x40\x00\x00\x00\x00'
+ msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\xc0\x93\x00\x00'
+ msg += b'\x00\x00\x33\xad\x00\x09\x00\x00\x98\x1c\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'\xa7\xab'
+ return msg
+
@pytest.fixture
def broken_recv_buf(): # There are two message in the buffer, but the second has overwritten the first partly
msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
@@ -892,6 +933,7 @@ def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid):
def test_msg_get_time(config_tsun_inv1, msg_get_time):
_ = config_tsun_inv1
m = MemoryStream(msg_get_time, (0,))
+ m.state = State.up
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -903,6 +945,7 @@ def test_msg_get_time(config_tsun_inv1, msg_get_time):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
+ assert m.state==State.pend
assert m._forward_buffer==msg_get_time
assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
@@ -911,6 +954,7 @@ def test_msg_get_time(config_tsun_inv1, msg_get_time):
def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
_ = config_no_tsun_inv1
m = MemoryStream(msg_get_time, (0,))
+ m.state = State.received
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -922,14 +966,19 @@ def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
+ assert m.state==State.received
assert m._forward_buffer==b''
assert m._send_buffer==bytearray(b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00')
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
+ # test if ts_offset will be set on client and server side
_ = config_tsun_inv1
m = MemoryStream(msg_time_rsp, (0,), False)
+ s = MemoryStream(b'', (0,), True)
+ assert s.ts_offset==0
+ m.remote_stream = s
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -940,10 +989,13 @@ def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
assert m.msg_id==34
assert m.header_len==23
assert m.ts_offset==3600000
+ assert s.ts_offset==3600000
assert m.data_len==8
assert m._forward_buffer==b''
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.remote_stream = None
+ s.close()
m.close()
def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp):
@@ -1022,6 +1074,169 @@ def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
+def test_msg_act_time(config_no_modbus_poll, msg_act_time, msg_act_time_ack):
+ _ = config_no_modbus_poll
+ m = MemoryStream(msg_act_time, (0,))
+ m.ts_offset=0
+ m.mb_timeout = 124
+ m.db.set_db_def_value(Register.POLLING_INTERVAL, 125)
+ m.state = State.received
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.ts_offset==0
+ assert m.header_len==23
+ assert m.data_len==9
+ assert m.state == State.up
+ assert m._forward_buffer==msg_act_time
+ assert m._send_buffer==msg_act_time_ack
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ assert 125 == m.db.get_db_value(Register.POLLING_INTERVAL, 0)
+ m.close()
+
+def test_msg_act_time2(config_tsun_inv1, msg_act_time, msg_act_time_ack):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time, (0,))
+ m.ts_offset=0
+ m.modbus_polling = True
+ m.mb_timeout = 123
+ m.db.set_db_def_value(Register.POLLING_INTERVAL, 125)
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.ts_offset==0
+ assert m.header_len==23
+ assert m.data_len==9
+ assert m._forward_buffer==msg_act_time
+ assert m._send_buffer==msg_act_time_ack
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ assert 123 == m.db.get_db_value(Register.POLLING_INTERVAL, 0)
+ m.close()
+
+def test_msg_act_time_ofs(config_tsun_inv1, msg_act_time, msg_act_time_ofs, msg_act_time_ack):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time, (0,))
+ m.ts_offset=3600
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.ts_offset==3600
+ assert m.header_len==23
+ assert m.data_len==9
+ assert m._forward_buffer==msg_act_time_ofs
+ assert m._send_buffer==msg_act_time_ack
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
+def test_msg_act_time_ofs2(config_tsun_inv1, msg_act_time, msg_act_time_ofs, msg_act_time_ack):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time_ofs, (0,))
+ m.ts_offset=-3600
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.ts_offset==-3600
+ assert m.header_len==23
+ assert m.data_len==9
+ assert m._forward_buffer==msg_act_time
+ assert m._send_buffer==msg_act_time_ack
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
+def test_msg_act_time_autark(config_no_tsun_inv1, msg_act_time, msg_act_time_ack):
+ _ = config_no_tsun_inv1
+ m = MemoryStream(msg_act_time, (0,))
+ m.ts_offset=0
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.ts_offset==0
+ assert m.header_len==23
+ assert m.data_len==9
+ assert m._forward_buffer==b''
+ assert m._send_buffer==msg_act_time_ack
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
+def test_msg_act_time_ack(config_tsun_inv1, msg_act_time_ack):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time_ack, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==153
+ assert m.msg_id==153
+ assert m.header_len==23
+ assert m.data_len==1
+ assert m._forward_buffer==b''
+ assert m._send_buffer==b''
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
+def test_msg_act_time_cmd(config_tsun_inv1, msg_act_time_cmd):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time_cmd, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==112
+ assert m.msg_id==153
+ assert m.header_len==23
+ assert m.data_len==1
+ assert m._forward_buffer==msg_act_time_cmd
+ assert m._send_buffer==b''
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
+ m.close()
+
+def test_msg_act_time_inv(config_tsun_inv1, msg_act_time_inv):
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_act_time_inv, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==153
+ assert m.header_len==23
+ assert m.data_len==8
+ assert m._forward_buffer==msg_act_time_inv
+ assert m._send_buffer==b''
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack):
_ = config_tsun_inv1
m = MemoryStream(msg_controller_ind, (0,))
@@ -1183,7 +1398,7 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
m._update_header(m._forward_buffer)
assert m._forward_buffer==msg_inverter_ind_0w
assert m._send_buffer==msg_inverter_ack
- assert m.db.get_db_value(Register.INVERTER_STATUS) == None
+ assert m.db.get_db_value(Register.INVERTER_STATUS) == 1
assert isclose(m.db.db['grid']['Output_Power'], 0.5)
m.close()
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
@@ -1583,7 +1798,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
assert m.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp20
assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
+ assert m.db.db == {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True
@@ -1613,13 +1828,64 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
assert m.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp21
assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
+ assert m.db.db == {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E'
assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True
m.close()
+def test_msg_modbus_rsp4(config_tsun_inv1, msg_modbus_rsp21):
+ '''Modbus response with a valid Modbus but no new values request must be forwarded'''
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp21)
+
+ m.mb.rsp_handler = m.msg_forward
+ m.mb.last_addr = 1
+ m.mb.last_fcode = 3
+ m.mb.last_len = 20
+ m.mb.last_reg = 0x3008
+ m.mb.req_pend = True
+ m.mb.err = 0
+ db_values = {'collector': {'Serial_Number': 'R170000000000001'}, 'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
+ m.db.db = db_values
+ m.new_data['inverter'] = False
+
+ 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.mb.err == 0
+ assert m.msg_count == 1
+ assert m._forward_buffer==msg_modbus_rsp21
+ assert m.modbus_elms == 19
+ assert m._send_buffer==b''
+ assert m.db.db == db_values
+ assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E'
+ assert m.db.get_db_value(Register.TS_GRID) == m._utc()
+ assert m.new_data['inverter'] == False
+
+ m.close()
+
+def test_msg_modbus_rsp_new(config_tsun_inv1, msg_modbus_rsp20_new):
+ '''Modbus response in new format with a valid Modbus request must be forwarded'''
+ _ = config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp20_new)
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ m.db.stat['proxy']['Modbus_Command'] = 0
+ 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 int(m.ctrl)==145
+ assert m.msg_id==135
+ assert m.header_len==23
+ assert m.data_len==107
+ assert m._forward_buffer==b''
+ assert m._send_buffer==b''
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ assert m.db.stat['proxy']['Modbus_Command'] == 0
+ m.close()
+
def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
_ = config_tsun_inv1
m = MemoryStream(msg_modbus_inv, (0,), False)