* 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
This commit is contained in:
Stefan Allius
2024-09-16 00:45:36 +02:00
committed by GitHub
parent a9dc7e6847
commit bfea38d9da
22 changed files with 1780 additions and 191 deletions

View File

@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased] ## [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 - 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 - Reading the version string from the image updates it even if the image is re-pulled without re-deployment

174
README.md
View File

@@ -7,7 +7,7 @@
<p align="center"> <p align="center">
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a> <a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
<a href="https://www.python.org/downloads/release/python-3120/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.12-blue.svg"></a> <a href="https://www.python.org/downloads/release/python-3120/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.12-blue.svg"></a>
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.2.0-lightblue.svg"></a> <a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.0-lightblue.svg"></a>
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a> <a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a> <a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
<br> <br>
@@ -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. 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> You find more details here: <https://toml.io/en/v1.0.0>
<details>
<summary>Here is an example of a <b>config.toml</b> file</summary>
```toml ```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' ### T S U N - G E N 3 - P R O X Y
tsun.port = 5005 ###
### from Stefan Allius
# 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 ### 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.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883 mqtt.port = 1883
mqtt.user = '' mqtt.user = ''
mqtt.passwd = '' 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.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values 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 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` tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
# tsun.host = 'logger.talent-monitoring.com'
# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"] 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"] [inverters."R17xxxxxxxxxxxx1"]
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number node_id = 'inv_1' # MQTT replacement for inverters serial number
suggested_area = 'roof' # Optional, suggested installation area for home-assistant suggested_area = 'roof' # suggested installation place for home-assistant
modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {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 ## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr ## definition. To do this, the corresponding configuration block is started with
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr ## `[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 [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 monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
node_id = 'inv_3' # MQTT replacement for inverters serial number node_id = 'inv_2' # MQTT replacement for inverters serial number
suggested_area = 'garage' # suggested installation place for home-assistant suggested_area = 'garage' # suggested installation place for home-assistant
modbus_polling = false # Enable optional MODBUS polling for GEN3PLUS inverter modbus_polling = true # Enable optional MODBUS polling
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment # 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 # the next line and configure the fixed IP of your inverter
#client_mode = {host = '192.168.0.1', port = 8899} #client_mode = {host = '192.168.0.1', port = 8899}
pv1 = {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 pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv3 = {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 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] [gen3plus.at_acl]
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access
tsun.block = [] tsun.block = []
@@ -190,6 +302,8 @@ mqtt.block = []
``` ```
</details>
## Inverter Configuration ## 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_<Montoring SN>`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the inverter. 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_<Montoring SN>`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the inverter.

View File

@@ -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' ### T S U N - G E N 3 - P R O X Y
tsun.port = 5005 ###
### 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.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883 mqtt.port = 1883
mqtt.user = '' mqtt.user = ''
mqtt.passwd = '' 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.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values 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_node_id = 'proxy' # MQTT node id, for the proxy_node_id
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance 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"] [inverters."R170000000000001"]
#node_id = '' # Optional, MQTT replacement for inverters serial number node_id = '' # MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation area for home-assistant suggested_area = '' # suggested installation area for home-assistant
modbus_polling = false # Disable optional MODBUS polling modbus_polling = false # Disable optional MODBUS polling
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {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 ## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr ## definition. To do this, the corresponding configuration block is started with
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr ## `[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"] [inverters."Y170000000000001"]
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
#node_id = '' # Optional, MQTT replacement for inverters serial number node_id = '' # MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation place for home-assistant suggested_area = '' # suggested installation place for home-assistant
modbus_polling = true # Enable optional MODBUS polling modbus_polling = true # Enable optional MODBUS polling
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment # 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 # the next line and configure the fixed IP of your inverter
#client_mode = {host = '192.168.0.1', port = 8899} #client_mode = {host = '192.168.0.1', port = 8899}
#pv1 = {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 pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv3 = {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 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] [gen3plus.at_acl]
# filter for received commands from the internet
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
tsun.block = [] tsun.block = []
# filter for received commands from the MQTT broker
mqtt.allow = ['AT+'] mqtt.allow = ['AT+']
mqtt.block = [] mqtt.block = []

View File

@@ -78,7 +78,7 @@ target "dev" {
target "preview" { target "preview" {
inherits = ["_common", "_prod"] inherits = ["_common", "_prod"]
tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"] tags = ["${IMAGE}:preview", "${IMAGE}:${VERSION}"]
} }
target "rc" { target "rc" {

View File

@@ -1,4 +1,4 @@
aiomqtt==2.2.0 aiomqtt==2.3.0
schema==0.7.7 schema==0.7.7
aiocron==1.8 aiocron==1.8
aiohttp==3.10.2 aiohttp==3.10.5

View File

@@ -1,7 +1,12 @@
import logging import logging
from asyncio import StreamReader, StreamWriter 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') logger = logging.getLogger('conn')

View File

@@ -11,81 +11,82 @@ else: # pragma: no cover
class RegisterMap: class RegisterMap:
map = { map = {
0x00092ba8: Register.COLLECTOR_FW_VERSION, 0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
0x000927c0: Register.CHIP_TYPE, 0x000927c0: {'reg': Register.CHIP_TYPE},
0x00092f90: Register.CHIP_MODEL, 0x00092f90: {'reg': Register.CHIP_MODEL},
0x00095a88: Register.TRACE_URL, 0x00094ae8: {'reg': Register.MAC_ADDR},
0x00095aec: Register.LOGGER_URL, 0x00095a88: {'reg': Register.TRACE_URL},
0x0000000a: Register.PRODUCT_NAME, 0x00095aec: {'reg': Register.LOGGER_URL},
0x00000014: Register.MANUFACTURER, 0x0000000a: {'reg': Register.PRODUCT_NAME},
0x0000001e: Register.VERSION, 0x00000014: {'reg': Register.MANUFACTURER},
0x00000028: Register.SERIAL_NUMBER, 0x0000001e: {'reg': Register.VERSION},
0x00000032: Register.EQUIPMENT_MODEL, 0x00000028: {'reg': Register.SERIAL_NUMBER},
0x00013880: Register.NO_INPUTS, 0x00000032: {'reg': Register.EQUIPMENT_MODEL},
0xffffff00: Register.INVERTER_CNT, 0x00013880: {'reg': Register.NO_INPUTS},
0xffffff01: Register.UNKNOWN_SNR, 0xffffff00: {'reg': Register.INVERTER_CNT},
0xffffff02: Register.UNKNOWN_MSG, 0xffffff01: {'reg': Register.UNKNOWN_SNR},
0xffffff03: Register.INVALID_DATA_TYPE, 0xffffff02: {'reg': Register.UNKNOWN_MSG},
0xffffff04: Register.INTERNAL_ERROR, 0xffffff03: {'reg': Register.INVALID_DATA_TYPE},
0xffffff05: Register.UNKNOWN_CTRL, 0xffffff04: {'reg': Register.INTERNAL_ERROR},
0xffffff06: Register.OTA_START_MSG, 0xffffff05: {'reg': Register.UNKNOWN_CTRL},
0xffffff07: Register.SW_EXCEPTION, 0xffffff06: {'reg': Register.OTA_START_MSG},
0xffffff08: Register.MAX_DESIGNED_POWER, 0xffffff07: {'reg': Register.SW_EXCEPTION},
0xffffff09: Register.OUTPUT_COEFFICIENT, 0xffffff08: {'reg': Register.POLLING_INTERVAL},
0xffffff0a: Register.INVERTER_STATUS, 0xfffffffe: {'reg': Register.TEST_REG1},
0xffffff0b: Register.POLLING_INTERVAL, 0xffffffff: {'reg': Register.TEST_REG2},
0xfffffffe: Register.TEST_REG1, 0x00000640: {'reg': Register.OUTPUT_POWER},
0xffffffff: Register.TEST_REG2, 0x000005dc: {'reg': Register.RATED_POWER},
0x00000640: Register.OUTPUT_POWER, 0x00000514: {'reg': Register.INVERTER_TEMP},
0x000005dc: Register.RATED_POWER, 0x000006a4: {'reg': Register.PV1_VOLTAGE},
0x00000514: Register.INVERTER_TEMP, 0x00000708: {'reg': Register.PV1_CURRENT},
0x000006a4: Register.PV1_VOLTAGE, 0x0000076c: {'reg': Register.PV1_POWER},
0x00000708: Register.PV1_CURRENT, 0x000007d0: {'reg': Register.PV2_VOLTAGE},
0x0000076c: Register.PV1_POWER, 0x00000834: {'reg': Register.PV2_CURRENT},
0x000007d0: Register.PV2_VOLTAGE, 0x00000898: {'reg': Register.PV2_POWER},
0x00000834: Register.PV2_CURRENT, 0x000008fc: {'reg': Register.PV3_VOLTAGE},
0x00000898: Register.PV2_POWER, 0x00000960: {'reg': Register.PV3_CURRENT},
0x000008fc: Register.PV3_VOLTAGE, 0x000009c4: {'reg': Register.PV3_POWER},
0x00000960: Register.PV3_CURRENT, 0x00000a28: {'reg': Register.PV4_VOLTAGE},
0x000009c4: Register.PV3_POWER, 0x00000a8c: {'reg': Register.PV4_CURRENT},
0x00000a28: Register.PV4_VOLTAGE, 0x00000af0: {'reg': Register.PV4_POWER},
0x00000a8c: Register.PV4_CURRENT, 0x00000c1c: {'reg': Register.PV1_DAILY_GENERATION},
0x00000af0: Register.PV4_POWER, 0x00000c80: {'reg': Register.PV1_TOTAL_GENERATION},
0x00000c1c: Register.PV1_DAILY_GENERATION, 0x00000ce4: {'reg': Register.PV2_DAILY_GENERATION},
0x00000c80: Register.PV1_TOTAL_GENERATION, 0x00000d48: {'reg': Register.PV2_TOTAL_GENERATION},
0x00000ce4: Register.PV2_DAILY_GENERATION, 0x00000dac: {'reg': Register.PV3_DAILY_GENERATION},
0x00000d48: Register.PV2_TOTAL_GENERATION, 0x00000e10: {'reg': Register.PV3_TOTAL_GENERATION},
0x00000dac: Register.PV3_DAILY_GENERATION, 0x00000e74: {'reg': Register.PV4_DAILY_GENERATION},
0x00000e10: Register.PV3_TOTAL_GENERATION, 0x00000ed8: {'reg': Register.PV4_TOTAL_GENERATION},
0x00000e74: Register.PV4_DAILY_GENERATION, 0x00000b54: {'reg': Register.DAILY_GENERATION},
0x00000ed8: Register.PV4_TOTAL_GENERATION, 0x00000bb8: {'reg': Register.TOTAL_GENERATION},
0x00000b54: Register.DAILY_GENERATION, 0x000003e8: {'reg': Register.GRID_VOLTAGE},
0x00000bb8: Register.TOTAL_GENERATION, 0x0000044c: {'reg': Register.GRID_CURRENT},
0x000003e8: Register.GRID_VOLTAGE, 0x000004b0: {'reg': Register.GRID_FREQUENCY},
0x0000044c: Register.GRID_CURRENT, 0x000cfc38: {'reg': Register.CONNECT_COUNT},
0x000004b0: Register.GRID_FREQUENCY, 0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
0x000cfc38: Register.CONNECT_COUNT, 0x000c96a8: {'reg': Register.POWER_ON_TIME},
0x000c3500: Register.SIGNAL_STRENGTH, 0x000d0020: {'reg': Register.COLLECT_INTERVAL},
0x000c96a8: Register.POWER_ON_TIME, 0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
0x000d0020: Register.COLLECT_INTERVAL, 0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
0x000cf850: Register.DATA_UP_INTERVAL, 0x00000191: {'reg': Register.EVENT_401},
0x000c7f38: Register.COMMUNICATION_TYPE, 0x00000192: {'reg': Register.EVENT_402},
0x00000191: Register.EVENT_401, 0x00000193: {'reg': Register.EVENT_403},
0x00000192: Register.EVENT_402, 0x00000194: {'reg': Register.EVENT_404},
0x00000193: Register.EVENT_403, 0x00000195: {'reg': Register.EVENT_405},
0x00000194: Register.EVENT_404, 0x00000196: {'reg': Register.EVENT_406},
0x00000195: Register.EVENT_405, 0x00000197: {'reg': Register.EVENT_407},
0x00000196: Register.EVENT_406, 0x00000198: {'reg': Register.EVENT_408},
0x00000197: Register.EVENT_407, 0x00000199: {'reg': Register.EVENT_409},
0x00000198: Register.EVENT_408, 0x0000019a: {'reg': Register.EVENT_410},
0x00000199: Register.EVENT_409, 0x0000019b: {'reg': Register.EVENT_411},
0x0000019a: Register.EVENT_410, 0x0000019c: {'reg': Register.EVENT_412},
0x0000019b: Register.EVENT_411, 0x0000019d: {'reg': Register.EVENT_413},
0x0000019c: Register.EVENT_412, 0x0000019e: {'reg': Register.EVENT_414},
0x0000019d: Register.EVENT_413, 0x0000019f: {'reg': Register.EVENT_415},
0x0000019e: Register.EVENT_414, 0x000001a0: {'reg': Register.EVENT_416},
0x0000019f: Register.EVENT_415, 0x00000064: {'reg': Register.INVERTER_STATUS},
0x000001a0: Register.EVENT_416, 0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
} }
@@ -103,7 +104,8 @@ class InfosG3(Infos):
entity strings entity strings
sug_area:str ==> suggested area string from the config file''' sug_area:str ==> suggested area string from the config file'''
# iterate over RegisterMap.map and get the register values # 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 res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
if res: if res:
yield res yield res
@@ -122,9 +124,11 @@ class InfosG3(Infos):
result = struct.unpack_from('!lB', buf, ind) result = struct.unpack_from('!lB', buf, ind)
addr = result[0] addr = result[0]
if addr not in RegisterMap.map: if addr not in RegisterMap.map:
row = None
info_id = -1 info_id = -1
else: else:
info_id = RegisterMap.map[addr] row = RegisterMap.map[addr]
info_id = row['reg']
data_type = result[1] data_type = result[1]
ind += 5 ind += 5
@@ -170,9 +174,19 @@ class InfosG3(Infos):
" not supported") " not supported")
return return
result = self.__modify_val(row, result)
yield from self.__store_result(addr, result, info_id, node_id) yield from self.__store_result(addr, result, info_id, node_id)
i += 1 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): def __store_result(self, addr, result, info_id, node_id):
keys, level, unit, must_incr = self._key_obj(info_id) keys, level, unit, must_incr = self._key_obj(info_id)
if keys: if keys:

View File

@@ -3,11 +3,18 @@ import traceback
import json import json
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from config import Config
from inverter import Inverter
from gen3.connection_g3 import ConnectionG3
from aiomqtt import MqttCodeError 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') logger_mqtt = logging.getLogger('mqtt')

View File

@@ -56,20 +56,24 @@ class Talent(Message):
0x00: self.msg_contact_info, 0x00: self.msg_contact_info,
0x13: self.msg_ota_update, 0x13: self.msg_ota_update,
0x22: self.msg_get_time, 0x22: self.msg_get_time,
0x99: self.msg_act_time,
0x71: self.msg_collector_data, 0x71: self.msg_collector_data,
# 0x76: # 0x76:
0x77: self.msg_modbus, 0x77: self.msg_modbus,
# 0x78: # 0x78:
0x87: self.msg_modbus2,
0x04: self.msg_inverter_data, 0x04: self.msg_inverter_data,
} }
self.log_lvl = { self.log_lvl = {
0x00: logging.INFO, 0x00: logging.INFO,
0x13: logging.INFO, 0x13: logging.INFO,
0x22: logging.INFO, 0x22: logging.INFO,
0x99: logging.INFO,
0x71: logging.INFO, 0x71: logging.INFO,
# 0x76: # 0x76:
0x77: self.get_modbus_log_lvl, 0x77: self.get_modbus_log_lvl,
# 0x78: # 0x78:
0x87: self.get_modbus_log_lvl,
0x04: logging.INFO, 0x04: logging.INFO,
} }
self.modbus_elms = 0 # for unit tests self.modbus_elms = 0 # for unit tests
@@ -127,6 +131,7 @@ class Talent(Message):
logger.debug(f'SerialNo {serial_no} not known but accepted!') logger.debug(f'SerialNo {serial_no} not known but accepted!')
self.unique_id = serial_no self.unique_id = serial_no
self.db.set_db_def_value(Register.COLLECTOR_SNR, serial_no)
def read(self) -> float: def read(self) -> float:
'''process all received messages in the _recv_buffer''' '''process all received messages in the _recv_buffer'''
@@ -170,6 +175,25 @@ class Talent(Message):
logger.info(self.__flow_str(self.server_side, 'forwrd') + logger.info(self.__flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') 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): def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
if self.state != State.up: if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,' logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
@@ -400,6 +424,8 @@ class Talent(Message):
result = struct.unpack_from('!q', self._recv_buffer, result = struct.unpack_from('!q', self._recv_buffer,
self.header_len) self.header_len)
self.ts_offset = result[0]-ts 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}' logger.debug(f'tsun-time: {int(result[0]):08x}'
f' proxy-time: {ts:08x}' f' proxy-time: {ts:08x}'
f' offset: {self.ts_offset}') f' offset: {self.ts_offset}')
@@ -410,6 +436,41 @@ class Talent(Message):
self.forward() 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): def parse_msg_header(self):
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len) result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
@@ -492,6 +553,15 @@ class Talent(Message):
modbus_len = result[1] modbus_len = result[1]
return msg_hdr_len, modbus_len 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: def get_modbus_log_lvl(self) -> int:
if self.ctrl.is_req(): if self.ctrl.is_req():
return logging.INFO return logging.INFO
@@ -501,6 +571,13 @@ class Talent(Message):
def msg_modbus(self): def msg_modbus(self):
hdr_len, _ = self.parse_modbus_header() 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: data = self._recv_buffer[self.header_len:
self.header_len+self.data_len] self.header_len+self.data_len]

View File

@@ -19,6 +19,7 @@ class RegisterMap:
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501 0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501 0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501 0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!BBBBBB', 'eval': '"%02x:%02x:%02x:%02x:%02x:%02x" % res'}, # noqa: E501
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501 0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501 0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501 0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
@@ -59,7 +60,7 @@ class RegisterMap:
0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0xffffff01: {'reg': Register.OUTPUT_COEFFICIENT}, 0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
0xffffff02: {'reg': Register.POLLING_INTERVAL}, 0xffffff02: {'reg': Register.POLLING_INTERVAL},
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501 # 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501

View File

@@ -203,13 +203,15 @@ class SolarmanV5(Message):
inverters = Config.get('inverters') inverters = Config.get('inverters')
# logger.debug(f'Inverters: {inverters}') # logger.debug(f'Inverters: {inverters}')
for inv in inverters.values(): for key, inv in inverters.items():
# logger.debug(f'key: {key} -> {inv}') # logger.debug(f'key: {key} -> {inv}')
if (type(inv) is dict and 'monitor_sn' in inv if (type(inv) is dict and 'monitor_sn' in inv
and inv['monitor_sn'] == snr): and inv['monitor_sn'] == snr):
self.__set_config_parms(inv) self.__set_config_parms(inv)
self.db.set_pv_module_details(inv) self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
break break
else: else:
self.node_id = '' self.node_id = ''

View File

@@ -16,6 +16,8 @@ class Register(Enum):
CHIP_MODEL = 3 CHIP_MODEL = 3
TRACE_URL = 4 TRACE_URL = 4
LOGGER_URL = 5 LOGGER_URL = 5
MAC_ADDR = 6
COLLECTOR_SNR = 7
PRODUCT_NAME = 20 PRODUCT_NAME = 20
MANUFACTURER = 21 MANUFACTURER = 21
VERSION = 22 VERSION = 22
@@ -188,8 +190,8 @@ class Infos:
__info_devs = { __info_devs = {
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501 '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 '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}, # 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_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_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 '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.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.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.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: # inverter values used for device registration:
Register.PRODUCT_NAME: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 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['name'] = device['name']+' - '+sug_area
dev['sa'] = device['name']+' - '+sug_area dev['sa'] = device['name']+' - '+sug_area
self.__add_via_dev(dev, device, key, snr) 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 # values fpr 'modell', 'manufacturer', 'sw version' and
# 'hw version' # 'hw version'
if key in device: if key in device:
@@ -518,8 +523,17 @@ class Infos:
dev['ids'] = [f"{ha['dev']}"] dev['ids'] = [f"{ha['dev']}"]
else: else:
dev['ids'] = [f"{ha['dev']}_{snr}"] dev['ids'] = [f"{ha['dev']}_{snr}"]
self.__add_connection(dev, device)
return dev 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): def __add_via_dev(self, dev, device, key, snr):
if 'via' in device: # add the link to the parent device if 'via' in device: # add the link to the parent device
via = device['via'] via = device['via']

View File

@@ -30,7 +30,33 @@ def test_default_config():
validated = Config.conf_schema.validate(cnf) validated = Config.conf_schema.validate(cnf)
except Exception: except Exception:
assert False 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(): def test_full_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -71,7 +97,37 @@ def test_read_empty():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() 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') defcnf = TstConfig.def_config.get('solarman')
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000} assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
@@ -93,7 +149,37 @@ def test_read_cnf1():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() 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') cnf = TstConfig.get('solarman')
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000} assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
defcnf = TstConfig.def_config.get('solarman') defcnf = TstConfig.def_config.get('solarman')
@@ -106,7 +192,37 @@ def test_read_cnf2():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() 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') assert True == TstConfig.is_default('solarman')
def test_read_cnf3(): def test_read_cnf3():
@@ -123,7 +239,37 @@ def test_read_cnf4():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() 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') assert False == TstConfig.is_default('solarman')
def test_read_cnf5(): def test_read_cnf5():

View File

@@ -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()

View File

@@ -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()

View File

@@ -1,7 +1,7 @@
# test_with_pytest.py # test_with_pytest.py
import pytest, json import pytest, json, math
from app.src.infos import Register, ClrAtMidnight from app.src.infos import Register
from app.src.gen3.infos_g3 import InfosG3 from app.src.gen3.infos_g3 import InfosG3, RegisterMap
@pytest.fixture @pytest.fixture
def contr_data_seq(): # Get Time Request message 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': if id == 'out_power_123':
assert comp == 'sensor' 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 tests +=1
if id == 'daily_gen_123': if id == 'daily_gen_123':
assert comp == 'sensor' 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 tests +=1
elif id == 'power_pv1_123': 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 tests +=1
assert tests==5 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): def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3() i = InfosG3()
tests = 0 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': if key == 'total' or key == 'inverter' or key == 'env':
assert update == True assert update == True
tests +=1 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['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['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 tests = 0
for key, update in i.parse (inv_data_seq2): for key, update in i.parse (inv_data_seq2):
if key == 'total' or key == 'env': if key == 'total' or key == 'env':
assert update == False assert update == False
tests +=1 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['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['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})
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2}) assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "Max_Designed_Power": -1, "Output_Coefficient": 100.0, "No_Inputs": 2})
tests = 0 tests = 0
for key, update in i.parse (inv_data_seq2_zero): 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 assert update == False
tests +=1 tests +=1
elif key == 'env': elif key == 'env':
assert update == True
tests +=1 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['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['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): def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3() i = InfosG3()
@@ -436,10 +461,10 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert update == True assert update == True
tests +=1 tests +=1
assert tests==3 assert tests==4
assert json.dumps(i.db['total']) == json.dumps({}) 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['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 tests = 0
for key, update in i.parse (inv_data_seq2_zero): 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 assert update == False
tests +=1 tests +=1
assert tests==3 assert tests==4
assert json.dumps(i.db['total']) == json.dumps({}) 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['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 tests = 0
for key, update in i.parse (inv_data_seq2): for key, update in i.parse (inv_data_seq2):
if key == 'total' or key == 'env': if key == 'total' or key == 'env':
assert update == True
tests +=1 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['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['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 val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
assert val == 1 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

View File

@@ -86,7 +86,7 @@ def test_parse_4110(str_test_ip, device_data: bytes):
assert json.dumps(i.db) == json.dumps({ 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"}, '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): 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({ assert json.dumps(i.db) == json.dumps({
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051}, "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}, "env": {"Inverter_Status": 1, "Inverter_Temp": 14},
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "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}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},

View File

@@ -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()

View File

@@ -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()

View File

@@ -1,10 +1,10 @@
# test_with_pytest.py # test_with_pytest.py
import pytest import pytest
import asyncio import asyncio
from aiomqtt import MqttCodeError
from mock import patch from mock import patch
from enum import Enum from enum import Enum
from enum import Enum
from app.src.singleton import Singleton from app.src.singleton import Singleton
from app.src.config import Config from app.src.config import Config
from app.src.infos import Infos from app.src.infos import Infos
@@ -134,10 +134,20 @@ def patch_no_mqtt():
yield conn yield conn
@pytest.fixture @pytest.fixture
def patch_no_mqtt(): def patch_mqtt_err():
with patch.object(Mqtt, 'publish') as conn: def new_publish(self, key, data):
raise MqttCodeError(None)
with patch.object(Mqtt, 'publish', new_publish) as conn:
yield 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 @pytest.mark.asyncio
async def test_modbus_conn(patch_open): 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 assert 1 == test
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0 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 @pytest.mark.asyncio
async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open): 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 assert 2 == test
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0 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

View File

@@ -184,6 +184,35 @@ def device_rsp_msg(): # 0x1110
msg += b'\x15' msg += b'\x15'
return msg 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 @pytest.fixture
def invalid_start_byte(): # 0x4110 def invalid_start_byte(): # 0x4110
msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' 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'' assert m._send_buffer==b''
m.close() 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): def test_unkown_message(config_tsun_inv1, unknown_msg):
_ = config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(unknown_msg, (0,)) m = MemoryStream(unknown_msg, (0,))

View File

@@ -41,6 +41,7 @@ class MemoryStream(Talent):
self.send_msg_ofs = 0 self.send_msg_ofs = 0
self.test_exception_async_write = False self.test_exception_async_write = False
self.msg_recvd = [] self.msg_recvd = []
self.remote_stream = None
def append_msg(self, msg): def append_msg(self, msg):
self.__msg += 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 def msg_time_invalid(): # Get Time Request message
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22' 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 @pytest.fixture
def msg_controller_ind(): # Data indication from the controller def msg_controller_ind(): # Data indication from the controller
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001' 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' msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
return msg 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 @pytest.fixture
def broken_recv_buf(): # There are two message in the buffer, but the second has overwritten the first partly 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' 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): def test_msg_get_time(config_tsun_inv1, msg_get_time):
_ = config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_get_time, (0,)) m = MemoryStream(msg_get_time, (0,))
m.state = State.up
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed 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.header_len==23
assert m.ts_offset==0 assert m.ts_offset==0
assert m.data_len==0 assert m.data_len==0
assert m.state==State.pend
assert m._forward_buffer==msg_get_time 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._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 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): def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
_ = config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(msg_get_time, (0,)) m = MemoryStream(msg_get_time, (0,))
m.state = State.received
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed 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.header_len==23
assert m.ts_offset==0 assert m.ts_offset==0
assert m.data_len==0 assert m.data_len==0
assert m.state==State.received
assert m._forward_buffer==b'' 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._send_buffer==bytearray(b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00')
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close() m.close()
def test_msg_time_resp(config_tsun_inv1, msg_time_rsp): 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 _ = config_tsun_inv1
m = MemoryStream(msg_time_rsp, (0,), False) 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.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed 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.msg_id==34
assert m.header_len==23 assert m.header_len==23
assert m.ts_offset==3600000 assert m.ts_offset==3600000
assert s.ts_offset==3600000
assert m.data_len==8 assert m.data_len==8
assert m._forward_buffer==b'' assert m._forward_buffer==b''
assert m._send_buffer==b'' assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.remote_stream = None
s.close()
m.close() m.close()
def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp): 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 assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close() 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): def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack):
_ = config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_controller_ind, (0,)) 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) m._update_header(m._forward_buffer)
assert m._forward_buffer==msg_inverter_ind_0w assert m._forward_buffer==msg_inverter_ind_0w
assert m._send_buffer==msg_inverter_ack 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) assert isclose(m.db.db['grid']['Output_Power'], 0.5)
m.close() m.close()
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0 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.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp20 assert m._forward_buffer==msg_modbus_rsp20
assert m._send_buffer==b'' 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.VERSION) == 'V5.1.09'
assert m.db.get_db_value(Register.TS_GRID) == m._utc() assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True 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.msg_count == 2
assert m._forward_buffer==msg_modbus_rsp21 assert m._forward_buffer==msg_modbus_rsp21
assert m._send_buffer==b'' 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.VERSION) == 'V5.1.0E'
assert m.db.get_db_value(Register.TS_GRID) == m._utc() assert m.db.get_db_value(Register.TS_GRID) == m._utc()
assert m.new_data['inverter'] == True assert m.new_data['inverter'] == True
m.close() 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): def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
_ = config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_inv, (0,), False) m = MemoryStream(msg_modbus_inv, (0,), False)