Compare commits
73 Commits
v0.8.0-rc.
...
v0.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ffcc0949 | ||
|
|
85fe7261d5 | ||
|
|
d4b618742c | ||
|
|
719c6f703a | ||
|
|
62ea2a9e6f | ||
|
|
166a856705 | ||
|
|
bfea38d9da | ||
|
|
d5ec47fd1e | ||
|
|
828f26cf24 | ||
|
|
0b3d84ff36 | ||
|
|
5642c912a8 | ||
|
|
614acbf32d | ||
|
|
57525ca519 | ||
|
|
5ef68280b1 | ||
|
|
e12c78212f | ||
|
|
2ab35a8257 | ||
|
|
865216b8d9 | ||
|
|
5d5d7c218f | ||
|
|
be4c6ac77f | ||
|
|
a9dc7e6847 | ||
|
|
270732f1d0 | ||
|
|
7b4fabdc25 | ||
|
|
2351ec314a | ||
|
|
604d30c711 | ||
|
|
ab5256659b | ||
|
|
a76c0ac440 | ||
|
|
215dcd98e6 | ||
|
|
627ca97360 | ||
|
|
d2b88ab838 | ||
|
|
6d9addc7d5 | ||
|
|
1bb08fb211 | ||
|
|
193eea65af | ||
|
|
2b8dacb0de | ||
|
|
cb0c69944f | ||
|
|
7f41365815 | ||
|
|
5db3fbf495 | ||
|
|
d44726c0f3 | ||
|
|
1985557bce | ||
|
|
7dc2595d71 | ||
|
|
6d9a446bfe | ||
|
|
f9c1b83ccd | ||
|
|
58b42f7d7c | ||
|
|
27045cac6e | ||
|
|
54de2aecfe | ||
|
|
5a39370cc3 | ||
|
|
7a9b23d068 | ||
|
|
e34afcb523 | ||
|
|
22df381da5 | ||
|
|
117e6a7570 | ||
|
|
65de946992 | ||
|
|
33d385db10 | ||
|
|
1e610af1df | ||
|
|
db1169f61f | ||
|
|
383be10e87 | ||
|
|
b364fb3f8e | ||
|
|
a42ba8a8c6 | ||
|
|
f3e69ff217 | ||
|
|
a3c054d2b1 | ||
|
|
c34b33ed5f | ||
|
|
0a18918326 | ||
|
|
aa3bb4a1fa | ||
|
|
a62864218d | ||
|
|
0b2631c162 | ||
|
|
c59bd16664 | ||
|
|
039a021cda | ||
|
|
49e2dfbd86 | ||
|
|
e6ecf5911b | ||
|
|
6e1ed5d1e7 | ||
|
|
ad885e9644 | ||
|
|
8f81ceda98 | ||
|
|
8204cae2b1 | ||
|
|
8baa68e615 | ||
|
|
7b4ed406a1 |
@@ -1,2 +1,3 @@
|
||||
[run]
|
||||
branch = True
|
||||
branch = True
|
||||
relative_files = True
|
||||
42
.github/workflows/python-app.yml
vendored
42
.github/workflows/python-app.yml
vendored
@@ -5,7 +5,7 @@ name: Python application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev-*" ]
|
||||
branches: [ "main", "dev-*", "*/issue*" ]
|
||||
paths-ignore:
|
||||
- '**.md' # Do no build on *.md changes
|
||||
- '**.yml' # Do no build on *.yml changes
|
||||
@@ -18,10 +18,11 @@ on:
|
||||
- '**.dockerfile' # Do no build on *.dockerfile changes
|
||||
- '**.sh' # Do no build on *.sh changes
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: [ "main", "dev-*" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -29,7 +30,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set timezone
|
||||
uses: szenius/set-timezone@v2.0
|
||||
with:
|
||||
timezoneLinux: "Europe/Berlin"
|
||||
timezoneMacos: "Europe/Berlin"
|
||||
timezoneWindows: "Europe/Berlin"
|
||||
# - name: Start Mosquitto
|
||||
# uses: namoshek/mosquitto-github-action@v1
|
||||
# with:
|
||||
# version: '1.6'
|
||||
# ports: '1883:1883 8883:8883'
|
||||
# certificates: ${{ github.workspace }}/.ci/tls-certificates
|
||||
# config: ${{ github.workspace }}/.ci/mosquitto.conf
|
||||
# password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
|
||||
# container-name: 'mqtt'
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -37,14 +55,28 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest pytest-asyncio
|
||||
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
python -m pytest app
|
||||
python -m pytest app --cov=app/src --cov-report=xml
|
||||
coverage report
|
||||
- name: Analyze with SonarCloud
|
||||
uses: SonarSource/sonarcloud-github-action@v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
projectBaseDir: .
|
||||
args:
|
||||
-Dsonar.projectKey=s-allius_tsun-gen3-proxy
|
||||
-Dsonar.python.coverage.reportPaths=coverage.xml
|
||||
-Dsonar.python.flake8.reportPaths=output_flake.txt
|
||||
# -Dsonar.docker.hadolint.reportPaths=
|
||||
|
||||
4
.sonarlint/connectedMode.json
Normal file
4
.sonarlint/connectedMode.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sonarCloudOrganization": "s-allius",
|
||||
"projectKey": "s-allius_tsun-gen3-proxy"
|
||||
}
|
||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Aktuelle Datei",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -11,5 +11,12 @@
|
||||
"python.testing.pytestEnabled": true,
|
||||
"flake8.args": [
|
||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
||||
]
|
||||
],
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "s-allius",
|
||||
"projectKey": "s-allius_tsun-gen3-proxy"
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/*.pyi": true
|
||||
}
|
||||
}
|
||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -5,15 +5,89 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [unreleased]
|
||||
|
||||
## [0.11.0] - 2024-10-13
|
||||
|
||||
- fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196)
|
||||
- refactoring: cleaner architecture, increase test coverage
|
||||
- Parse more values in Server Mode [#186](https://github.com/s-allius/tsun-gen3-proxy/issues/186)
|
||||
- GEN3: add support for new messages of version 3 firmwares [#182](https://github.com/s-allius/tsun-gen3-proxy/issues/182)
|
||||
- add support for controller MAC and serial number
|
||||
- GEN3: don't crash on overwritten msg in the receive buffer
|
||||
- Reading the version string from the image updates it even if the image is re-pulled without re-deployment
|
||||
|
||||
## [0.10.1] - 2024-08-10
|
||||
|
||||
- fix displaying the version string at startup and in HA [#153](https://github.com/s-allius/tsun-gen3-proxy/issues/153)
|
||||
|
||||
## [0.10.0] - 2024-08-09
|
||||
|
||||
- bump aiohttp to version 3.10.2
|
||||
- add SonarQube and code coverage support
|
||||
- don't send MODBUS request when state is note up; adapt timeouts [#141](https://github.com/s-allius/tsun-gen3-proxy/issues/141)
|
||||
- build multi arch images with sboms [#144](https://github.com/s-allius/tsun-gen3-proxy/issues/144)
|
||||
- add timestamp to MQTT topics [#138](https://github.com/s-allius/tsun-gen3-proxy/issues/138)
|
||||
- improve the message handling, to avoid hangs
|
||||
- GEN3: allow long timeouts until we received first inverter data (not only device data)
|
||||
- bump aiomqtt to version 2.2.0
|
||||
- bump schema to version 0.7.7
|
||||
- Home Assistant: improve inverter status value texts
|
||||
- GEN3: add inverter status
|
||||
- fix flapping registers [#128](https://github.com/s-allius/tsun-gen3-proxy/issues/128)
|
||||
- register OUTPUT_COEFFICIENT at HA
|
||||
- GEN3: INVERTER_STATUS,
|
||||
- add config option to disable the MODBUS polling [#120](https://github.com/s-allius/tsun-gen3-proxy/issues/120)
|
||||
- make the maximum output coefficient configurable [#123](https://github.com/s-allius/tsun-gen3-proxy/issues/123)
|
||||
- cleanup shutdown
|
||||
- add preview build
|
||||
- MODBUS: the last digit of the inverter version is a hexadecimal number [#119](https://github.com/s-allius/tsun-gen3-proxy/issues/119)
|
||||
- GEN3PLUS: add client_mode connection on port 8899 [#117](https://github.com/s-allius/tsun-gen3-proxy/issues/117)
|
||||
|
||||
## [0.9.0] - 2024-07-01
|
||||
|
||||
- fix exception in MODBUS timeout callback
|
||||
|
||||
## [0.9.0-RC1] - 2024-06-29
|
||||
|
||||
- add asyncio log and debug mode
|
||||
- stop the HTTP server on shutdown gracefully
|
||||
- Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to
|
||||
unexpected packets. [#111](https://github.com/s-allius/tsun-gen3-proxy/issues/111)
|
||||
- GEN3: avoid sending MODBUS commands to the inverter during the inverter's reporting phase
|
||||
- GEN3: determine the connection timeout based on the connection state
|
||||
- GEN3: support more data encodings for DSP version V5.0.17 [#108](https://github.com/s-allius/tsun-gen3-proxy/issues/108)
|
||||
- detect dead connections [#100](https://github.com/s-allius/tsun-gen3-proxy/issues/100)
|
||||
- improve connection logging wirt a unique connection id
|
||||
- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91)
|
||||
- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93)
|
||||
- add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94)
|
||||
- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)
|
||||
- GEN3PLUS: dump invalid packages with wrong start or stop byte
|
||||
- label debug imagages als `debug`
|
||||
- print imgae build time during proxy start
|
||||
- add type annotations
|
||||
- improve async unit test and fix pytest warnings
|
||||
- run github tests even for pulls on issue branches
|
||||
|
||||
## [0.8.1] - 2024-06-21
|
||||
|
||||
- Fix MODBUS responses are dropped and not forwarded to the TSUN cloud [#104](https://github.com/s-allius/tsun-gen3-proxy/issues/104)
|
||||
- GEN3: Fix connections losts due MODBUS requests [#102](https://github.com/s-allius/tsun-gen3-proxy/issues/102)
|
||||
|
||||
## [0.8.0] - 2024-06-07
|
||||
|
||||
- improve logging: add protocol or node_id to connection logs
|
||||
- improve logging: log ignored AT+ or MODBUS commands
|
||||
- improve tracelog: log level depends on message type and source
|
||||
- fix typo in docker-compose.yaml and remove the external network definition
|
||||
- trace heartbeat and regular modbus pakets witl log level DEBUG
|
||||
- GEN3PLUS: don't forward ack paket from tsun to the inverter
|
||||
- add allow and block filter for AT+ commands
|
||||
- GEN3PLUS: add allow and block filter for AT+ commands
|
||||
- catch all OSError errors in the read loop
|
||||
- log Modbus traces with different log levels
|
||||
- add Modbus fifo and timeout handler
|
||||
- build version string in the same format as TSUN for GEN3 invterts
|
||||
- build version string in the same format as TSUN for GEN3 inverters
|
||||
- add graceful shutdown
|
||||
- parse Modbus values and store them in the database
|
||||
- add cron task to request the output power every minute
|
||||
@@ -24,7 +98,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- change logging level to DEBUG for some logs
|
||||
- remove experimental value Register.VALUE_1
|
||||
- format Register.POWER_ON_TIME as integer
|
||||
- ignore non realtime values for now
|
||||
- ignore catch-up values from the inverters for now
|
||||
|
||||
## [0.7.0] - 2024-04-20
|
||||
|
||||
|
||||
213
README.md
213
README.md
@@ -7,9 +7,15 @@
|
||||
<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://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.0.1-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://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>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=alert_status"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=alert_status"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=bugs"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=bugs"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=code_smells"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=code_smells"></a>
|
||||
<br>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=coverage"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
|
||||
</p>
|
||||
|
||||
# Overview
|
||||
@@ -115,26 +121,63 @@ The proxy can be configured via the file 'config.toml'. When the proxy is starte
|
||||
The configration uses the TOML format, which aims to be easy to read due to obvious semantics.
|
||||
You find more details here: <https://toml.io/en/v1.0.0>
|
||||
|
||||
<details>
|
||||
<summary>Here is an example of a <b>config.toml</b> file</summary>
|
||||
|
||||
```toml
|
||||
# configuration for tsun cloud for 'GEN3' inverters
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
tsun.host = 'logger.talent-monitoring.com'
|
||||
tsun.port = 5005
|
||||
|
||||
# configuration for solarman cloud for 'GEN3 PLUS' inverters
|
||||
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
solarman.host = 'iot.talent-monitoring.com'
|
||||
solarman.port = 10000
|
||||
##########################################################################################
|
||||
###
|
||||
### T S U N - G E N 3 - P R O X Y
|
||||
###
|
||||
### from Stefan Allius
|
||||
###
|
||||
##########################################################################################
|
||||
###
|
||||
### The readme will give you an overview of the project:
|
||||
### https://s-allius.github.io/tsun-gen3-proxy/
|
||||
###
|
||||
### The proxy supports different operation modes. Select the proper mode
|
||||
### which depends on your inverter type and you inverter firmware.
|
||||
### Please read:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
|
||||
###
|
||||
### Here you will find a description of all configuration options:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
|
||||
###
|
||||
### The configration uses the TOML format, which aims to be easy to read due to
|
||||
### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
|
||||
###
|
||||
##########################################################################################
|
||||
|
||||
|
||||
# mqtt broker configuration
|
||||
##########################################################################################
|
||||
##
|
||||
## MQTT broker configuration
|
||||
##
|
||||
## In this block, you must configure the connection to your MQTT broker and specify the
|
||||
## required credentials. As the proxy does not currently support an encrypted connection
|
||||
## to the MQTT broker, it is strongly recommended that you do not use a public broker.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
|
||||
##
|
||||
|
||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||
mqtt.port = 1883
|
||||
mqtt.user = ''
|
||||
mqtt.passwd = ''
|
||||
|
||||
|
||||
# home-assistant
|
||||
##########################################################################################
|
||||
##
|
||||
## HOME ASSISTANT
|
||||
##
|
||||
## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
|
||||
## values match the HA default configuration. If you need to change these or want to use
|
||||
## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
|
||||
##
|
||||
|
||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
|
||||
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
|
||||
@@ -142,34 +185,115 @@ ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_i
|
||||
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
|
||||
|
||||
|
||||
# microinverters
|
||||
inverters.allow_all = false # True: allow inverters, even if we have no inverter mapping
|
||||
##########################################################################################
|
||||
##
|
||||
## GEN3 Proxy Mode Configuration
|
||||
##
|
||||
## In this block, you can configure an optional connection to the TSUN cloud for GEN3
|
||||
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||
## to use the TSUN APPs or receive firmware updates.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
|
||||
##
|
||||
|
||||
# inverter mapping, maps a `serial_no* to a `node_id` and defines an optional `suggested_area` for `home-assistant`
|
||||
#
|
||||
# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
tsun.host = 'logger.talent-monitoring.com'
|
||||
tsun.port = 5005
|
||||
|
||||
|
||||
##########################################################################################
|
||||
##
|
||||
## GEN3PLUS Proxy Mode Configuration
|
||||
##
|
||||
## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
|
||||
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||
## to use the TSUN APPs or receive firmware updates.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
|
||||
##
|
||||
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
solarman.host = 'iot.talent-monitoring.com'
|
||||
solarman.port = 10000
|
||||
|
||||
|
||||
##########################################################################################
|
||||
###
|
||||
### Inverter Definitions
|
||||
###
|
||||
### The proxy supports the simultaneous operation of several inverters, even of different
|
||||
### types. A configuration block must be defined for each inverter, in which all necessary
|
||||
### parameters must be specified. These depend on the operation mode used and also differ
|
||||
### slightly depending on the inverter type.
|
||||
###
|
||||
### In addition, the PV modules can be defined at the individual inputs for documentation
|
||||
### purposes, whereby these are displayed in Home Assistant.
|
||||
###
|
||||
### The proxy only accepts connections from known inverters. This can be switched off for
|
||||
### test purposes and unknown serial numbers are also accepted.
|
||||
###
|
||||
|
||||
inverters.allow_all = false # only allow known inverters
|
||||
|
||||
|
||||
##########################################################################################
|
||||
##
|
||||
## For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
## definition. To do this, the corresponding configuration block is started with
|
||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
|
||||
## in the configuration block
|
||||
##
|
||||
## The serial numbers of all GEN3 inverters start with `R17`!
|
||||
##
|
||||
|
||||
[inverters."R17xxxxxxxxxxxx1"]
|
||||
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
|
||||
suggested_area = 'roof' # Optional, suggested installation area for home-assistant
|
||||
node_id = 'inv_1' # MQTT replacement for inverters serial number
|
||||
suggested_area = 'roof' # suggested installation place for home-assistant
|
||||
modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
|
||||
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
[inverters."R17xxxxxxxxxxxx2"]
|
||||
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
||||
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
||||
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
[inverters."Y17xxxxxxxxxxxx1"]
|
||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||
##########################################################################################
|
||||
##
|
||||
## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
## definition. To do this, the corresponding configuration block is started with
|
||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
|
||||
## can be set in the configuration block
|
||||
##
|
||||
## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
|
||||
## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
|
||||
## with the inverter.
|
||||
##
|
||||
|
||||
[inverters."Y17xxxxxxxxxxxx1"] # This block is also for inverters with a Y47 serial no
|
||||
monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
|
||||
node_id = 'inv_2' # MQTT replacement for inverters serial number
|
||||
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||
modbus_polling = true # Enable optional MODBUS polling
|
||||
|
||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||
# the next line and configure the fixed IP of your inverter
|
||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
|
||||
##########################################################################################
|
||||
###
|
||||
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via
|
||||
### this connection or parameters (e.g. network credentials) can be queried. Filters can
|
||||
### then be configured for the AT+ commands from the TSUN Cloud so that only certain
|
||||
### accesses are permitted.
|
||||
###
|
||||
### An overview of all known AT+ commands can be found here:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
|
||||
###
|
||||
|
||||
[gen3plus.at_acl]
|
||||
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access
|
||||
tsun.block = []
|
||||
@@ -178,6 +302,8 @@ mqtt.block = []
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
@@ -188,8 +314,25 @@ The standard web interface of the inverter can be accessed at `http://<ip-adress
|
||||
|
||||
For our purpose, the hidden URL `http://<ip-adress>/config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port `10000` for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems.
|
||||
|
||||
```txt
|
||||
❗If the IP port is set to 10443 in the inverter configuration, you probably have a firmware with SSL support.
|
||||
In this case, you MUST NOT change the port or the host address, as this may cause the inverter to hang and
|
||||
require a complete reset. Use the configuration in client mode instead.
|
||||
```
|
||||
|
||||
|
||||
If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters.
|
||||
|
||||
## Client Mode (GEN3PLUS only)
|
||||
|
||||
Newer GEN3PLUS inverters support SSL encrypted connections over port 10443 to the TSUN cloud. In this case you can't loop the proxy into this connection, since the certicate verification of the inverter don't allow this. You can configure the proxy in client-mode to establish an unencrypted connection to the inverter. For this porpuse the inverter listen on port `8899`.
|
||||
|
||||
There are some requirements to be met:
|
||||
|
||||
- the inverter should have a fixed IP
|
||||
- the proxy must be able to reach the inverter. You must configure a corresponding route in your router if the inverter and the proxy are in different IP networks
|
||||
- add a 'client_mode' line to your config.toml file, to specify the inverter's ip address
|
||||
|
||||
## DNS Settings
|
||||
|
||||
### Loop the proxy into the connection
|
||||
@@ -226,11 +369,11 @@ In the following table you will find an overview of which inverter model has bee
|
||||
A combination with a red question mark should work, but I have not checked it in detail.
|
||||
|
||||
<table align="center">
|
||||
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 1.1.00.0B</th></tr>
|
||||
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">➖</td></tr>
|
||||
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">➖</td></tr>
|
||||
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center">➖</td><td align="center">➖</td><td align="center">➖</td><td align="center">✔️</td></tr>
|
||||
<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250, MS3000</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td></tr>
|
||||
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 4.0.10</th><th align="center">Fw. 4.0.20</th></tr>
|
||||
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">➖</td><td align="center">➖</td></tr>
|
||||
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">➖</td><td align="center">➖</td></tr>
|
||||
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center">➖</td><td align="center">➖</td><td align="center">➖</td><td align="center">✔️</td><td align="center">✔️</td></tr>
|
||||
<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250, MS3000</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td></tr>
|
||||
</table>
|
||||
|
||||
```txt
|
||||
@@ -241,7 +384,7 @@ Legend
|
||||
🚧: Proxy support in preparation
|
||||
```
|
||||
|
||||
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E`
|
||||
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` or `Y47E` instead of `R17E`
|
||||
|
||||
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN apk upgrade --no-cache && \
|
||||
|
||||
#
|
||||
# second stage for building wheels packages
|
||||
FROM base as builder
|
||||
FROM base AS builder
|
||||
|
||||
# copy the dependencies file to the root dir and install requirements
|
||||
COPY ./requirements.txt /root/
|
||||
@@ -26,7 +26,7 @@ RUN apk add --no-cache build-base && \
|
||||
|
||||
#
|
||||
# third stage for our runtime image
|
||||
FROM base as runtime
|
||||
FROM base AS runtime
|
||||
ARG SERVICE_NAME
|
||||
ARG VERSION
|
||||
ARG UID
|
||||
@@ -34,7 +34,6 @@ ARG GID
|
||||
ARG LOG_LVL
|
||||
ARG environment
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
ENV SERVICE_NAME=$SERVICE_NAME
|
||||
ENV UID=$UID
|
||||
ENV GID=$GID
|
||||
@@ -63,17 +62,10 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||
COPY config .
|
||||
COPY src .
|
||||
|
||||
EXPOSE 5005
|
||||
RUN echo ${VERSION} > /proxy-version.txt \
|
||||
&& date > /build-date.txt
|
||||
EXPOSE 5005 8127 10000
|
||||
|
||||
# command to run on container start
|
||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||
CMD [ "python3", "./server.py" ]
|
||||
|
||||
|
||||
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
|
||||
LABEL org.opencontainers.image.authors="Stefan Allius"
|
||||
LABEL org.opencontainers.image.source https://github.com/s-allius/tsun-gen3-proxy
|
||||
LABEL org.opencontainers.image.description 'This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
||||
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
|
||||
LABEL org.opencontainers.image.vendor="Stefan Allius"
|
||||
|
||||
45
app/build.sh
45
app/build.sh
@@ -4,7 +4,7 @@
|
||||
# rc: release candidate build
|
||||
# rel: release build and push to ghcr.io
|
||||
# Note: for release build, you need to set GHCR_TOKEN
|
||||
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .profile
|
||||
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .zprofile
|
||||
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||
|
||||
|
||||
@@ -18,35 +18,34 @@ arr=(${VERSION//./ })
|
||||
MAJOR=${arr[0]}
|
||||
IMAGE=tsun-gen3-proxy
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
||||
IMAGE=docker.io/sallius/${IMAGE}
|
||||
VERSION=${VERSION}-$1
|
||||
elif [[ $1 == rc ]] || [[ $1 == rel ]];then
|
||||
VERSION=${VERSION}+$1
|
||||
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
|
||||
IMAGE=ghcr.io/s-allius/${IMAGE}
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
else
|
||||
echo argument missing!
|
||||
echo try: $0 '[debug|dev|rc|rel]'
|
||||
echo try: $0 '[debug|dev|preview|rc|rel]'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export IMAGE
|
||||
export VERSION
|
||||
export BUILD_DATE
|
||||
export BRANCH
|
||||
export MAJOR
|
||||
|
||||
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||
if [[ $1 == debug ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||
elif [[ $1 == dev ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||
docker buildx bake -f app/docker-bake.hcl $1
|
||||
|
||||
elif [[ $1 == rc ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:rc
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
|
||||
elif [[ $1 == rel ]];then
|
||||
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
fi
|
||||
echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
|
||||
docker-compose config -q
|
||||
echo
|
||||
echo -e "${GREEN}${BUILD_DATE} => Version: ${VERSION}${NC} finished"
|
||||
echo
|
||||
|
||||
@@ -1,56 +1,177 @@
|
||||
# configuration to reach tsun cloud
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
tsun.host = 'logger.talent-monitoring.com'
|
||||
tsun.port = 5005
|
||||
##########################################################################################
|
||||
###
|
||||
### T S U N - G E N 3 - P R O X Y
|
||||
###
|
||||
### from Stefan Allius
|
||||
###
|
||||
##########################################################################################
|
||||
###
|
||||
### The readme will give you an overview of the project:
|
||||
### https://s-allius.github.io/tsun-gen3-proxy/
|
||||
###
|
||||
### The proxy supports different operation modes. Select the proper mode
|
||||
### which depends on your inverter type and you inverter firmware.
|
||||
### Please read:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
|
||||
###
|
||||
### Here you will find a description of all configuration options:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
|
||||
###
|
||||
### The configration uses the TOML format, which aims to be easy to read due to
|
||||
### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
|
||||
###
|
||||
##########################################################################################
|
||||
|
||||
# configuration to reach the new tsun cloud for G3 Plus inverters
|
||||
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
solarman.host = 'iot.talent-monitoring.com'
|
||||
solarman.port = 10000
|
||||
|
||||
# mqtt broker configuration
|
||||
##########################################################################################
|
||||
##
|
||||
## MQTT broker configuration
|
||||
##
|
||||
## In this block, you must configure the connection to your MQTT broker and specify the
|
||||
## required credentials. As the proxy does not currently support an encrypted connection
|
||||
## to the MQTT broker, it is strongly recommended that you do not use a public broker.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
|
||||
##
|
||||
|
||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||
mqtt.port = 1883
|
||||
mqtt.user = ''
|
||||
mqtt.passwd = ''
|
||||
|
||||
|
||||
# home-assistant
|
||||
##########################################################################################
|
||||
##
|
||||
## HOME ASSISTANT
|
||||
##
|
||||
## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
|
||||
## values match the HA default configuration. If you need to change these or want to use
|
||||
## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
|
||||
##
|
||||
|
||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
|
||||
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
|
||||
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
|
||||
|
||||
# microinverters
|
||||
inverters.allow_all = true # allow inverters, even if we have no inverter mapping
|
||||
|
||||
# inverter mapping, maps a `serial_no* to a `mqtt_id` and defines an optional `suggested_place` for `home-assistant`
|
||||
#
|
||||
# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
|
||||
##########################################################################################
|
||||
##
|
||||
## GEN3 Proxy Mode Configuration
|
||||
##
|
||||
## In this block, you can configure an optional connection to the TSUN cloud for GEN3
|
||||
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||
## to use the TSUN APPs or receive firmware updates.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
|
||||
##
|
||||
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
tsun.host = 'logger.talent-monitoring.com'
|
||||
tsun.port = 5005
|
||||
|
||||
|
||||
##########################################################################################
|
||||
##
|
||||
## GEN3PLUS Proxy Mode Configuration
|
||||
##
|
||||
## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
|
||||
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||
## to use the TSUN APPs or receive firmware updates.
|
||||
##
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
|
||||
##
|
||||
|
||||
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
solarman.host = 'iot.talent-monitoring.com'
|
||||
solarman.port = 10000
|
||||
|
||||
|
||||
##########################################################################################
|
||||
###
|
||||
### Inverter Definitions
|
||||
###
|
||||
### The proxy supports the simultaneous operation of several inverters, even of different
|
||||
### types. A configuration block must be defined for each inverter, in which all necessary
|
||||
### parameters must be specified. These depend on the operation mode used and also differ
|
||||
### slightly depending on the inverter type.
|
||||
###
|
||||
### In addition, the PV modules can be defined at the individual inputs for documentation
|
||||
### purposes, whereby these are displayed in Home Assistant.
|
||||
###
|
||||
### The proxy only accepts connections from known inverters. This can be switched off for
|
||||
### test purposes and unknown serial numbers are also accepted.
|
||||
###
|
||||
|
||||
inverters.allow_all = false # only allow known inverters
|
||||
|
||||
|
||||
##########################################################################################
|
||||
##
|
||||
## For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
## definition. To do this, the corresponding configuration block is started with
|
||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
|
||||
## in the configuration block
|
||||
##
|
||||
## The serial numbers of all GEN3 inverters start with `R17`!
|
||||
##
|
||||
|
||||
[inverters."R170000000000001"]
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
node_id = '' # MQTT replacement for inverters serial number
|
||||
suggested_area = '' # suggested installation area for home-assistant
|
||||
modbus_polling = false # Disable optional MODBUS polling
|
||||
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
#[inverters."R17xxxxxxxxxxxx2"]
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
##########################################################################################
|
||||
##
|
||||
## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
## definition. To do this, the corresponding configuration block is started with
|
||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
|
||||
## can be set in the configuration block
|
||||
##
|
||||
## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
|
||||
## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
|
||||
## with the inverter.
|
||||
##
|
||||
|
||||
[inverters."Y170000000000001"]
|
||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
|
||||
node_id = '' # MQTT replacement for inverters serial number
|
||||
suggested_area = '' # suggested installation place for home-assistant
|
||||
modbus_polling = true # Enable optional MODBUS polling
|
||||
|
||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||
# the next line and configure the fixed IP of your inverter
|
||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
|
||||
##########################################################################################
|
||||
###
|
||||
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via
|
||||
### this connection or parameters (e.g. network credentials) can be queried. Filters can
|
||||
### then be configured for the AT+ commands from the TSUN Cloud so that only certain
|
||||
### accesses are permitted.
|
||||
###
|
||||
### An overview of all known AT+ commands can be found here:
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
|
||||
###
|
||||
|
||||
[gen3plus.at_acl]
|
||||
# filter for received commands from the internet
|
||||
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
|
||||
tsun.block = []
|
||||
# filter for received commands from the MQTT broker
|
||||
mqtt.allow = ['AT+']
|
||||
mqtt.block = []
|
||||
|
||||
93
app/docker-bake.hcl
Normal file
93
app/docker-bake.hcl
Normal file
@@ -0,0 +1,93 @@
|
||||
variable "IMAGE" {
|
||||
default = "tsun-gen3-proxy"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = "0.0.0"
|
||||
}
|
||||
variable "MAJOR" {
|
||||
default = "0"
|
||||
}
|
||||
variable "BUILD_DATE" {
|
||||
default = "dev"
|
||||
}
|
||||
variable "BRANCH" {
|
||||
default = ""
|
||||
}
|
||||
variable "DESCRIPTION" {
|
||||
default = "This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations."
|
||||
}
|
||||
|
||||
target "_common" {
|
||||
context = "app"
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
environment = "production"
|
||||
}
|
||||
attest = [
|
||||
"type =provenance,mode=max",
|
||||
"type =sbom,generator=docker/scout-sbom-indexer:latest"
|
||||
]
|
||||
annotations = [
|
||||
"index:org.opencontainers.image.title=TSUN Gen3 Proxy",
|
||||
"index:org.opencontainers.image.authors=Stefan Allius",
|
||||
"index:org.opencontainers.image.created=${BUILD_DATE}",
|
||||
"index:org.opencontainers.image.version=${VERSION}",
|
||||
"index:org.opencontainers.image.revision=${BRANCH}",
|
||||
"index:org.opencontainers.image.description=${DESCRIPTION}",
|
||||
"index:org.opencontainers.image.licenses=BSD-3-Clause",
|
||||
"index:org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy"
|
||||
]
|
||||
labels = {
|
||||
"org.opencontainers.image.title" = "TSUN Gen3 Proxy"
|
||||
"org.opencontainers.image.authors" = "Stefan Allius"
|
||||
"org.opencontainers.image.created" = "${BUILD_DATE}"
|
||||
"org.opencontainers.image.version" = "${VERSION}"
|
||||
"org.opencontainers.image.revision" = "${BRANCH}"
|
||||
"org.opencontainers.image.description" = "${DESCRIPTION}"
|
||||
"org.opencontainers.image.licenses" = "BSD-3-Clause"
|
||||
"org.opencontainers.image.source" = "https://github.com/s-allius/tsun-gen3-proxy"
|
||||
}
|
||||
output = [
|
||||
"type=image,push=true"
|
||||
]
|
||||
|
||||
no-cache = false
|
||||
platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
|
||||
}
|
||||
|
||||
target "_debug" {
|
||||
args = {
|
||||
LOG_LVL = "DEBUG"
|
||||
environment = "dev"
|
||||
}
|
||||
}
|
||||
target "_prod" {
|
||||
args = {
|
||||
}
|
||||
}
|
||||
target "debug" {
|
||||
inherits = ["_common", "_debug"]
|
||||
tags = ["${IMAGE}:debug"]
|
||||
}
|
||||
|
||||
target "dev" {
|
||||
inherits = ["_common"]
|
||||
tags = ["${IMAGE}:dev"]
|
||||
}
|
||||
|
||||
target "preview" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:preview", "${IMAGE}:${VERSION}"]
|
||||
}
|
||||
|
||||
target "rc" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:rc", "${IMAGE}:${VERSION}"]
|
||||
}
|
||||
|
||||
target "rel" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:latest", "${IMAGE}:${MAJOR}", "${IMAGE}:${VERSION}"]
|
||||
no-cache = true
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
set -e
|
||||
|
||||
user="$(id -u)"
|
||||
export VERSION=$(cat /proxy-version.txt)
|
||||
|
||||
echo "######################################################"
|
||||
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
||||
echo "# for running with UserID:$UID, GroupID:$GID"
|
||||
echo "# Image built: $(cat /build-date.txt) "
|
||||
echo "#"
|
||||
|
||||
if [ "$user" = '0' ]; then
|
||||
|
||||
@@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \
|
||||
-name od -o \
|
||||
-name strings -o \
|
||||
-name su -o \
|
||||
-name wget -o \
|
||||
\) -delete \
|
||||
; fi
|
||||
|
||||
500
app/proxy.svg
500
app/proxy.svg
@@ -4,373 +4,257 @@
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="673pt" height="1216pt"
|
||||
viewBox="0.00 0.00 673.35 1216.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1212)">
|
||||
<svg width="626pt" height="966pt"
|
||||
viewBox="0.00 0.00 625.50 966.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 962)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1212 669.348,-1212 669.348,4 -4,4"/>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-962 621.5,-962 621.5,4 -4,4"/>
|
||||
<!-- A0 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1112 .1516,-1112 .1516,-1076 114.5444,-1076 114.5444,-1106 108.5444,-1112"/>
|
||||
<polyline fill="none" stroke="#000000" points="108.5444,-1112 108.5444,-1106 "/>
|
||||
<polyline fill="none" stroke="#000000" points="114.5444,-1106 108.5444,-1106 "/>
|
||||
<text text-anchor="middle" x="57.348" y="-1097" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||
<text text-anchor="middle" x="57.348" y="-1085" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="191.6964,-934 83.3036,-934 83.3036,-898 197.6964,-898 197.6964,-928 191.6964,-934"/>
|
||||
<polyline fill="none" stroke="#000000" points="191.6964,-934 191.6964,-928 "/>
|
||||
<polyline fill="none" stroke="#000000" points="197.6964,-928 191.6964,-928 "/>
|
||||
<text text-anchor="middle" x="140.5" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||
<text text-anchor="middle" x="140.5" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="639.0297,-816 569.6663,-816 569.6663,-780 639.0297,-780 639.0297,-816"/>
|
||||
<text text-anchor="middle" x="604.348" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-524 543.348,-556 665.348,-556 665.348,-524 543.348,-524"/>
|
||||
<text text-anchor="start" x="594.625" y="-537" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-468 543.348,-524 665.348,-524 665.348,-468 543.348,-468"/>
|
||||
<text text-anchor="start" x="561.8355" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||
<text text-anchor="start" x="569.6145" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||
<text text-anchor="start" x="553.2215" y="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-424 543.348,-468 665.348,-468 665.348,-424 543.348,-424"/>
|
||||
<text text-anchor="start" x="566.284" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||
<text text-anchor="start" x="570.4525" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||
</g>
|
||||
<!-- A1->A2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A1->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M604.348,-769.6429C604.348,-721.5141 604.348,-622.6159 604.348,-556.2865"/>
|
||||
<polygon fill="none" stroke="#000000" points="600.8481,-769.6555 604.348,-779.6556 607.8481,-769.6556 600.8481,-769.6555"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-282 550.348,-314 658.348,-314 658.348,-282 550.348,-282"/>
|
||||
<text text-anchor="start" x="587.4015" y="-295" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-190 550.348,-282 658.348,-282 658.348,-190 550.348,-190"/>
|
||||
<text text-anchor="start" x="580.452" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
|
||||
<text text-anchor="start" x="573.7885" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
|
||||
<text text-anchor="start" x="564.6235" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
||||
<text text-anchor="start" x="564.0595" y="-227" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
||||
<text text-anchor="start" x="560.1705" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
||||
<text text-anchor="start" x="576.0135" y="-203" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-170 550.348,-190 658.348,-190 658.348,-170 550.348,-170"/>
|
||||
</g>
|
||||
<!-- A2->A11 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A2->A11</title>
|
||||
<path fill="none" stroke="#000000" d="M604.348,-423.8663C604.348,-390.0029 604.348,-348.7174 604.348,-314.0468"/>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-276 274.348,-308 346.348,-308 346.348,-276 274.348,-276"/>
|
||||
<text text-anchor="start" x="292.5655" y="-289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-232 274.348,-276 346.348,-276 346.348,-232 274.348,-232"/>
|
||||
<text text-anchor="start" x="304.2395" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="290.9015" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-176 274.348,-232 346.348,-232 346.348,-176 274.348,-176"/>
|
||||
<text text-anchor="start" x="284.238" y="-213" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="287.572" y="-201" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="285.072" y="-189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<polygon fill="none" stroke="#000000" points="215.5,-926 215.5,-958 331.5,-958 331.5,-926 215.5,-926"/>
|
||||
<text text-anchor="start" x="225.149" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="215.5,-906 215.5,-926 331.5,-926 331.5,-906 215.5,-906"/>
|
||||
<polygon fill="none" stroke="#000000" points="215.5,-874 215.5,-906 331.5,-906 331.5,-874 215.5,-874"/>
|
||||
<text text-anchor="start" x="252.11" y="-887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1104 263.348,-1136 334.348,-1136 334.348,-1104 263.348,-1104"/>
|
||||
<text text-anchor="start" x="273.293" y="-1117" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1084 263.348,-1104 334.348,-1104 334.348,-1084 263.348,-1084"/>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1052 263.348,-1084 334.348,-1084 334.348,-1052 263.348,-1052"/>
|
||||
<text text-anchor="start" x="280.787" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
||||
<polygon fill="none" stroke="#000000" points="178.5,-726 178.5,-758 369.5,-758 369.5,-726 178.5,-726"/>
|
||||
<text text-anchor="start" x="240.0965" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<InverterIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="178.5,-706 178.5,-726 369.5,-726 369.5,-706 178.5,-706"/>
|
||||
<polygon fill="none" stroke="#000000" points="178.5,-650 178.5,-706 369.5,-706 369.5,-650 178.5,-650"/>
|
||||
<text text-anchor="start" x="240.522" y="-687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()->bool</text>
|
||||
<text text-anchor="start" x="188.2835" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>disc(shutdown_started=False)</text>
|
||||
<text text-anchor="start" x="219.544" y="-663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>create_remote()</text>
|
||||
</g>
|
||||
<!-- A1->A4 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A1->A4</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M273.5,-863.7744C273.5,-831.6663 273.5,-790.6041 273.5,-758.1476"/>
|
||||
<polygon fill="none" stroke="#000000" points="270.0001,-863.8621 273.5,-873.8622 277.0001,-863.8622 270.0001,-863.8621"/>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="441.5,-454 441.5,-498 563.5,-498 563.5,-454 441.5,-454"/>
|
||||
<text text-anchor="start" x="492.777" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||
<text text-anchor="start" x="469.9815" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<Singleton>></text>
|
||||
<polygon fill="none" stroke="#000000" points="441.5,-398 441.5,-454 563.5,-454 563.5,-398 441.5,-398"/>
|
||||
<text text-anchor="start" x="459.9875" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||
<text text-anchor="start" x="467.7665" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||
<text text-anchor="start" x="451.3735" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||
<polygon fill="none" stroke="#000000" points="441.5,-354 441.5,-398 563.5,-398 563.5,-354 441.5,-354"/>
|
||||
<text text-anchor="start" x="464.436" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||
<text text-anchor="start" x="468.6045" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="387.5,-792 387.5,-824 617.5,-824 617.5,-792 387.5,-792"/>
|
||||
<text text-anchor="start" x="489.7215" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Proxy</text>
|
||||
<polygon fill="none" stroke="#000000" points="387.5,-676 387.5,-792 617.5,-792 617.5,-676 387.5,-676"/>
|
||||
<text text-anchor="start" x="474.1545" y="-773" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>db_stat</text>
|
||||
<text text-anchor="start" x="467.491" y="-761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>entity_prfx</text>
|
||||
<text text-anchor="start" x="458.326" y="-749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>discovery_prfx</text>
|
||||
<text text-anchor="start" x="457.762" y="-737" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>proxy_node_id</text>
|
||||
<text text-anchor="start" x="453.873" y="-725" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>proxy_unique_id</text>
|
||||
<text text-anchor="start" x="469.716" y="-713" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><cls>mqtt:Mqtt</text>
|
||||
<text text-anchor="start" x="471.9355" y="-689" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<polygon fill="none" stroke="#000000" points="387.5,-584 387.5,-676 617.5,-676 617.5,-584 387.5,-584"/>
|
||||
<text text-anchor="start" x="478.6145" y="-657" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_init()</text>
|
||||
<text text-anchor="start" x="473.334" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_close()</text>
|
||||
<text text-anchor="start" x="444.984" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_cb_mqtt_is_up()</text>
|
||||
<text text-anchor="start" x="397.197" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_register_proxy_stat_home_assistant()</text>
|
||||
<text text-anchor="start" x="406.084" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_publ_mqtt_proxy_stat(key)</text>
|
||||
</g>
|
||||
<!-- A3->A2 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A3->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M502.5,-571.373C502.5,-549.9571 502.5,-528.339 502.5,-508.5579"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="502.5001,-571.682 506.5,-577.6821 502.5,-583.682 498.5,-577.682 502.5001,-571.682"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="502.5,-498.392 507.0001,-508.3919 502.5,-503.392 502.5001,-508.392 502.5001,-508.392 502.5001,-508.392 502.5,-503.392 498.0001,-508.392 502.5,-498.392 502.5,-498.392"/>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-898 231.348,-930 365.348,-930 365.348,-898 231.348,-898"/>
|
||||
<text text-anchor="start" x="278.0655" y="-911" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-734 231.348,-898 365.348,-898 365.348,-734 231.348,-734"/>
|
||||
<text text-anchor="start" x="261.6745" y="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="258.891" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="251.662" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
||||
<text text-anchor="start" x="257.496" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
||||
<text text-anchor="start" x="276.6725" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="280.5615" y="-819" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="277.5065" y="-807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
||||
<text text-anchor="start" x="248.337" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="246.9425" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="241.1145" y="-771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="280.5615" y="-759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
||||
<text text-anchor="start" x="269.174" y="-747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-666 231.348,-734 365.348,-734 365.348,-666 231.348,-666"/>
|
||||
<text text-anchor="start" x="248.0575" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
||||
<text text-anchor="start" x="272.7925" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
||||
<text text-anchor="start" x="258.6205" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
||||
<text text-anchor="start" x="256.9505" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
||||
<polygon fill="none" stroke="#000000" points="205.5,-502 205.5,-534 396.5,-534 396.5,-502 205.5,-502"/>
|
||||
<text text-anchor="start" x="272.66" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterBase</text>
|
||||
<polygon fill="none" stroke="#000000" points="205.5,-386 205.5,-502 396.5,-502 396.5,-386 205.5,-386"/>
|
||||
<text text-anchor="start" x="281.8335" y="-483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<text text-anchor="start" x="270.4355" y="-471" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<text text-anchor="start" x="290.997" y="-447" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="274.0505" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">config_id:str</text>
|
||||
<text text-anchor="start" x="247.3785" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:MessageProt</text>
|
||||
<text text-anchor="start" x="261.553" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="266.832" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="205.5,-318 205.5,-386 396.5,-386 396.5,-318 205.5,-318"/>
|
||||
<text text-anchor="start" x="267.522" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()->bool</text>
|
||||
<text text-anchor="start" x="215.2835" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>disc(shutdown_started=False)</text>
|
||||
<text text-anchor="start" x="246.544" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>create_remote()</text>
|
||||
<text text-anchor="start" x="240.984" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>async_publ_mqtt()</text>
|
||||
</g>
|
||||
<!-- A3->A5 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A3->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M409.1791,-575.5683C399.1409,-561.7533 389.0008,-547.7982 379.1588,-534.2532"/>
|
||||
<polygon fill="none" stroke="#000000" points="406.3649,-577.6495 415.0747,-583.682 412.0279,-573.5347 406.3649,-577.6495"/>
|
||||
</g>
|
||||
<!-- A4->A5 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A4->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M298.348,-1041.7414C298.348,-1010.6043 298.348,-969.5621 298.348,-930.0536"/>
|
||||
<polygon fill="none" stroke="#000000" points="294.8481,-1041.9047 298.348,-1051.9048 301.8481,-1041.9048 294.8481,-1041.9047"/>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M279.7719,-639.4228C282.8086,-608.1559 286.5373,-569.7639 289.991,-534.2034"/>
|
||||
<polygon fill="none" stroke="#000000" points="276.2531,-639.4473 278.77,-649.7389 283.2203,-640.1241 276.2531,-639.4473"/>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-584 370.348,-616 484.348,-616 484.348,-584 370.348,-584"/>
|
||||
<text text-anchor="start" x="413.456" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-480 370.348,-584 484.348,-584 484.348,-480 370.348,-480"/>
|
||||
<text text-anchor="start" x="380.111" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="415.1255" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="395.948" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="399.288" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="402.8925" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="401.232" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="413.46" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-364 370.348,-480 484.348,-480 484.348,-364 370.348,-364"/>
|
||||
<text text-anchor="start" x="384.8405" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="386.7805" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="392.6245" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="380.6765" y="-425" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="382.6215" y="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="391.7885" y="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="412.3505" y="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="356.5,-236 356.5,-268 456.5,-268 456.5,-236 356.5,-236"/>
|
||||
<text text-anchor="start" x="383.9995" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="356.5,-216 356.5,-236 456.5,-236 456.5,-216 356.5,-216"/>
|
||||
<polygon fill="none" stroke="#000000" points="356.5,-172 356.5,-216 456.5,-216 456.5,-172 356.5,-172"/>
|
||||
<text text-anchor="start" x="366.2175" y="-197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:ProtocolIfc</text>
|
||||
<text text-anchor="start" x="381.2185" y="-185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge8" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M357.819,-656.0073C363.3477,-642.8069 368.9261,-629.488 374.3902,-616.442"/>
|
||||
<polygon fill="none" stroke="#000000" points="354.4392,-655.017 353.8043,-665.5928 360.8958,-657.7213 354.4392,-655.017"/>
|
||||
<path fill="none" stroke="#000000" d="M356.1387,-317.872C363.3786,-303.802 370.5526,-289.86 377.1187,-277.0995"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="381.7846,-268.0318 381.2105,-278.9826 379.4969,-272.4777 377.2091,-276.9237 377.2091,-276.9237 377.2091,-276.9237 379.4969,-272.4777 373.2078,-274.8647 381.7846,-268.0318 381.7846,-268.0318"/>
|
||||
<text text-anchor="middle" x="381.0069" y="-285.0166" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">2</text>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-548 127.348,-580 218.348,-580 218.348,-548 127.348,-548"/>
|
||||
<text text-anchor="start" x="145.343" y="-561" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-456 127.348,-548 218.348,-548 218.348,-456 127.348,-456"/>
|
||||
<text text-anchor="start" x="157.846" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="160.9055" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="165.904" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="145.058" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="146.732" y="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="158.96" y="-469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-400 127.348,-456 218.348,-456 218.348,-400 127.348,-400"/>
|
||||
<text text-anchor="start" x="137.2885" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="157.8505" y="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="338.2314,-238 262.7686,-238 262.7686,-202 338.2314,-202 338.2314,-238"/>
|
||||
<text text-anchor="middle" x="300.5" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
</g>
|
||||
<!-- A5->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A5->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M240.2947,-656.0919C229.716,-630.2328 218.9501,-603.9162 209.2,-580.0827"/>
|
||||
<polygon fill="none" stroke="#000000" points="237.1556,-657.6626 244.1814,-665.5928 243.6344,-655.0121 237.1556,-657.6626"/>
|
||||
</g>
|
||||
<!-- A6->A3 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A6->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M370.219,-368.906C361.9756,-351.4328 353.6959,-333.8827 346.0351,-317.6444"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="341.601,-308.2456 349.9376,-315.3696 343.7344,-312.7676 345.8678,-317.2897 345.8678,-317.2897 345.8678,-317.2897 343.7344,-312.7676 341.7979,-319.2097 341.601,-308.2456 341.601,-308.2456"/>
|
||||
<text text-anchor="middle" x="356.9793" y="-318.0326" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="354.8406" y="-353.119" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-258 364.348,-290 514.348,-290 514.348,-258 364.348,-258"/>
|
||||
<text text-anchor="start" x="407.3935" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-226 364.348,-258 514.348,-258 514.348,-226 364.348,-226"/>
|
||||
<text text-anchor="start" x="374.335" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-194 364.348,-226 514.348,-226 514.348,-194 364.348,-194"/>
|
||||
<text text-anchor="start" x="424.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A6->A8 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A6->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M433.943,-353.7029C435.0429,-330.9727 436.1158,-308.7991 437.012,-290.2778"/>
|
||||
<polygon fill="none" stroke="#000000" points="430.441,-353.6629 433.4535,-363.8204 437.4328,-354.0013 430.441,-353.6629"/>
|
||||
</g>
|
||||
<!-- A7->A3 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A7->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M208.5342,-399.8871C214.3752,-387.5945 220.7034,-375.3217 227.348,-364 241.4757,-339.9278 249.4905,-336.9696 265.348,-314 266.3556,-312.5405 267.3678,-311.0592 268.3821,-309.5614"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="274.1947,-300.8381 272.3944,-311.6552 271.4221,-304.999 268.6496,-309.1599 268.6496,-309.1599 268.6496,-309.1599 271.4221,-304.999 264.9048,-306.6646 274.1947,-300.8381 274.1947,-300.8381"/>
|
||||
<text text-anchor="middle" x="271.1774" y="-317.6092" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="208.7462" y="-376.8883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<title>A5->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M300.5,-307.7729C300.5,-280.5002 300.5,-254.684 300.5,-238.2013"/>
|
||||
<polygon fill="none" stroke="#000000" points="297.0001,-307.872 300.5,-317.872 304.0001,-307.872 297.0001,-307.872"/>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-258 82.348,-290 238.348,-290 238.348,-258 82.348,-258"/>
|
||||
<text text-anchor="start" x="125.059" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-226 82.348,-258 238.348,-258 238.348,-226 82.348,-226"/>
|
||||
<text text-anchor="start" x="92.0005" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-194 82.348,-226 238.348,-226 238.348,-194 82.348,-194"/>
|
||||
<text text-anchor="start" x="145.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="94.4001,-238 12.5999,-238 12.5999,-202 94.4001,-202 94.4001,-238"/>
|
||||
<text text-anchor="middle" x="53.5" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
</g>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M167.4931,-389.6656C165.8292,-355.2775 164.0432,-318.3674 162.6731,-290.0512"/>
|
||||
<polygon fill="none" stroke="#000000" points="164.0025,-389.9456 167.9818,-399.7648 170.9943,-389.6073 164.0025,-389.9456"/>
|
||||
<!-- A5->A9 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A5->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M196.7667,-346.4637C165.8973,-321.9347 132.3582,-294.4156 102.5,-268 91.7971,-258.5312 80.3616,-247.3925 71.232,-238.23"/>
|
||||
<polygon fill="none" stroke="#000000" points="194.962,-349.4991 204.9739,-352.965 199.3086,-344.0121 194.962,-349.4991"/>
|
||||
</g>
|
||||
<!-- A8->A8 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A8->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M514.5164,-272.6238C525.1874,-267.6708 532.348,-257.4629 532.348,-242 532.348,-231.1277 528.8079,-222.8533 522.9966,-217.1769"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="514.5164,-211.3762 525.3108,-213.3079 518.6433,-214.1991 522.7702,-217.0221 522.7702,-217.0221 522.7702,-217.0221 518.6433,-214.1991 520.2296,-220.7363 514.5164,-211.3762 514.5164,-211.3762"/>
|
||||
<text text-anchor="middle" x="534.2494" y="-211.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="526.5555" y="-253.6532" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="450.1421,-36 360.8579,-36 360.8579,0 450.1421,0 450.1421,-36"/>
|
||||
<text text-anchor="middle" x="405.5" y="-15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
</g>
|
||||
<!-- A6->A11 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A6->A11</title>
|
||||
<path fill="none" stroke="#000000" d="M392.6633,-171.974C386.9982,-146.4565 382.5868,-114.547 386.5,-86 388.3468,-72.5276 392.161,-57.9618 395.8907,-45.7804"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="398.9587,-36.1851 400.1994,-47.0805 397.4359,-40.9476 395.9131,-45.71 395.9131,-45.71 395.9131,-45.71 397.4359,-40.9476 391.6269,-44.3395 398.9587,-36.1851 398.9587,-36.1851"/>
|
||||
<text text-anchor="middle" x="401.4892" y="-53.0243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,-88 460.348,-120 582.348,-120 582.348,-88 460.348,-88"/>
|
||||
<text text-anchor="start" x="497.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,-56 460.348,-88 582.348,-88 582.348,-56 460.348,-56"/>
|
||||
<text text-anchor="start" x="490.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,0 460.348,-56 582.348,-56 582.348,0 460.348,0"/>
|
||||
<text text-anchor="start" x="469.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||
<text text-anchor="start" x="506.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="493.5879,-122 395.4121,-122 395.4121,-86 493.5879,-86 493.5879,-122"/>
|
||||
<text text-anchor="middle" x="444.5" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
</g>
|
||||
<!-- A8->A12 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A8->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M465.2524,-184.5048C474.4734,-164.0387 484.8784,-140.9447 494.2007,-120.2539"/>
|
||||
<polygon fill="none" stroke="#000000" points="462.024,-183.1501 461.1072,-193.7052 468.4062,-186.0256 462.024,-183.1501"/>
|
||||
<!-- A6->A12 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A6->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M422.2853,-171.8133C426.7329,-158.2365 431.4225,-143.9208 435.3408,-131.9595"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="438.5602,-122.132 439.7235,-133.036 437.0036,-126.8835 435.4471,-131.6351 435.4471,-131.6351 435.4471,-131.6351 437.0036,-126.8835 431.1707,-130.2341 438.5602,-122.132 438.5602,-122.132"/>
|
||||
<text text-anchor="middle" x="440.9498" y="-138.9887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
</g>
|
||||
<!-- A9->A9 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A9->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M238.6951,-272.2739C249.2923,-267.1987 256.348,-257.1074 256.348,-242 256.348,-231.3776 252.8598,-223.2351 247.1049,-217.5725"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="238.6951,-211.7261 249.4746,-213.7393 242.8005,-214.5802 246.9059,-217.4342 246.9059,-217.4342 246.9059,-217.4342 242.8005,-214.5802 244.3373,-221.1291 238.6951,-211.7261 238.6951,-211.7261"/>
|
||||
<text text-anchor="middle" x="258.4028" y="-212.1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="250.5654" y="-253.1774" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="574.906,-248 474.094,-248 474.094,-192 580.906,-192 580.906,-242 574.906,-248"/>
|
||||
<polyline fill="none" stroke="#000000" points="574.906,-248 574.906,-242 "/>
|
||||
<polyline fill="none" stroke="#000000" points="580.906,-242 574.906,-242 "/>
|
||||
<text text-anchor="middle" x="527.5" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3</text>
|
||||
<text text-anchor="middle" x="527.5" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
|
||||
<text text-anchor="middle" x="527.5" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
|
||||
<text text-anchor="middle" x="527.5" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:Talent</text>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="251.348,-88 251.348,-120 373.348,-120 373.348,-88 251.348,-88"/>
|
||||
<text text-anchor="start" x="285.398" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="251.348,-56 251.348,-88 373.348,-88 373.348,-56 251.348,-56"/>
|
||||
<text text-anchor="start" x="281.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<polygon fill="none" stroke="#000000" points="251.348,0 251.348,-56 373.348,-56 373.348,0 251.348,0"/>
|
||||
<text text-anchor="start" x="260.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||
<text text-anchor="start" x="297.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A9->A13 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A9->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M207.2144,-185.8836C224.5887,-165.0802 244.3566,-141.4107 262.0261,-120.2539"/>
|
||||
<polygon fill="none" stroke="#000000" points="204.406,-183.7863 200.6821,-193.7052 209.7787,-188.2734 204.406,-183.7863"/>
|
||||
<!-- A7->A8 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A7->A8</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M308.5491,-238.3283C317.4345,-256.0056 333.5793,-281.6949 356.5,-293 396.3598,-312.6598 415.5578,-310.2929 456.5,-293 478.1607,-283.8511 496.4784,-264.5049 509.0802,-248.0264"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-578 236.348,-610 352.348,-610 352.348,-578 236.348,-578"/>
|
||||
<text text-anchor="start" x="264.622" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-498 236.348,-578 352.348,-578 352.348,-498 236.348,-498"/>
|
||||
<text text-anchor="start" x="279.901" y="-559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="282.131" y="-547" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="284.345" y="-535" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="279.901" y="-523" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="280.456" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-370 236.348,-498 352.348,-498 352.348,-370 236.348,-370"/>
|
||||
<text text-anchor="start" x="246.0055" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="248.226" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="266.002" y="-455" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="282.13" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="279.3505" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="259.6185" y="-407" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="264.628" y="-395" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</text>
|
||||
<text text-anchor="start" x="252.955" y="-383" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="239.022,-248 111.978,-248 111.978,-192 245.022,-192 245.022,-242 239.022,-248"/>
|
||||
<polyline fill="none" stroke="#000000" points="239.022,-248 239.022,-242 "/>
|
||||
<polyline fill="none" stroke="#000000" points="245.022,-242 239.022,-242 "/>
|
||||
<text text-anchor="middle" x="178.5" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3PLUS</text>
|
||||
<text text-anchor="middle" x="178.5" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
|
||||
<text text-anchor="middle" x="178.5" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
|
||||
<text text-anchor="middle" x="178.5" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:SolarmanV5</text>
|
||||
</g>
|
||||
<!-- A10->A8 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M357.4808,-370.672C358.7731,-368.4252 360.063,-366.1993 361.348,-364 375.806,-339.2552 392.855,-312.3558 407.3203,-290.1276"/>
|
||||
<polygon fill="none" stroke="#000000" points="354.4004,-369.0085 352.4898,-379.4297 360.4821,-372.4746 354.4004,-369.0085"/>
|
||||
<!-- A9->A10 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A9->A10</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M94.5156,-220C100.3114,-220 106.1072,-220 111.903,-220"/>
|
||||
</g>
|
||||
<!-- A10->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A10->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M231.527,-371.7451C230.1228,-369.1386 228.7284,-366.5542 227.348,-364 214.1359,-339.5527 199.2736,-312.4504 186.9099,-290.012"/>
|
||||
<polygon fill="none" stroke="#000000" points="228.5137,-373.5316 236.3333,-380.6803 234.6785,-370.2155 228.5137,-373.5316"/>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M567.1644,-160.4648C560.9703,-146.8826 554.6465,-133.016 548.7531,-120.0931"/>
|
||||
<polygon fill="none" stroke="#000000" points="564.0911,-162.1611 571.425,-169.8074 570.4601,-159.2566 564.0911,-162.1611"/>
|
||||
</g>
|
||||
<!-- A11->A13 -->
|
||||
<!-- A12->A11 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A11->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M542.4867,-170.8745C542.1078,-170.5805 541.7282,-170.2889 541.348,-170 513.3438,-148.7162 431.3406,-111.2534 373.497,-86.0342"/>
|
||||
<polygon fill="none" stroke="#000000" points="540.4079,-173.6974 550.3407,-177.3838 544.8747,-168.3078 540.4079,-173.6974"/>
|
||||
<title>A12->A11</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M436.2291,-85.7616C430.9033,-74.0176 423.8824,-58.5355 417.896,-45.3349"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="413.759,-36.2121 421.9874,-43.4608 415.824,-40.7657 417.8891,-45.3194 417.8891,-45.3194 417.8891,-45.3194 415.824,-40.7657 413.7908,-47.1779 413.759,-36.2121 413.759,-36.2121"/>
|
||||
<text text-anchor="middle" x="421.0451" y="-69.7445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points=".5,-454 .5,-486 107.5,-486 107.5,-454 .5,-454"/>
|
||||
<text text-anchor="start" x="24.2695" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusConn</text>
|
||||
<polygon fill="none" stroke="#000000" points=".5,-386 .5,-454 107.5,-454 107.5,-386 .5,-386"/>
|
||||
<text text-anchor="start" x="44.5515" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">host</text>
|
||||
<text text-anchor="start" x="45.387" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">port</text>
|
||||
<text text-anchor="start" x="43.997" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="10.383" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points=".5,-366 .5,-386 107.5,-386 107.5,-366 .5,-366"/>
|
||||
</g>
|
||||
<!-- A13->A9 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A13->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M53.5,-365.8625C53.5,-327.1513 53.5,-278.6088 53.5,-248.4442"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="53.5,-238.2147 58.0001,-248.2147 53.5,-243.2147 53.5001,-248.2147 53.5001,-248.2147 53.5001,-248.2147 53.5,-243.2147 49.0001,-248.2148 53.5,-238.2147 53.5,-238.2147"/>
|
||||
<text text-anchor="middle" x="61.9524" y="-253.3409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="45.0476" y="-344.7363" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A14 -->
|
||||
<g id="node15" class="node">
|
||||
<title>A14</title>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-1176 133.348,-1208 236.348,-1208 236.348,-1176 133.348,-1176"/>
|
||||
<text text-anchor="start" x="174.01" y="-1189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-1120 133.348,-1176 236.348,-1176 236.348,-1120 133.348,-1120"/>
|
||||
<text text-anchor="start" x="176.7895" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="152.334" y="-1145" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="165.9515" y="-1133" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-980 133.348,-1120 236.348,-1120 236.348,-980 133.348,-980"/>
|
||||
<text text-anchor="start" x="160.6835" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="158.7325" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="155.6785" y="-1077" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="154.0085" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="152.058" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="167.061" y="-1041" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="161.2225" y="-1029" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="145.385" y="-1017" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="154.8335" y="-1005" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="143.1705" y="-993" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
<polygon fill="none" stroke="#000000" points="93.7333,-722 13.2667,-722 13.2667,-686 93.7333,-686 93.7333,-722"/>
|
||||
<text text-anchor="middle" x="53.5" y="-701" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusTcp</text>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-814 386.348,-846 453.348,-846 453.348,-814 386.348,-814"/>
|
||||
<text text-anchor="start" x="402.341" y="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-794 386.348,-814 453.348,-814 453.348,-794 386.348,-794"/>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-750 386.348,-794 453.348,-794 453.348,-750 386.348,-750"/>
|
||||
<text text-anchor="start" x="396.232" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="404.016" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A14->A15 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>A14->A15</title>
|
||||
<path fill="none" stroke="#000000" d="M242.8857,-990.9876C246.5464,-987.0913 250.3682,-983.4032 254.348,-980 298.2601,-942.4501 334.8682,-972.1855 374.348,-930 395.7725,-907.1072 407.0366,-873.5975 412.9375,-846.0704"/>
|
||||
<polygon fill="none" stroke="#000000" points="240.0515,-988.9088 236.0452,-998.717 245.2936,-993.548 240.0515,-988.9088"/>
|
||||
</g>
|
||||
<!-- A16 -->
|
||||
<g id="node17" class="node">
|
||||
<title>A16</title>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-814 142.348,-846 209.348,-846 209.348,-814 142.348,-814"/>
|
||||
<text text-anchor="start" x="155.0065" y="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-794 142.348,-814 209.348,-814 209.348,-794 142.348,-794"/>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-750 142.348,-794 209.348,-794 209.348,-750 142.348,-750"/>
|
||||
<text text-anchor="start" x="152.232" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="160.016" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A14->A16 -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>A14->A16</title>
|
||||
<path fill="none" stroke="#000000" d="M180.5733,-969.853C179.2476,-926.2551 177.8358,-879.821 176.8143,-846.2247"/>
|
||||
<polygon fill="none" stroke="#000000" points="177.0788,-970.0931 180.8812,-979.9821 184.0756,-969.8803 177.0788,-970.0931"/>
|
||||
</g>
|
||||
<!-- A15->A6 -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>A15->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M420.598,-749.875C421.4623,-716.5989 422.6586,-670.54 423.8044,-626.4296"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="424.071,-616.1641 428.3097,-626.2776 423.9411,-621.1624 423.8113,-626.1607 423.8113,-626.1607 423.8113,-626.1607 423.9411,-621.1624 419.3128,-626.0438 424.071,-616.1641 424.071,-616.1641"/>
|
||||
</g>
|
||||
<!-- A16->A7 -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>A16->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M174.8793,-749.875C174.4651,-707.3571 173.8477,-643.9701 173.3263,-590.4435"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="173.2268,-580.2253 177.8241,-590.181 173.2756,-585.2251 173.3243,-590.2249 173.3243,-590.2249 173.3243,-590.2249 173.2756,-585.2251 168.8245,-590.2687 173.2268,-580.2253 173.2268,-580.2253"/>
|
||||
<!-- A14->A13 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A14->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M53.5,-685.7596C53.5,-647.9991 53.5,-559.5189 53.5,-496.3277"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="53.5,-486.0223 58.0001,-496.0223 53.5,-491.0223 53.5001,-496.0223 53.5001,-496.0223 53.5001,-496.0223 53.5,-491.0223 49.0001,-496.0224 53.5,-486.0223 53.5,-486.0223"/>
|
||||
<text text-anchor="middle" x="61.9524" y="-501.1485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">*</text>
|
||||
<text text-anchor="middle" x="45.0476" y="-664.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">creates</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 20 KiB |
@@ -3,26 +3,34 @@
|
||||
// {generate:true}
|
||||
|
||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
||||
[Modbus|err;retry_cnt|build_msg();recv_req();recv_resp()]
|
||||
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
||||
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
||||
[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
|
||||
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
|
||||
[Talent]has-1>[Modbus]
|
||||
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
|
||||
[SolarmanV5]has-1>[Modbus]
|
||||
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
|
||||
[AsyncStream]^[ConnectionG3P]
|
||||
[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
|
||||
[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
|
||||
[Mqtt]-[Inverter]
|
||||
[ConnectionG3]^[InverterG3]
|
||||
[ConnectionG3]has-0..1>[ConnectionG3]
|
||||
[ConnectionG3P]^[InverterG3P]
|
||||
[ConnectionG3P]has-0..1>[ConnectionG3P]
|
||||
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;update_db;set_db_def_value;get_db_value;ignore_this_device]^[InfosG3||ha_confs();parse()]
|
||||
[Infos]^[InfosG3P||ha_confs();parse()]
|
||||
[InfosG3P]->[SolarmanV5]
|
||||
[InfosG3]->[Talent]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[Mqtt;<<Singleton>>|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
||||
[Proxy|<cls>db_stat;<cls>entity_prfx;<cls>discovery_prfx;<cls>proxy_node_id;<cls>proxy_unique_id;<cls>mqtt:Mqtt;;__ha_restarts|class_init();class_close();;<async>_cb_mqtt_is_up();<async>_register_proxy_stat_home_assistant();<async>_async_publ_mqtt_proxy_stat(key)]
|
||||
|
||||
[<<InverterIfc>>||healthy()->bool;<async>disc(shutdown_started=False);<async>create_remote();]
|
||||
[<<AbstractIterMeta>>]^-.-[<<InverterIfc>>]
|
||||
[InverterBase|_registry;__ha_restarts;;addr;config_id:str;prot_class:MessageProt;remote:StreamPtr;local:StreamPtr;|healthy()->bool;<async>disc(shutdown_started=False);<async>create_remote();<async>async_publ_mqtt()]
|
||||
[StreamPtr||stream:ProtocolIfc;ifc:AsyncIfc]
|
||||
[<<InverterIfc>>]^-.-[InverterBase]
|
||||
[InverterG3]-[note: Creates an GEN3 inverter instance with prot_class:Talent{bg:cornsilk}]
|
||||
[InverterG3P]-[note: Creates an GEN3PLUS inverter instance with prot_class:SolarmanV5{bg:cornsilk}]
|
||||
[InverterBase]^[InverterG3]
|
||||
[InverterBase]^[InverterG3P]
|
||||
[Proxy]^[InverterBase]
|
||||
[InverterBase]-2>[StreamPtr]
|
||||
[Proxy]++->[Mqtt;<<Singleton>>]
|
||||
|
||||
[<<AsyncIfc>>]
|
||||
|
||||
|
||||
[StreamPtr]-1>[<<ProtocolIfc>>]
|
||||
[StreamPtr]-1>[<<AsyncIfc>>]
|
||||
|
||||
|
||||
[<<ProtocolIfc>>]use-.->[<<AsyncIfc>>]
|
||||
|
||||
|
||||
[ModbusConn|host;port;addr;stream:InverterG3P;|]has-1>[InverterG3P]
|
||||
[ModbusTcp]creates-*>[ModbusConn]
|
||||
|
||||
|
||||
432
app/proxy_2.svg
Normal file
432
app/proxy_2.svg
Normal file
@@ -0,0 +1,432 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="468pt" height="1942pt"
|
||||
viewBox="0.00 0.00 468.35 1942.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1938)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1938 464.348,-1938 464.348,4 -4,4"/>
|
||||
<!-- A0 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1910 .1516,-1910 .1516,-1874 114.5444,-1874 114.5444,-1904 108.5444,-1910"/>
|
||||
<polyline fill="none" stroke="#000000" points="108.5444,-1910 108.5444,-1904 "/>
|
||||
<polyline fill="none" stroke="#000000" points="114.5444,-1904 108.5444,-1904 "/>
|
||||
<text text-anchor="middle" x="57.348" y="-1895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||
<text text-anchor="middle" x="57.348" y="-1883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1902 132.348,-1934 248.348,-1934 248.348,-1902 132.348,-1902"/>
|
||||
<text text-anchor="start" x="141.997" y="-1915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1882 132.348,-1902 248.348,-1902 248.348,-1882 132.348,-1882"/>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1850 132.348,-1882 248.348,-1882 248.348,-1850 132.348,-1850"/>
|
||||
<text text-anchor="start" x="168.958" y="-1863" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
||||
</g>
|
||||
<!-- A14 -->
|
||||
<g id="node15" class="node">
|
||||
<title>A14</title>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1768 145.348,-1800 235.348,-1800 235.348,-1768 145.348,-1768"/>
|
||||
<text text-anchor="start" x="155.0545" y="-1781" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1736 145.348,-1768 235.348,-1768 235.348,-1736 145.348,-1736"/>
|
||||
<text text-anchor="start" x="171.1815" y="-1749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1704 145.348,-1736 235.348,-1736 235.348,-1704 145.348,-1704"/>
|
||||
<text text-anchor="start" x="175.3505" y="-1717" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A1->A14 -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>A1->A14</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1839.847C190.348,-1826.914 190.348,-1813.1101 190.348,-1800.3669"/>
|
||||
<polygon fill="none" stroke="#000000" points="186.8481,-1839.9953 190.348,-1849.9954 193.8481,-1839.9954 186.8481,-1839.9953"/>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-584 362.348,-616 460.348,-616 460.348,-584 362.348,-584"/>
|
||||
<text text-anchor="start" x="387.7325" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-528 362.348,-584 460.348,-584 460.348,-528 362.348,-528"/>
|
||||
<text text-anchor="start" x="401.345" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="371.901" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="377.18" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-472 362.348,-528 460.348,-528 460.348,-472 362.348,-472"/>
|
||||
<text text-anchor="start" x="375.79" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="396.3505" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,-100 29.348,-132 207.348,-132 207.348,-100 29.348,-100"/>
|
||||
<text text-anchor="start" x="73.8995" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,-68 29.348,-100 207.348,-100 207.348,-68 29.348,-68"/>
|
||||
<text text-anchor="start" x="86.119" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,0 29.348,-68 207.348,-68 207.348,0 29.348,0"/>
|
||||
<text text-anchor="start" x="70.0055" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="60.8365" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="39.157" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="103.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A2->A7 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A2->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M377.1669,-460.7015C372.2935,-447.8271 367.5247,-434.6178 363.348,-422 328.6206,-317.0888 364.6855,-270.3837 298.348,-182 272.7246,-147.8611 252.2817,-155.039 216.348,-132 216.2563,-131.9412 216.1645,-131.8823 216.0727,-131.8234"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="377.1669,-460.7018 383.0503,-464.8714 381.4643,-471.9059 375.5809,-467.7363 377.1669,-460.7018"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="207.433,-126.2441 218.2748,-127.8888 211.6333,-128.9566 215.8336,-131.6691 215.8336,-131.6691 215.8336,-131.6691 211.6333,-128.9566 213.3923,-135.4493 207.433,-126.2441 207.433,-126.2441"/>
|
||||
<text text-anchor="middle" x="367.0813" y="-455.0087" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-82 225.348,-114 363.348,-114 363.348,-82 225.348,-82"/>
|
||||
<text text-anchor="start" x="251.845" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-62 225.348,-82 363.348,-82 363.348,-62 225.348,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-18 225.348,-62 363.348,-62 363.348,-18 225.348,-18"/>
|
||||
<text text-anchor="start" x="248.226" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="235.172" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A2->A8 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A2->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M410.2845,-471.9734C407.2471,-397.4776 396.9339,-278.7213 363.348,-182 356.28,-161.6455 345.3872,-140.9471 334.3293,-122.7781"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="328.9036,-114.0745 338.0125,-120.18 331.5487,-118.3175 334.1938,-122.5606 334.1938,-122.5606 334.1938,-122.5606 331.5487,-118.3175 330.375,-124.9412 328.9036,-114.0745 328.9036,-114.0745"/>
|
||||
<text text-anchor="middle" x="345.6654" y="-121.9851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-342 40.348,-374 138.348,-374 138.348,-342 40.348,-342"/>
|
||||
<text text-anchor="start" x="62.398" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-286 40.348,-342 138.348,-342 138.348,-286 40.348,-286"/>
|
||||
<text text-anchor="start" x="79.345" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="49.901" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="55.18" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-230 40.348,-286 138.348,-286 138.348,-230 40.348,-230"/>
|
||||
<text text-anchor="start" x="53.79" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="74.3505" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3->A7 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A3->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M99.7163,-217.6237C102.7418,-193.0021 106.0292,-166.2494 108.9885,-142.1671"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="99.6702,-217.9995 102.9085,-224.4425 98.2065,-229.9099 94.9682,-223.4668 99.6702,-217.9995"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="110.2349,-132.0235 113.4816,-142.4978 109.6251,-136.9862 109.0152,-141.9489 109.0152,-141.9489 109.0152,-141.9489 109.6251,-136.9862 104.5488,-141.4 110.2349,-132.0235 110.2349,-132.0235"/>
|
||||
<text text-anchor="middle" x="92.028" y="-207.8882" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M138.4873,-232.1115C151.0204,-215.3231 164.7944,-197.7122 178.348,-182 196.1632,-161.3474 216.8826,-139.9135 235.8403,-121.173"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="243.01,-114.1296 239.0299,-124.3477 239.4432,-117.6336 235.8763,-121.1375 235.8763,-121.1375 235.8763,-121.1375 239.4432,-117.6336 232.7227,-117.9274 243.01,-114.1296 243.01,-114.1296"/>
|
||||
<text text-anchor="middle" x="236.0028" y="-129.8619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-958 180.348,-990 297.348,-990 297.348,-958 180.348,-958"/>
|
||||
<text text-anchor="start" x="208.277" y="-971" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-938 180.348,-958 297.348,-958 297.348,-938 180.348,-938"/>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-666 180.348,-938 297.348,-938 297.348,-666 180.348,-666"/>
|
||||
<text text-anchor="start" x="208.284" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="206.614" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="220.5115" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="218.292" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="221.9015" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="218.0115" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="222.1815" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="218.017" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="222.1815" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="216.6225" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="218.2925" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="221.6265" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="217.7365" y="-751" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="221.9065" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="217.742" y="-727" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="221.9065" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="213.847" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="190.2275" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="192.348,-574 192.348,-606 285.348,-606 285.348,-574 192.348,-574"/>
|
||||
<text text-anchor="start" x="210.512" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="192.348,-482 192.348,-574 285.348,-574 285.348,-482 192.348,-482"/>
|
||||
<text text-anchor="start" x="201.896" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="205.785" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="205.51" y="-531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="204.944" y="-519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="221.0615" y="-507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="214.3975" y="-495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A4->A5 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A4->A5</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M238.348,-655.339C238.348,-637.8929 238.348,-621.0952 238.348,-606.0686"/>
|
||||
<polygon fill="none" stroke="#000000" points="234.8481,-655.6774 238.348,-665.6775 241.8481,-655.6775 234.8481,-655.6774"/>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-390 187.348,-422 289.348,-422 289.348,-390 187.348,-390"/>
|
||||
<text text-anchor="start" x="208.622" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-310 187.348,-390 289.348,-390 289.348,-310 187.348,-310"/>
|
||||
<text text-anchor="start" x="223.901" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="226.131" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="228.345" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="223.901" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="224.456" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-182 187.348,-310 289.348,-310 289.348,-182 187.348,-182"/>
|
||||
<text text-anchor="start" x="210.002" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="226.13" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="223.3505" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="218.902" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="203.6185" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="203.069" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="196.955" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M238.348,-471.886C238.348,-456.1951 238.348,-439.1858 238.348,-422.1976"/>
|
||||
<polygon fill="none" stroke="#000000" points="234.8481,-471.9932 238.348,-481.9932 241.8481,-471.9933 234.8481,-471.9932"/>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M182.6502,-192.461C172.1789,-171.8674 161.5319,-150.9284 151.9938,-132.1701"/>
|
||||
<polygon fill="none" stroke="#000000" points="179.6308,-194.2451 187.2832,-201.5725 185.8705,-191.0723 179.6308,-194.2451"/>
|
||||
</g>
|
||||
<!-- A6->A8 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M269.1746,-172.088C274.0746,-151.438 278.8579,-131.2796 282.9161,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="265.7436,-171.3879 266.8402,-181.9259 272.5545,-173.0041 265.7436,-171.3879"/>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1320 302.348,-1352 416.348,-1352 416.348,-1320 302.348,-1320"/>
|
||||
<text text-anchor="start" x="345.456" y="-1333" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1168 302.348,-1320 416.348,-1320 416.348,-1168 302.348,-1168"/>
|
||||
<text text-anchor="start" x="334.0665" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="340.171" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="349.345" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="312.111" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="347.1255" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="327.948" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="331.288" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="334.8925" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="333.232" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="345.46" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1040 302.348,-1168 416.348,-1168 416.348,-1040 302.348,-1040"/>
|
||||
<text text-anchor="start" x="316.8405" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="318.7805" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="324.6245" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="312.6765" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="314.6215" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="323.7885" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="339.902" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="344.3505" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A9->A2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A9->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M377.8061,-1029.6429C379.5037,-1016.2527 381.0593,-1002.9144 382.348,-990 395.4702,-858.5013 399.9848,-704.3277 404.1955,-616.0127"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="376.4823,-1039.8864 373.3012,-1029.3921 377.1232,-1034.9276 377.764,-1029.9689 377.764,-1029.9689 377.764,-1029.9689 377.1232,-1034.9276 382.2269,-1030.5457 376.4823,-1039.8864 376.4823,-1039.8864"/>
|
||||
<text text-anchor="middle" x="370.4228" y="-1017.8264" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
</g>
|
||||
<!-- A9->A2 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A9->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M395.7566,-1029.6429C397.5037,-1016.2527 399.0593,-1002.9144 400.348,-990 412.8936,-864.2801 417.5714,-717.8344 416.9909,-628.0298"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="394.3845,-1039.8864 391.2521,-1029.3774 395.0484,-1034.9307 395.7122,-1029.9749 395.7122,-1029.9749 395.7122,-1029.9749 395.0484,-1034.9307 400.1724,-1030.5724 394.3845,-1039.8864 394.3845,-1039.8864"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="416.9908,-628.0122 412.9345,-622.0501 416.8778,-616.0127 420.9341,-621.9747 416.9908,-628.0122"/>
|
||||
<text text-anchor="middle" x="425.5004" y="-631.0585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A9->A4 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A9->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M308.0331,-1039.9349C303.6793,-1026.6936 299.2605,-1013.2547 294.8698,-999.9011"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="291.679,-990.1968 299.0774,-998.2908 293.2408,-994.9466 294.8026,-999.6964 294.8026,-999.6964 294.8026,-999.6964 293.2408,-994.9466 290.5277,-1001.102 291.679,-990.1968 291.679,-990.1968"/>
|
||||
<text text-anchor="middle" x="294.3419" y="-1022.3558" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-844 315.348,-876 382.348,-876 382.348,-844 315.348,-844"/>
|
||||
<text text-anchor="start" x="331.341" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-824 315.348,-844 382.348,-844 382.348,-824 315.348,-824"/>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-780 315.348,-824 382.348,-824 382.348,-780 315.348,-780"/>
|
||||
<text text-anchor="start" x="325.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="333.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A9->A12 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A9->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M354.683,-1039.9349C353.0582,-985.577 351.3338,-927.8888 350.095,-886.4463"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="349.7909,-876.272 354.5878,-886.1331 349.9404,-881.2698 350.0898,-886.2676 350.0898,-886.2676 350.0898,-886.2676 349.9404,-881.2698 345.5918,-886.4021 349.7909,-876.272 349.7909,-876.272"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1284 72.348,-1316 163.348,-1316 163.348,-1284 72.348,-1284"/>
|
||||
<text text-anchor="start" x="90.343" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1144 72.348,-1284 163.348,-1284 163.348,-1144 72.348,-1144"/>
|
||||
<text text-anchor="start" x="92.5665" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="98.671" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="107.845" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="102.846" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="105.9055" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="110.904" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="90.058" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="91.732" y="-1169" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="103.96" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1076 72.348,-1144 163.348,-1144 163.348,-1076 72.348,-1076"/>
|
||||
<text text-anchor="start" x="82.2885" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="98.402" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="102.8505" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M85.2254,-1066.0442C81.4271,-1040.8951 78.2542,-1014.6889 76.348,-990 59.0619,-766.107 69.322,-500.0139 79.5568,-374.449"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="86.7695,-1075.9973 80.7895,-1066.8055 86.0029,-1071.0565 85.2364,-1066.1156 85.2364,-1066.1156 85.2364,-1066.1156 86.0029,-1071.0565 89.6832,-1065.4256 86.7695,-1075.9973 86.7695,-1075.9973"/>
|
||||
<text text-anchor="middle" x="75.6382" y="-1056.3813" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M102.8817,-1066.0442C99.4271,-1040.8951 96.2542,-1014.6889 94.348,-990 77.6021,-773.1036 86.7076,-516.6035 90.1168,-386.6269"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="104.2701,-1075.9973 98.4317,-1066.7149 103.5793,-1071.0453 102.8885,-1066.0932 102.8885,-1066.0932 102.8885,-1066.0932 103.5793,-1071.0453 107.3454,-1065.4715 104.2701,-1075.9973 104.2701,-1075.9973"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="90.1213,-386.4451 86.2759,-380.3449 90.4278,-374.449 94.2733,-380.5493 90.1213,-386.4451"/>
|
||||
<text text-anchor="middle" x="98.4146" y="-389.7851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A10->A4 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A10->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M156.8842,-1075.7577C164.8622,-1051.494 173.3952,-1025.5424 181.8248,-999.9053"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="185.0491,-990.0992 186.2003,-1001.0045 183.4873,-994.849 181.9255,-999.5989 181.9255,-999.5989 181.9255,-999.5989 183.4873,-994.849 177.6506,-998.1932 185.0491,-990.0992 185.0491,-990.0992"/>
|
||||
<text text-anchor="middle" x="154.5165" y="-1052.8983" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-844 95.348,-876 162.348,-876 162.348,-844 95.348,-844"/>
|
||||
<text text-anchor="start" x="108.0065" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-824 95.348,-844 162.348,-844 162.348,-824 95.348,-824"/>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-780 95.348,-824 162.348,-824 162.348,-780 95.348,-780"/>
|
||||
<text text-anchor="start" x="105.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="113.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A10->A13 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>A10->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M120.9422,-1075.7577C122.842,-1012.1995 125.0881,-937.0598 126.6045,-886.3285"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="126.9072,-876.2026 131.1063,-886.3326 126.7577,-881.2004 126.6083,-886.1981 126.6083,-886.1981 126.6083,-886.1981 126.7577,-881.2004 122.1103,-886.0636 126.9072,-876.2026 126.9072,-876.2026"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1284 181.348,-1316 284.348,-1316 284.348,-1284 181.348,-1284"/>
|
||||
<text text-anchor="start" x="222.01" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1228 181.348,-1284 284.348,-1284 284.348,-1228 181.348,-1228"/>
|
||||
<text text-anchor="start" x="224.7895" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="200.334" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="213.9515" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1076 181.348,-1228 284.348,-1228 284.348,-1076 181.348,-1076"/>
|
||||
<text text-anchor="start" x="208.6835" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="206.7325" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="203.6785" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="202.0085" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="200.058" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="215.061" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="207.842" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="209.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="193.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="202.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="191.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M280.9382,-1066.3836C289.808,-1041.1849 298.6736,-1014.8736 306.348,-990 318.0407,-952.103 329.205,-908.562 337.0802,-876.1703"/>
|
||||
<polygon fill="none" stroke="#000000" points="277.5783,-1065.3868 277.541,-1075.9815 284.1771,-1067.7225 277.5783,-1065.3868"/>
|
||||
</g>
|
||||
<!-- A11->A13 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A11->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M192.4636,-1066.2454C184.9411,-1041.0063 177.2684,-1014.7156 170.348,-990 159.6936,-951.9489 148.506,-908.6064 140.3587,-876.3336"/>
|
||||
<polygon fill="none" stroke="#000000" points="189.1205,-1067.2825 195.3374,-1075.8616 195.8274,-1065.2781 189.1205,-1067.2825"/>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1550 150.348,-1582 231.348,-1582 231.348,-1550 150.348,-1550"/>
|
||||
<text text-anchor="start" x="170.5655" y="-1563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1518 150.348,-1550 231.348,-1550 231.348,-1518 150.348,-1518"/>
|
||||
<text text-anchor="start" x="173.0615" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1474 150.348,-1518 231.348,-1518 231.348,-1474 150.348,-1474"/>
|
||||
<text text-anchor="start" x="161.6785" y="-1499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="160.0085" y="-1487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
</g>
|
||||
<!-- A14->A15 -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>A14->A15</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1693.5712C190.348,-1659.1613 190.348,-1615.9264 190.348,-1582.2666"/>
|
||||
<polygon fill="none" stroke="#000000" points="186.8481,-1693.9463 190.348,-1703.9464 193.8481,-1693.9464 186.8481,-1693.9463"/>
|
||||
</g>
|
||||
<!-- A15->A9 -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>A15->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M229.7336,-1465.2507C249.8317,-1432.1375 274.0435,-1390.492 293.348,-1352 296.3143,-1346.0855 299.269,-1340.0055 302.1912,-1333.8349"/>
|
||||
<polygon fill="none" stroke="#000000" points="226.6683,-1463.555 224.4506,-1473.9151 232.6449,-1467.1992 226.6683,-1463.555"/>
|
||||
</g>
|
||||
<!-- A15->A10 -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>A15->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M176.3052,-1464.1339C167.0994,-1422.2665 154.7762,-1366.2214 143.7769,-1316.197"/>
|
||||
<polygon fill="none" stroke="#000000" points="172.8909,-1464.9045 178.4568,-1473.9196 179.7276,-1463.4012 172.8909,-1464.9045"/>
|
||||
</g>
|
||||
<!-- A16 -->
|
||||
<g id="node17" class="node">
|
||||
<title>A16</title>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1622 285.348,-1654 360.348,-1654 360.348,-1622 285.348,-1622"/>
|
||||
<text text-anchor="start" x="305.0655" y="-1635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1470 285.348,-1622 360.348,-1622 360.348,-1470 285.348,-1470"/>
|
||||
<text text-anchor="start" x="314.5095" y="-1603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="295.338" y="-1579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="296.453" y="-1567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="306.4565" y="-1555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="296.7375" y="-1543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="304.79" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="316.7395" y="-1519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="303.4015" y="-1507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="301.727" y="-1495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="316.1845" y="-1483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1402 285.348,-1470 360.348,-1470 360.348,-1402 285.348,-1402"/>
|
||||
<text text-anchor="start" x="296.738" y="-1451" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="300.072" y="-1439" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="297.572" y="-1427" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="307.8505" y="-1415" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A16->A9 -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>A16->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M337.5861,-1391.2689C339.0212,-1378.3918 340.4833,-1365.272 341.9365,-1352.2329"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="336.4417,-1401.5379 333.077,-1391.101 336.9955,-1396.5687 337.5493,-1391.5995 337.5493,-1391.5995 337.5493,-1391.5995 336.9955,-1396.5687 342.0217,-1392.0979 336.4417,-1401.5379 336.4417,-1401.5379"/>
|
||||
<text text-anchor="middle" x="348.3292" y="-1368.1837" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="330.049" y="-1379.5871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
</g>
|
||||
<!-- A16->A10 -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>A16->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M279.1725,-1451.7757C267.6828,-1434.4941 254.4915,-1416.871 240.348,-1402 214.2483,-1374.5578 193.9188,-1382.411 171.348,-1352 163.2346,-1341.0684 156.273,-1328.8227 150.315,-1316.1269"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="284.6729,-1460.2184 275.4437,-1454.2962 281.9435,-1456.0291 279.2141,-1451.8397 279.2141,-1451.8397 279.2141,-1451.8397 281.9435,-1456.0291 282.9845,-1449.3833 284.6729,-1460.2184 284.6729,-1460.2184"/>
|
||||
<text text-anchor="middle" x="165.7688" y="-1325.8225" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="267.6964" y="-1446.645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 38 KiB |
51
app/proxy_2.yuml
Normal file
51
app/proxy_2.yuml
Normal file
@@ -0,0 +1,51 @@
|
||||
// {type:class}
|
||||
// {direction:topDown}
|
||||
// {generate:true}
|
||||
|
||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[InverterG3|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
|
||||
[<<AsyncIfc>>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()]
|
||||
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
|
||||
[AsyncStream|reader;writer;addr;r_addr;l_addr|;<async>loop;disc();close();healthy();;__async_read();__async_write();__async_forward()]
|
||||
[AsyncStreamServer|create_remote|<async>server_loop();<async>_async_forward();<async>publish_outstanding_mqtt();close()]
|
||||
[AsyncStreamClient||<async>client_loop();<async>_async_forward())]
|
||||
[<<AsyncIfc>>]^-.-[AsyncIfcImpl]
|
||||
[AsyncIfcImpl]^[AsyncStream]
|
||||
[AsyncStream]^[AsyncStreamServer]
|
||||
[AsyncStream]^[AsyncStreamClient]
|
||||
|
||||
|
||||
[Talent|ifc:AsyncIfc;conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()]
|
||||
[Talent]<remote-[InverterG3]
|
||||
[InverterG3]-remote>[AsyncStreamClient]
|
||||
[Talent]<-local++[InverterG3]
|
||||
[InverterG3]++local->[AsyncStreamServer]
|
||||
|
||||
[SolarmanV5|ifc:AsyncIfc;conn_no;addr;;control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;healthy();close()]
|
||||
[SolarmanV5]<remote-[InverterG3P]
|
||||
[InverterG3P]-remote>[AsyncStreamClient]
|
||||
[SolarmanV5]<-local++[InverterG3P]
|
||||
[InverterG3P]++local->[AsyncStreamServer]
|
||||
|
||||
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
|
||||
[Infos]^[InfosG3||ha_confs();parse()]
|
||||
[Infos]^[InfosG3P||ha_confs();parse()]
|
||||
|
||||
[Talent]use->[<<AsyncIfc>>]
|
||||
[Talent]->[InfosG3]
|
||||
[SolarmanV5]use->[<<AsyncIfc>>]
|
||||
[SolarmanV5]->[InfosG3P]
|
||||
|
||||
[<<ProtocolIfc>>|_registry|close()]
|
||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||
[<<ProtocolIfc>>]^-.-[Message|node_id|inc_counter();dec_counter()]
|
||||
[Message]^[Talent]
|
||||
[Message]^[SolarmanV5]
|
||||
|
||||
[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
|
||||
[Modbus]<1-has[SolarmanV5]
|
||||
[Modbus]<1-has[Talent]
|
||||
6
app/requirements-test.txt
Normal file
6
app/requirements-test.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
flake8
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
mock
|
||||
coverage
|
||||
@@ -1,3 +1,4 @@
|
||||
aiomqtt==2.0.1
|
||||
schema==0.7.5
|
||||
aiocron==1.8
|
||||
aiomqtt==2.3.0
|
||||
schema==0.7.7
|
||||
aiocron==1.8
|
||||
aiohttp==3.10.5
|
||||
104
app/src/async_ifc.py
Normal file
104
app/src/async_ifc.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class AsyncIfc(ABC):
|
||||
@abstractmethod
|
||||
def get_conn_no(self):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def set_node_id(self, value: str):
|
||||
pass # pragma: no cover
|
||||
|
||||
#
|
||||
# TX - QUEUE
|
||||
#
|
||||
@abstractmethod
|
||||
def tx_add(self, data: bytearray):
|
||||
''' add data to transmit queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def tx_flush(self):
|
||||
''' send transmit queue and clears it'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def tx_peek(self, size: int = None) -> bytearray:
|
||||
'''returns size numbers of byte without removing them'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def tx_log(self, level, info):
|
||||
''' log the transmit queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def tx_clear(self):
|
||||
''' clear transmit queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def tx_len(self):
|
||||
''' get numner of bytes in the transmit queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
#
|
||||
# FORWARD - QUEUE
|
||||
#
|
||||
@abstractmethod
|
||||
def fwd_add(self, data: bytearray):
|
||||
''' add data to forward queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def fwd_log(self, level, info):
|
||||
''' log the forward queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
#
|
||||
# RX - QUEUE
|
||||
#
|
||||
@abstractmethod
|
||||
def rx_get(self, size: int = None) -> bytearray:
|
||||
'''removes size numbers of bytes and return them'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def rx_peek(self, size: int = None) -> bytearray:
|
||||
'''returns size numbers of byte without removing them'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def rx_log(self, level, info):
|
||||
''' logs the receive queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def rx_clear(self):
|
||||
''' clear receive queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def rx_len(self):
|
||||
''' get numner of bytes in the receive queue'''
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def rx_set_cb(self, callback):
|
||||
pass # pragma: no cover
|
||||
|
||||
#
|
||||
# Protocol Callbacks
|
||||
#
|
||||
@abstractmethod
|
||||
def prot_set_timeout_cb(self, callback):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def prot_set_init_new_client_conn_cb(self, callback):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def prot_set_update_header_cb(self, callback):
|
||||
pass # pragma: no cover
|
||||
@@ -1,139 +1,395 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from messages import hex_dump_memory
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from typing import Self
|
||||
from itertools import count
|
||||
|
||||
if __name__ == "app.src.async_stream":
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
|
||||
|
||||
import gc
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
|
||||
class AsyncStream():
|
||||
class AsyncIfcImpl(AsyncIfc):
|
||||
_ids = count(0)
|
||||
|
||||
def __init__(self) -> None:
|
||||
logger.debug('AsyncIfcImpl.__init__')
|
||||
self.fwd_fifo = ByteFifo()
|
||||
self.tx_fifo = ByteFifo()
|
||||
self.rx_fifo = ByteFifo()
|
||||
self.conn_no = next(self._ids)
|
||||
self.node_id = ''
|
||||
self.timeout_cb = None
|
||||
self.init_new_client_conn_cb = None
|
||||
self.update_header_cb = None
|
||||
|
||||
def close(self):
|
||||
self.timeout_cb = None
|
||||
self.fwd_fifo.reg_trigger(None)
|
||||
self.tx_fifo.reg_trigger(None)
|
||||
self.rx_fifo.reg_trigger(None)
|
||||
|
||||
def set_node_id(self, value: str):
|
||||
self.node_id = value
|
||||
|
||||
def get_conn_no(self):
|
||||
return self.conn_no
|
||||
|
||||
def tx_add(self, data: bytearray):
|
||||
''' add data to transmit queue'''
|
||||
self.tx_fifo += data
|
||||
|
||||
def tx_flush(self):
|
||||
''' send transmit queue and clears it'''
|
||||
self.tx_fifo()
|
||||
|
||||
def tx_peek(self, size: int = None) -> bytearray:
|
||||
'''returns size numbers of byte without removing them'''
|
||||
return self.tx_fifo.peek(size)
|
||||
|
||||
def tx_log(self, level, info):
|
||||
''' log the transmit queue'''
|
||||
self.tx_fifo.logging(level, info)
|
||||
|
||||
def tx_clear(self):
|
||||
''' clear transmit queue'''
|
||||
self.tx_fifo.clear()
|
||||
|
||||
def tx_len(self):
|
||||
''' get numner of bytes in the transmit queue'''
|
||||
return len(self.tx_fifo)
|
||||
|
||||
def fwd_add(self, data: bytearray):
|
||||
''' add data to forward queue'''
|
||||
self.fwd_fifo += data
|
||||
|
||||
def fwd_log(self, level, info):
|
||||
''' log the forward queue'''
|
||||
self.fwd_fifo.logging(level, info)
|
||||
|
||||
def rx_get(self, size: int = None) -> bytearray:
|
||||
'''removes size numbers of bytes and return them'''
|
||||
return self.rx_fifo.get(size)
|
||||
|
||||
def rx_peek(self, size: int = None) -> bytearray:
|
||||
'''returns size numbers of byte without removing them'''
|
||||
return self.rx_fifo.peek(size)
|
||||
|
||||
def rx_log(self, level, info):
|
||||
''' logs the receive queue'''
|
||||
self.rx_fifo.logging(level, info)
|
||||
|
||||
def rx_clear(self):
|
||||
''' clear receive queue'''
|
||||
self.rx_fifo.clear()
|
||||
|
||||
def rx_len(self):
|
||||
''' get numner of bytes in the receive queue'''
|
||||
return len(self.rx_fifo)
|
||||
|
||||
def rx_set_cb(self, callback):
|
||||
self.rx_fifo.reg_trigger(callback)
|
||||
|
||||
def prot_set_timeout_cb(self, callback):
|
||||
self.timeout_cb = callback
|
||||
|
||||
def prot_set_init_new_client_conn_cb(self, callback):
|
||||
self.init_new_client_conn_cb = callback
|
||||
|
||||
def prot_set_update_header_cb(self, callback):
|
||||
self.update_header_cb = callback
|
||||
|
||||
|
||||
class StreamPtr():
|
||||
'''Descr StreamPtr'''
|
||||
def __init__(self, _stream, _ifc=None):
|
||||
self.stream = _stream
|
||||
self.ifc = _ifc
|
||||
|
||||
@property
|
||||
def ifc(self):
|
||||
return self._ifc
|
||||
|
||||
@ifc.setter
|
||||
def ifc(self, value):
|
||||
self._ifc = value
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
return self._stream
|
||||
|
||||
@stream.setter
|
||||
def stream(self, value):
|
||||
self._stream = value
|
||||
|
||||
|
||||
class AsyncStream(AsyncIfcImpl):
|
||||
MAX_PROC_TIME = 2
|
||||
'''maximum processing time for a received msg in sec'''
|
||||
MAX_START_TIME = 400
|
||||
'''maximum time without a received msg in sec'''
|
||||
MAX_INV_IDLE_TIME = 120
|
||||
'''maximum time without a received msg from the inverter in sec'''
|
||||
MAX_DEF_IDLE_TIME = 360
|
||||
'''maximum default time without a received msg in sec'''
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
rstream: "StreamPtr") -> None:
|
||||
AsyncIfcImpl.__init__(self)
|
||||
|
||||
def __init__(self, reader, writer, addr) -> None:
|
||||
logger.debug('AsyncStream.__init__')
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.addr = addr
|
||||
self.r_addr = ''
|
||||
self.l_addr = ''
|
||||
|
||||
async def server_loop(self, addr):
|
||||
'''Loop for receiving messages from the inverter (server-side)'''
|
||||
logging.info(f'Accept connection from {addr}')
|
||||
self.inc_counter('Inverter_Cnt')
|
||||
await self.loop()
|
||||
self.dec_counter('Inverter_Cnt')
|
||||
logging.info(f'Server loop stopped for r{self.r_addr}')
|
||||
self.remote = rstream
|
||||
self.tx_fifo.reg_trigger(self.__write_cb)
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self.r_addr = writer.get_extra_info('peername')
|
||||
self.l_addr = writer.get_extra_info('sockname')
|
||||
self.proc_start = None # start processing start timestamp
|
||||
self.proc_max = 0
|
||||
self.async_publ_mqtt = None # will be set AsyncStreamServer only
|
||||
|
||||
# if the server connection closes, we also have to disconnect
|
||||
# the connection to te TSUN cloud
|
||||
if self.remoteStream:
|
||||
logging.debug("disconnect client connection")
|
||||
await self.remoteStream.disc()
|
||||
try:
|
||||
await self._async_publ_mqtt_proxy_stat('proxy')
|
||||
except Exception:
|
||||
pass
|
||||
def __write_cb(self):
|
||||
self._writer.write(self.tx_fifo.get())
|
||||
|
||||
async def client_loop(self, addr):
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
clientStream = await self.remoteStream.loop()
|
||||
logging.info(f'Client loop stopped for l{clientStream.l_addr}')
|
||||
|
||||
# if the client connection closes, we don't touch the server
|
||||
# connection. Instead we erase the client connection stream,
|
||||
# thus on the next received packet from the inverter, we can
|
||||
# establish a new connection to the TSUN cloud
|
||||
|
||||
# erase backlink to inverter
|
||||
clientStream.remoteStream = None
|
||||
|
||||
if self.remoteStream == clientStream:
|
||||
# logging.debug(f'Client l{clientStream.l_addr} refs:'
|
||||
# f' {gc.get_referrers(clientStream)}')
|
||||
# than erase client connection
|
||||
self.remoteStream = None
|
||||
|
||||
async def loop(self):
|
||||
self.r_addr = self.writer.get_extra_info('peername')
|
||||
self.l_addr = self.writer.get_extra_info('sockname')
|
||||
def __timeout(self) -> int:
|
||||
if self.timeout_cb:
|
||||
return self.timeout_cb()
|
||||
return 360
|
||||
|
||||
async def loop(self) -> Self:
|
||||
"""Async loop handler for precessing all received messages"""
|
||||
self.proc_start = time.time()
|
||||
while True:
|
||||
try:
|
||||
await self.__async_read()
|
||||
self.__calc_proc_time()
|
||||
dead_conn_to = self.__timeout()
|
||||
await asyncio.wait_for(self.__async_read(),
|
||||
dead_conn_to)
|
||||
|
||||
if self.unique_id:
|
||||
await self.async_write()
|
||||
await self.__async_forward()
|
||||
await self.__async_write()
|
||||
await self.__async_forward()
|
||||
if self.async_publ_mqtt:
|
||||
await self.async_publ_mqtt()
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f'[{self.node_id}:{self.conn_no}] Dead '
|
||||
f'connection timeout ({dead_conn_to}s) '
|
||||
f'for {self.l_addr}')
|
||||
await self.disc()
|
||||
return self
|
||||
|
||||
except OSError as error:
|
||||
logger.error(f'{error} for l{self.l_addr} | '
|
||||
logger.error(f'[{self.node_id}:{self.conn_no}] '
|
||||
f'{error} for l{self.l_addr} | '
|
||||
f'r{self.r_addr}')
|
||||
await self.disc()
|
||||
self.close()
|
||||
return self
|
||||
|
||||
except RuntimeError as error:
|
||||
logger.warning(f"{error} for {self.l_addr}")
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
f'{error} for {self.l_addr}')
|
||||
await self.disc()
|
||||
self.close()
|
||||
return self
|
||||
|
||||
except Exception:
|
||||
self.inc_counter('SW_Exception')
|
||||
Infos.inc_counter('SW_Exception')
|
||||
logger.error(
|
||||
f"Exception for {self.addr}:\n"
|
||||
f"Exception for {self.r_addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
await asyncio.sleep(0) # be cooperative to other task
|
||||
|
||||
def __calc_proc_time(self):
|
||||
if self.proc_start:
|
||||
proc = time.time() - self.proc_start
|
||||
if proc > self.proc_max:
|
||||
self.proc_max = proc
|
||||
self.proc_start = None
|
||||
|
||||
async def disc(self) -> None:
|
||||
if self.writer.is_closing():
|
||||
"""Async disc handler for graceful disconnect"""
|
||||
self.remote = None
|
||||
if self._writer.is_closing():
|
||||
return
|
||||
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
self._writer.close()
|
||||
await self._writer.wait_closed()
|
||||
|
||||
def close(self):
|
||||
if self.writer.is_closing():
|
||||
def close(self) -> None:
|
||||
logging.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
||||
"""close handler for a no waiting disconnect
|
||||
|
||||
hint: must be called before releasing the connection instance
|
||||
"""
|
||||
super().close()
|
||||
self._reader.feed_eof() # abort awaited read
|
||||
if self._writer.is_closing():
|
||||
return
|
||||
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
||||
self.writer.close()
|
||||
self._writer.close()
|
||||
|
||||
def healthy(self) -> bool:
|
||||
elapsed = 0
|
||||
if self.proc_start is not None:
|
||||
elapsed = time.time() - self.proc_start
|
||||
if elapsed > self.MAX_PROC_TIME:
|
||||
logging.debug(f'[{self.node_id}:{self.conn_no}:'
|
||||
f'{type(self).__name__}]'
|
||||
f' act:{round(1000*elapsed)}ms'
|
||||
f' max:{round(1000*self.proc_max)}ms')
|
||||
logging.debug(f'Healthy()) refs: {gc.get_referrers(self)}')
|
||||
return elapsed < 5
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
async def __async_read(self) -> None:
|
||||
data = await self.reader.read(4096)
|
||||
"""Async read handler to read received data from TCP stream"""
|
||||
data = await self._reader.read(4096)
|
||||
if data:
|
||||
self._recv_buffer += data
|
||||
self.read() # call read in parent class
|
||||
self.proc_start = time.time()
|
||||
self.rx_fifo += data
|
||||
wait = self.rx_fifo() # call read in parent class
|
||||
if wait and wait > 0:
|
||||
await asyncio.sleep(wait)
|
||||
else:
|
||||
raise RuntimeError("Peer closed.")
|
||||
|
||||
async def async_write(self, headline='Transmit to ') -> None:
|
||||
if self._send_buffer:
|
||||
hex_dump_memory(logging.INFO, f'{headline}{self.addr}:',
|
||||
self._send_buffer, len(self._send_buffer))
|
||||
self.writer.write(self._send_buffer)
|
||||
await self.writer.drain()
|
||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||
async def __async_write(self, headline: str = 'Transmit to ') -> None:
|
||||
"""Async write handler to transmit the send_buffer"""
|
||||
if len(self.tx_fifo) > 0:
|
||||
self.tx_fifo.logging(logging.INFO, f'{headline}{self.r_addr}:')
|
||||
self._writer.write(self.tx_fifo.get())
|
||||
await self._writer.drain()
|
||||
|
||||
async def __async_forward(self) -> None:
|
||||
if self._forward_buffer:
|
||||
if not self.remoteStream:
|
||||
await self.async_create_remote()
|
||||
if self.remoteStream:
|
||||
if self.remoteStream._init_new_client_conn():
|
||||
await self.remoteStream.async_write()
|
||||
"""forward handler transmits data over the remote connection"""
|
||||
if len(self.fwd_fifo) == 0:
|
||||
return
|
||||
try:
|
||||
await self._async_forward()
|
||||
|
||||
if self.remoteStream:
|
||||
self.remoteStream._update_header(self._forward_buffer)
|
||||
hex_dump_memory(logging.INFO,
|
||||
f'Forward to {self.remoteStream.addr}:',
|
||||
self._forward_buffer,
|
||||
len(self._forward_buffer))
|
||||
self.remoteStream.writer.write(self._forward_buffer)
|
||||
await self.remoteStream.writer.drain()
|
||||
self._forward_buffer = bytearray(0)
|
||||
except OSError as error:
|
||||
if self.remote.stream:
|
||||
rmt = self.remote
|
||||
logger.error(f'[{rmt.stream.node_id}:{rmt.stream.conn_no}] '
|
||||
f'Fwd: {error} for '
|
||||
f'l{rmt.ifc.l_addr} | r{rmt.ifc.r_addr}')
|
||||
await rmt.ifc.disc()
|
||||
if rmt.ifc.close_cb:
|
||||
rmt.ifc.close_cb()
|
||||
|
||||
def __del__(self):
|
||||
logger.debug(
|
||||
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
||||
except RuntimeError as error:
|
||||
if self.remote.stream:
|
||||
rmt = self.remote
|
||||
logger.info(f'[{rmt.stream.node_id}:{rmt.stream.conn_no}] '
|
||||
f'Fwd: {error} for {rmt.ifc.l_addr}')
|
||||
await rmt.ifc.disc()
|
||||
if rmt.ifc.close_cb:
|
||||
rmt.ifc.close_cb()
|
||||
|
||||
except Exception:
|
||||
Infos.inc_counter('SW_Exception')
|
||||
logger.error(
|
||||
f"Fwd Exception for {self.r_addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
|
||||
class AsyncStreamServer(AsyncStream):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
async_publ_mqtt, create_remote,
|
||||
rstream: "StreamPtr") -> None:
|
||||
AsyncStream.__init__(self, reader, writer, rstream)
|
||||
self.create_remote = create_remote
|
||||
self.async_publ_mqtt = async_publ_mqtt
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug('AsyncStreamServer.close()')
|
||||
self.create_remote = None
|
||||
self.async_publ_mqtt = None
|
||||
super().close()
|
||||
|
||||
async def server_loop(self) -> None:
|
||||
'''Loop for receiving messages from the inverter (server-side)'''
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
f'Accept connection from {self.r_addr}')
|
||||
Infos.inc_counter('Inverter_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
await self.loop()
|
||||
Infos.dec_counter('Inverter_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for'
|
||||
f' r{self.r_addr}')
|
||||
|
||||
# if the server connection closes, we also have to disconnect
|
||||
# the connection to te TSUN cloud
|
||||
if self.remote and self.remote.stream:
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
|
||||
f'connection: [{self.remote.ifc.node_id}:'
|
||||
f'{self.remote.ifc.conn_no}]')
|
||||
await self.remote.ifc.disc()
|
||||
|
||||
async def _async_forward(self) -> None:
|
||||
"""forward handler transmits data over the remote connection"""
|
||||
if not self.remote.stream:
|
||||
await self.create_remote()
|
||||
if self.remote.stream and \
|
||||
self.remote.ifc.init_new_client_conn_cb():
|
||||
await self.remote.ifc._AsyncStream__async_write()
|
||||
if self.remote.stream:
|
||||
self.remote.ifc.update_header_cb(self.fwd_fifo.peek())
|
||||
self.fwd_fifo.logging(logging.INFO, 'Forward to '
|
||||
f'{self.remote.ifc.r_addr}:')
|
||||
self.remote.ifc._writer.write(self.fwd_fifo.get())
|
||||
await self.remote.ifc._writer.drain()
|
||||
|
||||
async def publish_outstanding_mqtt(self):
|
||||
'''Publish all outstanding MQTT topics'''
|
||||
try:
|
||||
await self.async_publ_mqtt()
|
||||
await Proxy._async_publ_mqtt_proxy_stat('proxy')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class AsyncStreamClient(AsyncStream):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
rstream: "StreamPtr", close_cb) -> None:
|
||||
AsyncStream.__init__(self, reader, writer, rstream)
|
||||
self.close_cb = close_cb
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug('AsyncStreamClient.close()')
|
||||
self.close_cb = None
|
||||
super().close()
|
||||
|
||||
async def client_loop(self, _: str) -> None:
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
await self.loop()
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
'Client loop stopped for'
|
||||
f' l{self.l_addr}')
|
||||
|
||||
if self.close_cb:
|
||||
self.close_cb()
|
||||
|
||||
async def _async_forward(self) -> None:
|
||||
"""forward handler transmits data over the remote connection"""
|
||||
if self.remote.stream:
|
||||
self.remote.ifc.update_header_cb(self.fwd_fifo.peek())
|
||||
self.fwd_fifo.logging(logging.INFO, 'Forward to '
|
||||
f'{self.remote.ifc.r_addr}:')
|
||||
self.remote.ifc._writer.write(self.fwd_fifo.get())
|
||||
await self.remote.ifc._writer.drain()
|
||||
|
||||
54
app/src/byte_fifo.py
Normal file
54
app/src/byte_fifo.py
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
if __name__ == "app.src.byte_fifo":
|
||||
from app.src.messages import hex_dump_str, hex_dump_memory
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
|
||||
|
||||
class ByteFifo:
|
||||
""" a byte FIFO buffer with trigger callback """
|
||||
def __init__(self):
|
||||
self.__buf = bytearray()
|
||||
self.__trigger_cb = None
|
||||
|
||||
def reg_trigger(self, cb) -> None:
|
||||
self.__trigger_cb = cb
|
||||
|
||||
def __iadd__(self, data):
|
||||
self.__buf.extend(data)
|
||||
return self
|
||||
|
||||
def __call__(self):
|
||||
'''triggers the observer'''
|
||||
if callable(self.__trigger_cb):
|
||||
return self.__trigger_cb()
|
||||
return None
|
||||
|
||||
def get(self, size: int = None) -> bytearray:
|
||||
'''removes size numbers of byte and return them'''
|
||||
if not size:
|
||||
data = self.__buf
|
||||
self.clear()
|
||||
else:
|
||||
data = self.__buf[:size]
|
||||
# The fast delete syntax
|
||||
self.__buf[:size] = b''
|
||||
return data
|
||||
|
||||
def peek(self, size: int = None) -> bytearray:
|
||||
'''returns size numbers of byte without removing them'''
|
||||
if not size:
|
||||
return self.__buf
|
||||
return self.__buf[:size]
|
||||
|
||||
def clear(self):
|
||||
self.__buf = bytearray()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.__buf)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return hex_dump_str(self.__buf, self.__len__())
|
||||
|
||||
def logging(self, level, info):
|
||||
hex_dump_memory(level, info, self.__buf, self.__len__())
|
||||
@@ -12,79 +12,87 @@ class Config():
|
||||
Read config.toml file and sanitize it with read().
|
||||
Get named parts of the config with get()'''
|
||||
|
||||
config = {}
|
||||
act_config = {}
|
||||
def_config = {}
|
||||
conf_schema = Schema({
|
||||
'tsun': {
|
||||
'enabled': Use(bool),
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
},
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
},
|
||||
'solarman': {
|
||||
'enabled': Use(bool),
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
},
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
},
|
||||
'mqtt': {
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
|
||||
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||
},
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
|
||||
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||
},
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
'discovery_prefix': Use(str),
|
||||
'entity_prefix': Use(str),
|
||||
'proxy_node_id': Use(str),
|
||||
'proxy_unique_id': Use(str)
|
||||
},
|
||||
'entity_prefix': Use(str),
|
||||
'proxy_node_id': Use(str),
|
||||
'proxy_unique_id': Use(str)
|
||||
},
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
Or('mqtt', 'tsun'): {
|
||||
'allow': [str],
|
||||
Optional('block', default=[]): [str]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
||||
Optional('monitor_sn', default=0): Use(int),
|
||||
Optional('node_id', default=""): And(Use(str),
|
||||
Use(lambda s: s + '/'
|
||||
if len(s) > 0 and
|
||||
s[-1] != '/' else s)),
|
||||
|
||||
Optional('suggested_area', default=""): Use(str),
|
||||
if len(s) > 0
|
||||
and s[-1] != '/'
|
||||
else s)),
|
||||
Optional('client_mode'): {
|
||||
'host': Use(str),
|
||||
Optional('port', default=8899):
|
||||
And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
},
|
||||
Optional('modbus_polling', default=True): Use(bool),
|
||||
Optional('suggested_area', default=""): Use(str),
|
||||
Optional('sensor_list', default=0x2b0): Use(int),
|
||||
Optional('pv1'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
},
|
||||
Optional('pv2'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
},
|
||||
Optional('pv3'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
},
|
||||
Optional('pv4'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
},
|
||||
Optional('pv5'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
},
|
||||
Optional('pv6'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
}
|
||||
}}
|
||||
}, ignore_extra_keys=True
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ignore_extra_keys=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def class_init(cls): # pragma: no cover
|
||||
def class_init(cls) -> None | str: # pragma: no cover
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
@@ -94,7 +102,9 @@ class Config():
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
cls.read()
|
||||
err_str = cls.read()
|
||||
del cls.conf_schema
|
||||
return err_str
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict: # pragma: no cover
|
||||
@@ -139,17 +149,17 @@ class Config():
|
||||
config[key] |= usr_config[key]
|
||||
|
||||
try:
|
||||
cls.config = cls.conf_schema.validate(config)
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
|
||||
# logging.debug(f'Readed config: "{cls.config}" ')
|
||||
# logging.debug(f'Readed config: "{cls.act_config}" ')
|
||||
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logger.error(err)
|
||||
cls.config = {}
|
||||
cls.act_config = {}
|
||||
|
||||
return err
|
||||
|
||||
@@ -159,12 +169,12 @@ class Config():
|
||||
None it returns the complete config dict'''
|
||||
|
||||
if member:
|
||||
return cls.config.get(member, {})
|
||||
return cls.act_config.get(member, {})
|
||||
else:
|
||||
return cls.config
|
||||
return cls.act_config
|
||||
|
||||
@classmethod
|
||||
def is_default(cls, member: str) -> bool:
|
||||
'''Check if the member is the default value'''
|
||||
|
||||
return cls.config.get(member) == cls.def_config.get(member)
|
||||
return cls.act_config.get(member) == cls.def_config.get(member)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import logging
|
||||
# import gc
|
||||
from async_stream import AsyncStream
|
||||
from gen3.talent import Talent
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
|
||||
class ConnectionG3(AsyncStream, Talent):
|
||||
|
||||
def __init__(self, reader, writer, addr, remote_stream, server_side: bool,
|
||||
id_str=b'') -> None:
|
||||
AsyncStream.__init__(self, reader, writer, addr)
|
||||
Talent.__init__(self, server_side, id_str)
|
||||
|
||||
self.remoteStream = remote_stream
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self):
|
||||
AsyncStream.close(self)
|
||||
Talent.close(self)
|
||||
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||
|
||||
async def async_create_remote(self) -> None:
|
||||
pass
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
pass
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __del__(self):
|
||||
super().__del__()
|
||||
@@ -11,78 +11,82 @@ else: # pragma: no cover
|
||||
|
||||
class RegisterMap:
|
||||
map = {
|
||||
0x00092ba8: Register.COLLECTOR_FW_VERSION,
|
||||
0x000927c0: Register.CHIP_TYPE,
|
||||
0x00092f90: Register.CHIP_MODEL,
|
||||
0x00095a88: Register.TRACE_URL,
|
||||
0x00095aec: Register.LOGGER_URL,
|
||||
0x0000000a: Register.PRODUCT_NAME,
|
||||
0x00000014: Register.MANUFACTURER,
|
||||
0x0000001e: Register.VERSION,
|
||||
0x00000028: Register.SERIAL_NUMBER,
|
||||
0x00000032: Register.EQUIPMENT_MODEL,
|
||||
0x00013880: Register.NO_INPUTS,
|
||||
0xffffff00: Register.INVERTER_CNT,
|
||||
0xffffff01: Register.UNKNOWN_SNR,
|
||||
0xffffff02: Register.UNKNOWN_MSG,
|
||||
0xffffff03: Register.INVALID_DATA_TYPE,
|
||||
0xffffff04: Register.INTERNAL_ERROR,
|
||||
0xffffff05: Register.UNKNOWN_CTRL,
|
||||
0xffffff06: Register.OTA_START_MSG,
|
||||
0xffffff07: Register.SW_EXCEPTION,
|
||||
0xffffff08: Register.MAX_DESIGNED_POWER,
|
||||
0xfffffffe: Register.TEST_REG1,
|
||||
0xffffffff: Register.TEST_REG2,
|
||||
0x00000640: Register.OUTPUT_POWER,
|
||||
0x000005dc: Register.RATED_POWER,
|
||||
0x00000514: Register.INVERTER_TEMP,
|
||||
0x000006a4: Register.PV1_VOLTAGE,
|
||||
0x00000708: Register.PV1_CURRENT,
|
||||
0x0000076c: Register.PV1_POWER,
|
||||
0x000007d0: Register.PV2_VOLTAGE,
|
||||
0x00000834: Register.PV2_CURRENT,
|
||||
0x00000898: Register.PV2_POWER,
|
||||
0x000008fc: Register.PV3_VOLTAGE,
|
||||
0x00000960: Register.PV3_CURRENT,
|
||||
0x000009c4: Register.PV3_POWER,
|
||||
0x00000a28: Register.PV4_VOLTAGE,
|
||||
0x00000a8c: Register.PV4_CURRENT,
|
||||
0x00000af0: Register.PV4_POWER,
|
||||
0x00000c1c: Register.PV1_DAILY_GENERATION,
|
||||
0x00000c80: Register.PV1_TOTAL_GENERATION,
|
||||
0x00000ce4: Register.PV2_DAILY_GENERATION,
|
||||
0x00000d48: Register.PV2_TOTAL_GENERATION,
|
||||
0x00000dac: Register.PV3_DAILY_GENERATION,
|
||||
0x00000e10: Register.PV3_TOTAL_GENERATION,
|
||||
0x00000e74: Register.PV4_DAILY_GENERATION,
|
||||
0x00000ed8: Register.PV4_TOTAL_GENERATION,
|
||||
0x00000b54: Register.DAILY_GENERATION,
|
||||
0x00000bb8: Register.TOTAL_GENERATION,
|
||||
0x000003e8: Register.GRID_VOLTAGE,
|
||||
0x0000044c: Register.GRID_CURRENT,
|
||||
0x000004b0: Register.GRID_FREQUENCY,
|
||||
0x000cfc38: Register.CONNECT_COUNT,
|
||||
0x000c3500: Register.SIGNAL_STRENGTH,
|
||||
0x000c96a8: Register.POWER_ON_TIME,
|
||||
0x000d0020: Register.COLLECT_INTERVAL,
|
||||
0x000cf850: Register.DATA_UP_INTERVAL,
|
||||
0x000c7f38: Register.COMMUNICATION_TYPE,
|
||||
0x00000191: Register.EVENT_401,
|
||||
0x00000192: Register.EVENT_402,
|
||||
0x00000193: Register.EVENT_403,
|
||||
0x00000194: Register.EVENT_404,
|
||||
0x00000195: Register.EVENT_405,
|
||||
0x00000196: Register.EVENT_406,
|
||||
0x00000197: Register.EVENT_407,
|
||||
0x00000198: Register.EVENT_408,
|
||||
0x00000199: Register.EVENT_409,
|
||||
0x0000019a: Register.EVENT_410,
|
||||
0x0000019b: Register.EVENT_411,
|
||||
0x0000019c: Register.EVENT_412,
|
||||
0x0000019d: Register.EVENT_413,
|
||||
0x0000019e: Register.EVENT_414,
|
||||
0x0000019f: Register.EVENT_415,
|
||||
0x000001a0: Register.EVENT_416,
|
||||
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
|
||||
0x000927c0: {'reg': Register.CHIP_TYPE},
|
||||
0x00092f90: {'reg': Register.CHIP_MODEL},
|
||||
0x00094ae8: {'reg': Register.MAC_ADDR},
|
||||
0x00095a88: {'reg': Register.TRACE_URL},
|
||||
0x00095aec: {'reg': Register.LOGGER_URL},
|
||||
0x0000000a: {'reg': Register.PRODUCT_NAME},
|
||||
0x00000014: {'reg': Register.MANUFACTURER},
|
||||
0x0000001e: {'reg': Register.VERSION},
|
||||
0x00000028: {'reg': Register.SERIAL_NUMBER},
|
||||
0x00000032: {'reg': Register.EQUIPMENT_MODEL},
|
||||
0x00013880: {'reg': Register.NO_INPUTS},
|
||||
0xffffff00: {'reg': Register.INVERTER_CNT},
|
||||
0xffffff01: {'reg': Register.UNKNOWN_SNR},
|
||||
0xffffff02: {'reg': Register.UNKNOWN_MSG},
|
||||
0xffffff03: {'reg': Register.INVALID_DATA_TYPE},
|
||||
0xffffff04: {'reg': Register.INTERNAL_ERROR},
|
||||
0xffffff05: {'reg': Register.UNKNOWN_CTRL},
|
||||
0xffffff06: {'reg': Register.OTA_START_MSG},
|
||||
0xffffff07: {'reg': Register.SW_EXCEPTION},
|
||||
0xffffff08: {'reg': Register.POLLING_INTERVAL},
|
||||
0xfffffffe: {'reg': Register.TEST_REG1},
|
||||
0xffffffff: {'reg': Register.TEST_REG2},
|
||||
0x00000640: {'reg': Register.OUTPUT_POWER},
|
||||
0x000005dc: {'reg': Register.RATED_POWER},
|
||||
0x00000514: {'reg': Register.INVERTER_TEMP},
|
||||
0x000006a4: {'reg': Register.PV1_VOLTAGE},
|
||||
0x00000708: {'reg': Register.PV1_CURRENT},
|
||||
0x0000076c: {'reg': Register.PV1_POWER},
|
||||
0x000007d0: {'reg': Register.PV2_VOLTAGE},
|
||||
0x00000834: {'reg': Register.PV2_CURRENT},
|
||||
0x00000898: {'reg': Register.PV2_POWER},
|
||||
0x000008fc: {'reg': Register.PV3_VOLTAGE},
|
||||
0x00000960: {'reg': Register.PV3_CURRENT},
|
||||
0x000009c4: {'reg': Register.PV3_POWER},
|
||||
0x00000a28: {'reg': Register.PV4_VOLTAGE},
|
||||
0x00000a8c: {'reg': Register.PV4_CURRENT},
|
||||
0x00000af0: {'reg': Register.PV4_POWER},
|
||||
0x00000c1c: {'reg': Register.PV1_DAILY_GENERATION},
|
||||
0x00000c80: {'reg': Register.PV1_TOTAL_GENERATION},
|
||||
0x00000ce4: {'reg': Register.PV2_DAILY_GENERATION},
|
||||
0x00000d48: {'reg': Register.PV2_TOTAL_GENERATION},
|
||||
0x00000dac: {'reg': Register.PV3_DAILY_GENERATION},
|
||||
0x00000e10: {'reg': Register.PV3_TOTAL_GENERATION},
|
||||
0x00000e74: {'reg': Register.PV4_DAILY_GENERATION},
|
||||
0x00000ed8: {'reg': Register.PV4_TOTAL_GENERATION},
|
||||
0x00000b54: {'reg': Register.DAILY_GENERATION},
|
||||
0x00000bb8: {'reg': Register.TOTAL_GENERATION},
|
||||
0x000003e8: {'reg': Register.GRID_VOLTAGE},
|
||||
0x0000044c: {'reg': Register.GRID_CURRENT},
|
||||
0x000004b0: {'reg': Register.GRID_FREQUENCY},
|
||||
0x000cfc38: {'reg': Register.CONNECT_COUNT},
|
||||
0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
|
||||
0x000c96a8: {'reg': Register.POWER_ON_TIME},
|
||||
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
|
||||
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
|
||||
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
|
||||
0x00000191: {'reg': Register.EVENT_401},
|
||||
0x00000192: {'reg': Register.EVENT_402},
|
||||
0x00000193: {'reg': Register.EVENT_403},
|
||||
0x00000194: {'reg': Register.EVENT_404},
|
||||
0x00000195: {'reg': Register.EVENT_405},
|
||||
0x00000196: {'reg': Register.EVENT_406},
|
||||
0x00000197: {'reg': Register.EVENT_407},
|
||||
0x00000198: {'reg': Register.EVENT_408},
|
||||
0x00000199: {'reg': Register.EVENT_409},
|
||||
0x0000019a: {'reg': Register.EVENT_410},
|
||||
0x0000019b: {'reg': Register.EVENT_411},
|
||||
0x0000019c: {'reg': Register.EVENT_412},
|
||||
0x0000019d: {'reg': Register.EVENT_413},
|
||||
0x0000019e: {'reg': Register.EVENT_414},
|
||||
0x0000019f: {'reg': Register.EVENT_415},
|
||||
0x000001a0: {'reg': Register.EVENT_416},
|
||||
0x00000064: {'reg': Register.INVERTER_STATUS},
|
||||
0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
|
||||
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +104,8 @@ class InfosG3(Infos):
|
||||
entity strings
|
||||
sug_area:str ==> suggested area string from the config file'''
|
||||
# iterate over RegisterMap.map and get the register values
|
||||
for reg in RegisterMap.map.values():
|
||||
for row in RegisterMap.map.values():
|
||||
reg = row['reg']
|
||||
res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||
if res:
|
||||
yield res
|
||||
@@ -119,9 +124,11 @@ class InfosG3(Infos):
|
||||
result = struct.unpack_from('!lB', buf, ind)
|
||||
addr = result[0]
|
||||
if addr not in RegisterMap.map:
|
||||
row = None
|
||||
info_id = -1
|
||||
else:
|
||||
info_id = RegisterMap.map[addr]
|
||||
row = RegisterMap.map[addr]
|
||||
info_id = row['reg']
|
||||
data_type = result[1]
|
||||
ind += 5
|
||||
|
||||
@@ -132,11 +139,23 @@ class InfosG3(Infos):
|
||||
errors='replace')
|
||||
ind += str_len+1
|
||||
|
||||
elif data_type == 0x00: # 'Nul' -> end
|
||||
i = elms # abort the loop
|
||||
|
||||
elif data_type == 0x41: # 'A' -> Nop ??
|
||||
ind += 0
|
||||
i += 1
|
||||
continue
|
||||
|
||||
elif data_type == 0x42: # 'B' -> byte, int8
|
||||
result = struct.unpack_from('!B', buf, ind)[0]
|
||||
ind += 1
|
||||
|
||||
elif data_type == 0x49: # 'I' -> int32
|
||||
result = struct.unpack_from('!l', buf, ind)[0]
|
||||
ind += 4
|
||||
|
||||
elif data_type == 0x53: # 'S' -> short
|
||||
elif data_type == 0x53: # 'S' -> short, int16
|
||||
result = struct.unpack_from('!h', buf, ind)[0]
|
||||
ind += 2
|
||||
|
||||
@@ -144,27 +163,38 @@ class InfosG3(Infos):
|
||||
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
||||
ind += 4
|
||||
|
||||
elif data_type == 0x4c: # 'L' -> int64
|
||||
elif data_type == 0x4c: # 'L' -> long, int64
|
||||
result = struct.unpack_from('!q', buf, ind)[0]
|
||||
ind += 8
|
||||
|
||||
else:
|
||||
self.inc_counter('Invalid_Data_Type')
|
||||
logging.error(f"Infos.parse: data_type: {data_type}"
|
||||
f" @0x{addr:04x} No:{i}"
|
||||
" not supported")
|
||||
return
|
||||
|
||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||
|
||||
if keys:
|
||||
name, update = self.update_db(keys, must_incr, result)
|
||||
yield keys[0], update
|
||||
else:
|
||||
update = False
|
||||
name = str(f'info-id.0x{addr:x}')
|
||||
|
||||
if update:
|
||||
self.tracer.log(level, f'[\'{node_id}\']GEN3: {name} :'
|
||||
f' {result}{unit}')
|
||||
result = self.__modify_val(row, result)
|
||||
|
||||
yield from self.__store_result(addr, result, info_id, node_id)
|
||||
i += 1
|
||||
|
||||
def __modify_val(self, row, result):
|
||||
if row:
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
def __store_result(self, addr, result, info_id, node_id):
|
||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||
if keys:
|
||||
name, update = self.update_db(keys, must_incr, result)
|
||||
yield keys[0], update
|
||||
else:
|
||||
update = False
|
||||
name = str(f'info-id.0x{addr:x}')
|
||||
if update:
|
||||
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
||||
f' {result}{unit}')
|
||||
|
||||
@@ -1,128 +1,12 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
from config import Config
|
||||
from inverter import Inverter
|
||||
from gen3.connection_g3 import ConnectionG3
|
||||
from aiomqtt import MqttCodeError
|
||||
from infos import Infos
|
||||
|
||||
# import gc
|
||||
|
||||
# logger = logging.getLogger('conn')
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
if __name__ == "app.src.gen3.inverter_g3":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3.talent import Talent
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
|
||||
class InverterG3(Inverter, ConnectionG3):
|
||||
'''class Inverter is a derivation of an Async_Stream
|
||||
|
||||
The class has some class method for managing common resources like a
|
||||
connection to the MQTT broker or proxy error counter which are common
|
||||
for all inverter connection
|
||||
|
||||
Instances of the class are connections to an inverter and can have an
|
||||
optional link to an remote connection to the TSUN cloud. A remote
|
||||
connection dies with the inverter connection.
|
||||
|
||||
class methods:
|
||||
class_init(): initialize the common resources of the proxy (MQTT
|
||||
broker, Proxy DB, etc). Must be called before the
|
||||
first inverter instance can be created
|
||||
class_close(): release the common resources of the proxy. Should not
|
||||
be called before any instances of the class are
|
||||
destroyed
|
||||
|
||||
methods:
|
||||
server_loop(addr): Async loop method for receiving messages from the
|
||||
inverter (server-side)
|
||||
client_loop(addr): Async loop method for receiving messages from the
|
||||
TSUN cloud (client-side)
|
||||
async_create_remote(): Establish a client connection to the TSUN cloud
|
||||
async_publ_mqtt(): Publish data to MQTT broker
|
||||
close(): Release method which must be called before a instance can be
|
||||
destroyed
|
||||
'''
|
||||
|
||||
def __init__(self, reader, writer, addr):
|
||||
super().__init__(reader, writer, addr, None, True)
|
||||
self.__ha_restarts = -1
|
||||
|
||||
async def async_create_remote(self) -> None:
|
||||
'''Establish a client connection to the TSUN cloud'''
|
||||
tsun = Config.get('tsun')
|
||||
host = tsun['host']
|
||||
port = tsun['port']
|
||||
addr = (host, port)
|
||||
|
||||
try:
|
||||
logging.info(f'Connected to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
||||
False, self.id_str)
|
||||
asyncio.create_task(self.client_loop(addr))
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
logging.info(f'{error}')
|
||||
except Exception:
|
||||
self.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception for {addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
'''publish data to MQTT broker'''
|
||||
# check if new inverter or collector infos are available or when the
|
||||
# home assistant has changed the status back to online
|
||||
try:
|
||||
if (('inverter' in self.new_data and self.new_data['inverter'])
|
||||
or ('collector' in self.new_data and
|
||||
self.new_data['collector'])
|
||||
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||
await self._register_proxy_stat_home_assistant()
|
||||
await self.__register_home_assistant()
|
||||
self.__ha_restarts = self.mqtt.ha_restarts
|
||||
|
||||
for key in self.new_data:
|
||||
await self.__async_publ_mqtt_packet(key)
|
||||
for key in Infos.new_stat_data:
|
||||
await self._async_publ_mqtt_proxy_stat(key)
|
||||
|
||||
except MqttCodeError as error:
|
||||
logging.error(f'Mqtt except: {error}')
|
||||
except Exception:
|
||||
self.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def __async_publ_mqtt_packet(self, key):
|
||||
db = self.db.db
|
||||
if key in db and self.new_data[key]:
|
||||
data_json = json.dumps(db[key])
|
||||
node_id = self.node_id
|
||||
logger_mqtt.debug(f'{key}: {data_json}')
|
||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
self.new_data[key] = False
|
||||
|
||||
async def __register_home_assistant(self) -> None:
|
||||
'''register all our topics at home assistant'''
|
||||
for data_json, component, node_id, id in self.db.ha_confs(
|
||||
self.entity_prfx, self.node_id, self.unique_id,
|
||||
self.sug_area):
|
||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
||||
f" node_id:'{node_id}' {data_json}")
|
||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
||||
f"/{node_id}{id}/config", data_json)
|
||||
|
||||
self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}')
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
|
||||
super().close() # call close handler in the parent class
|
||||
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
||||
|
||||
def __del__(self):
|
||||
logging.debug("InverterG3.__del__")
|
||||
super().__del__()
|
||||
class InverterG3(InverterBase):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter):
|
||||
super().__init__(reader, writer, 'tsun', Talent)
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import struct
|
||||
import logging
|
||||
import time
|
||||
from zoneinfo import ZoneInfo
|
||||
from datetime import datetime
|
||||
from tzlocal import get_localzone
|
||||
|
||||
if __name__ == "app.src.gen3.talent":
|
||||
from app.src.messages import hex_dump_memory, Message
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.infos_g3 import InfosG3
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_memory, Message
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -35,46 +42,82 @@ class Control:
|
||||
|
||||
|
||||
class Talent(Message):
|
||||
def __init__(self, server_side: bool, id_str=b''):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
|
||||
MB_START_TIMEOUT = 40
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
|
||||
|
||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||
client_mode: bool = False, id_str=b''):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=15)
|
||||
ifc.rx_set_cb(self.read)
|
||||
ifc.prot_set_timeout_cb(self._timeout)
|
||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||
ifc.prot_set_update_header_cb(self._update_header)
|
||||
self.addr = addr
|
||||
self.ifc = ifc
|
||||
self.conn_no = ifc.get_conn_no()
|
||||
self.await_conn_resp_cnt = 0
|
||||
self.id_str = id_str
|
||||
self.contact_name = b''
|
||||
self.contact_mail = b''
|
||||
self.ts_offset = 0 # time offset between tsun cloud and local
|
||||
self.db = InfosG3()
|
||||
self.switch = {
|
||||
0x00: self.msg_contact_info,
|
||||
0x13: self.msg_ota_update,
|
||||
0x22: self.msg_get_time,
|
||||
0x99: self.msg_heartbeat,
|
||||
0x71: self.msg_collector_data,
|
||||
# 0x76:
|
||||
0x77: self.msg_modbus,
|
||||
# 0x78:
|
||||
0x87: self.msg_modbus2,
|
||||
0x04: self.msg_inverter_data,
|
||||
}
|
||||
self.log_lvl = {
|
||||
0x00: logging.INFO,
|
||||
0x13: logging.INFO,
|
||||
0x22: logging.INFO,
|
||||
0x99: logging.INFO,
|
||||
0x71: logging.INFO,
|
||||
# 0x76:
|
||||
0x77: self.get_modbus_log_lvl,
|
||||
# 0x78:
|
||||
0x87: self.get_modbus_log_lvl,
|
||||
0x04: logging.INFO,
|
||||
}
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
||||
self.modbus_polling = False
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.debug('Talent.close()')
|
||||
# we have refernces to methods of this class in self.switch
|
||||
if self.server_side:
|
||||
# set inverter state to offline, if output power is very low
|
||||
logging.debug('close power: '
|
||||
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
|
||||
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
|
||||
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
|
||||
self.new_data['env'] = True
|
||||
|
||||
# we have references to methods of this class in self.switch
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = self.STATE_CLOSED
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
self.ifc.rx_set_cb(None)
|
||||
self.ifc.prot_set_timeout_cb(None)
|
||||
self.ifc.prot_set_init_new_client_conn_cb(None)
|
||||
self.ifc.prot_set_update_header_cb(None)
|
||||
self.ifc = None
|
||||
super().close()
|
||||
|
||||
def __set_serial_no(self, serial_no: str):
|
||||
@@ -89,6 +132,7 @@ class Talent(Message):
|
||||
inv = inverters[serial_no]
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
self.modbus_polling = inv['modbus_polling']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
self.db.set_pv_module_details(inv)
|
||||
else:
|
||||
@@ -102,61 +146,82 @@ class Talent(Message):
|
||||
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||
|
||||
self.unique_id = serial_no
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, serial_no)
|
||||
|
||||
def read(self) -> None:
|
||||
def read(self) -> float:
|
||||
'''process all received messages in the _recv_buffer'''
|
||||
self._read()
|
||||
while True:
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self.ifc.rx_peek(), self.ifc.rx_len())
|
||||
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
||||
if self.header_valid and \
|
||||
self.ifc.rx_len() >= (self.header_len + self.data_len):
|
||||
if self.state == State.init:
|
||||
self.state = State.received # received 1st package
|
||||
|
||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||
self.data_len):
|
||||
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
|
||||
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
||||
self._recv_buffer, self.header_len+self.data_len)
|
||||
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:'
|
||||
f' BufLen: {self.ifc.rx_len()}'
|
||||
f' HdrLen: {self.header_len}'
|
||||
f' DtaLen: {self.data_len}')
|
||||
|
||||
self.__set_serial_no(self.id_str.decode("utf-8"))
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
self.__set_serial_no(self.id_str.decode("utf-8"))
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
else:
|
||||
return 0 # don not wait before sending a response
|
||||
|
||||
def forward(self, buffer, buflen) -> None:
|
||||
def forward(self) -> None:
|
||||
'''add the actual receive msg to the forwarding queue'''
|
||||
tsun = Config.get('tsun')
|
||||
if tsun['enabled']:
|
||||
self._forward_buffer = buffer[:buflen]
|
||||
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
|
||||
buffer, buflen)
|
||||
buflen = self.header_len+self.data_len
|
||||
buffer = self.ifc.rx_peek(buflen)
|
||||
self.ifc.fwd_add(buffer)
|
||||
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
|
||||
|
||||
self.__parse_header(self._forward_buffer,
|
||||
len(self._forward_buffer))
|
||||
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}')
|
||||
return
|
||||
|
||||
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
|
||||
if self.state != self.STATE_UP:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
|
||||
self.__build_header(0x70, 0x77)
|
||||
self._send_buffer += b'\x00\x01\xa3\x28' # fixme
|
||||
self._send_buffer += struct.pack('!B', len(modbus_pdu))
|
||||
self._send_buffer += modbus_pdu
|
||||
self.ifc.tx_add(b'\x00\x01\xa3\x28') # magic ?
|
||||
self.ifc.tx_add(struct.pack('!B', len(modbus_pdu)))
|
||||
self.ifc.tx_add(modbus_pdu)
|
||||
self.__finish_send_msg()
|
||||
|
||||
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
||||
self._send_buffer, len(self._send_buffer))
|
||||
self.writer.write(self._send_buffer)
|
||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
if 2 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
||||
else:
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
contact_name = self.contact_name
|
||||
contact_mail = self.contact_mail
|
||||
@@ -164,9 +229,9 @@ class Talent(Message):
|
||||
self.msg_id = 0
|
||||
self.await_conn_resp_cnt += 1
|
||||
self.__build_header(0x91)
|
||||
self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
|
||||
f'{len(contact_mail)+1}p',
|
||||
contact_name, contact_mail)
|
||||
self.ifc.tx_add(struct.pack(f'!{len(contact_name)+1}p'
|
||||
f'{len(contact_mail)+1}p',
|
||||
contact_name, contact_mail))
|
||||
|
||||
self.__finish_send_msg()
|
||||
return True
|
||||
@@ -190,14 +255,44 @@ class Talent(Message):
|
||||
return switch.get(type, '???')
|
||||
|
||||
def _timestamp(self): # pragma: no cover
|
||||
if False:
|
||||
# utc as epoche
|
||||
ts = time.time()
|
||||
else:
|
||||
# convert localtime in epoche
|
||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||
'''returns timestamp fo the inverter as localtime
|
||||
since 1.1.1970 in msec'''
|
||||
# convert localtime in epoche
|
||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||
return round(ts*1000)
|
||||
|
||||
def _utcfromts(self, ts: float):
|
||||
'''converts inverter timestamp into unix time (epoche)'''
|
||||
dt = datetime.fromtimestamp(ts/1000, tz=ZoneInfo("UTC")). \
|
||||
replace(tzinfo=get_localzone())
|
||||
return dt.timestamp()
|
||||
|
||||
def _utc(self): # pragma: no cover
|
||||
'''returns unix time (epoche)'''
|
||||
return datetime.now().timestamp()
|
||||
|
||||
def _update_header(self, _forward_buffer):
|
||||
'''update header for message before forwarding,
|
||||
add time offset to timestamp'''
|
||||
_len = len(_forward_buffer)
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('!lB', _forward_buffer, 0)
|
||||
msg_len = 4 + result[0]
|
||||
id_len = result[1] # len of variable id string
|
||||
if _len < 2*id_len + 21:
|
||||
return
|
||||
|
||||
result = struct.unpack_from('!B', _forward_buffer, id_len+6)
|
||||
msg_code = result[0]
|
||||
if msg_code == 0x71 or msg_code == 0x04:
|
||||
result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len)
|
||||
ts = result[0] + self.ts_offset
|
||||
logger.debug(f'offset: {self.ts_offset:08x}'
|
||||
f' proxy-time: {ts:08x}')
|
||||
struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts)
|
||||
ofs += msg_len
|
||||
|
||||
# check if there is a complete header in the buffer, parse it
|
||||
# and set
|
||||
# self.header_len
|
||||
@@ -213,8 +308,15 @@ class Talent(Message):
|
||||
if (buf_len < 5): # enough bytes to read len and id_len?
|
||||
return
|
||||
result = struct.unpack_from('!lB', buf, 0)
|
||||
len = result[0] # len of complete message
|
||||
msg_len = result[0] # len of complete message
|
||||
id_len = result[1] # len of variable id string
|
||||
if id_len > 17:
|
||||
logger.warning(f'len of ID string must == 16 but is {id_len}')
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
return
|
||||
|
||||
hdr_len = 5+id_len+2
|
||||
|
||||
@@ -227,37 +329,38 @@ class Talent(Message):
|
||||
self.id_str = result[0]
|
||||
self.ctrl = Control(result[1])
|
||||
self.msg_id = result[2]
|
||||
self.data_len = len-id_len-3
|
||||
self.data_len = msg_len-id_len-3
|
||||
self.header_len = hdr_len
|
||||
self.header_valid = True
|
||||
return
|
||||
|
||||
def __build_header(self, ctrl, msg_id=None) -> None:
|
||||
if not msg_id:
|
||||
msg_id = self.msg_id
|
||||
self.send_msg_ofs = len(self._send_buffer)
|
||||
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
|
||||
0, self.id_str, ctrl, msg_id)
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
self.ifc.tx_add(struct.pack(f'!l{len(self.id_str)+1}pBB',
|
||||
0, self.id_str, ctrl, msg_id))
|
||||
fnc = self.switch.get(msg_id, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'tx') +
|
||||
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||
|
||||
def __finish_send_msg(self) -> None:
|
||||
_len = len(self._send_buffer) - self.send_msg_ofs
|
||||
struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4)
|
||||
_len = self.ifc.tx_len() - self.send_msg_ofs
|
||||
struct.pack_into('!l', self.ifc.tx_peek(), self.send_msg_ofs,
|
||||
_len-4)
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||
if self.unique_id:
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||
f' Ctl: {int(self.ctrl):#02x} ({self.state}) '
|
||||
f'Msg: {fnc.__name__!r}')
|
||||
fnc()
|
||||
else:
|
||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
self._recv_buffer = self._recv_buffer[(self.header_len+self.data_len):]
|
||||
self.ifc.rx_get(self.header_len+self.data_len)
|
||||
self.header_valid = False
|
||||
|
||||
'''
|
||||
@@ -267,181 +370,241 @@ class Talent(Message):
|
||||
if self.ctrl.is_ind():
|
||||
if self.server_side and self.__process_contact_info():
|
||||
self.__build_header(0x91)
|
||||
self._send_buffer += b'\x01'
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
# don't forward this contact info here, we will build one
|
||||
# when the remote connection is established
|
||||
elif self.await_conn_resp_cnt > 0:
|
||||
self.await_conn_resp_cnt -= 1
|
||||
else:
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
return
|
||||
self.forward()
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def __process_contact_info(self) -> bool:
|
||||
result = struct.unpack_from('!B', self._recv_buffer, self.header_len)
|
||||
buf = self.ifc.rx_peek()
|
||||
result = struct.unpack_from('!B', buf, self.header_len)
|
||||
name_len = result[0]
|
||||
if self.data_len < name_len+2:
|
||||
if self.data_len == 1: # this is a response withone status byte
|
||||
return False
|
||||
result = struct.unpack_from(f'!{name_len+1}pB', self._recv_buffer,
|
||||
self.header_len)
|
||||
self.contact_name = result[0]
|
||||
mail_len = result[1]
|
||||
logger.info(f'name: {self.contact_name}')
|
||||
if self.data_len >= name_len+2:
|
||||
result = struct.unpack_from(f'!{name_len+1}pB', buf,
|
||||
self.header_len)
|
||||
self.contact_name = result[0]
|
||||
mail_len = result[1]
|
||||
logger.info(f'name: {self.contact_name}')
|
||||
|
||||
result = struct.unpack_from(f'!{mail_len+1}p', self._recv_buffer,
|
||||
self.header_len+name_len+1)
|
||||
self.contact_mail = result[0]
|
||||
result = struct.unpack_from(f'!{mail_len+1}p', buf,
|
||||
self.header_len+name_len+1)
|
||||
self.contact_mail = result[0]
|
||||
logger.info(f'mail: {self.contact_mail}')
|
||||
return True
|
||||
|
||||
def msg_get_time(self):
|
||||
tsun = Config.get('tsun')
|
||||
if tsun['enabled']:
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len >= 8:
|
||||
ts = self._timestamp()
|
||||
result = struct.unpack_from('!q', self._recv_buffer,
|
||||
self.header_len)
|
||||
logger.debug(f'tsun-time: {result[0]:08x}'
|
||||
f' proxy-time: {ts:08x}')
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len == 0:
|
||||
if self.state == State.up:
|
||||
self.state = State.pend # block MODBUS cmds
|
||||
|
||||
ts = self._timestamp()
|
||||
logger.debug(f'time: {ts:08x}')
|
||||
self.__build_header(0x91)
|
||||
self.ifc.tx_add(struct.pack('!q', ts))
|
||||
self.__finish_send_msg()
|
||||
|
||||
elif self.data_len >= 8:
|
||||
ts = self._timestamp()
|
||||
result = struct.unpack_from('!q', self.ifc.rx_peek(),
|
||||
self.header_len)
|
||||
self.ts_offset = result[0]-ts
|
||||
if self.ifc.remote.stream:
|
||||
self.ifc.remote.stream.ts_offset = self.ts_offset
|
||||
logger.debug(f'tsun-time: {int(result[0]):08x}'
|
||||
f' proxy-time: {ts:08x}'
|
||||
f' offset: {self.ts_offset}')
|
||||
return # ignore received response
|
||||
else:
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len == 0:
|
||||
ts = self._timestamp()
|
||||
logger.debug(f'time: {ts:08x}')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
|
||||
self.__build_header(0x91)
|
||||
self._send_buffer += struct.pack('!q', ts)
|
||||
self.__finish_send_msg()
|
||||
self.forward()
|
||||
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
def msg_heartbeat(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.ifc.tx_add(b'\x02')
|
||||
self.__finish_send_msg()
|
||||
|
||||
result = struct.unpack_from('!Bq', self.ifc.rx_peek(),
|
||||
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}')
|
||||
struct.pack_into('!Bq', self.ifc.rx_peek(),
|
||||
self.header_len, resp_code, ts)
|
||||
elif self.ctrl.is_resp():
|
||||
result = struct.unpack_from('!B', self.ifc.rx_peek(),
|
||||
self.header_len)
|
||||
resp_code = result[0]
|
||||
logging.debug(f'Heartbeat-RespCode: {resp_code}')
|
||||
return
|
||||
else:
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
|
||||
self.forward()
|
||||
|
||||
def parse_msg_header(self):
|
||||
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
||||
result = struct.unpack_from('!lB', self.ifc.rx_peek(),
|
||||
self.header_len)
|
||||
|
||||
data_id = result[0] # len of complete message
|
||||
id_len = result[1] # len of variable id string
|
||||
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
|
||||
logger.debug(f'Data_ID: 0x{data_id:08x} id_len: {id_len}')
|
||||
|
||||
msg_hdr_len = 5+id_len+9
|
||||
|
||||
result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer,
|
||||
result = struct.unpack_from(f'!{id_len+1}pBq', self.ifc.rx_peek(),
|
||||
self.header_len + 4)
|
||||
|
||||
timestamp = result[2]
|
||||
logger.debug(f'ID: {result[0]} B: {result[1]}')
|
||||
logger.debug(f'time: {result[2]:08x}')
|
||||
logger.debug(f'time: {timestamp:08x}')
|
||||
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
|
||||
# "%Y-%m-%d %H:%M:%S")}')
|
||||
return msg_hdr_len
|
||||
return msg_hdr_len, timestamp
|
||||
|
||||
def msg_collector_data(self):
|
||||
if self.ctrl.is_ind():
|
||||
self.__build_header(0x99)
|
||||
self._send_buffer += b'\x01'
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.state = self.STATE_UP
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def msg_inverter_data(self):
|
||||
if self.ctrl.is_ind():
|
||||
self.__build_header(0x99)
|
||||
self._send_buffer += b'\x01'
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.state = self.STATE_UP
|
||||
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)
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def __process_data(self):
|
||||
msg_hdr_len = self.parse_msg_header()
|
||||
msg_hdr_len, ts = self.parse_msg_header()
|
||||
|
||||
for key, update in self.db.parse(self._recv_buffer, self.header_len
|
||||
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
|
||||
+ msg_hdr_len, self.node_id):
|
||||
if update:
|
||||
self._set_mqtt_timestamp(key, self._utcfromts(ts))
|
||||
self.new_data[key] = True
|
||||
|
||||
def msg_ota_update(self):
|
||||
if self.ctrl.is_req():
|
||||
self.inc_counter('OTA_Start_Msg')
|
||||
elif self.ctrl.is_ind():
|
||||
pass
|
||||
pass # Ok, nothing to do
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def parse_modbus_header(self):
|
||||
|
||||
msg_hdr_len = 5
|
||||
|
||||
result = struct.unpack_from('!lBB', self._recv_buffer,
|
||||
result = struct.unpack_from('!lBB', self.ifc.rx_peek(),
|
||||
self.header_len)
|
||||
modbus_len = result[1]
|
||||
# logger.debug(f'Ref: {result[0]}')
|
||||
# logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}')
|
||||
return msg_hdr_len, modbus_len
|
||||
|
||||
def parse_modbus_header2(self):
|
||||
|
||||
msg_hdr_len = 6
|
||||
|
||||
result = struct.unpack_from('!lBBB', self.ifc.rx_peek(),
|
||||
self.header_len)
|
||||
modbus_len = result[2]
|
||||
return msg_hdr_len, modbus_len
|
||||
|
||||
def get_modbus_log_lvl(self) -> int:
|
||||
if self.ctrl.is_req():
|
||||
return logging.INFO
|
||||
elif self.ctrl.is_ind():
|
||||
if self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
elif self.ctrl.is_ind() and self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
return logging.WARNING
|
||||
|
||||
def msg_modbus(self):
|
||||
hdr_len, modbus_len = self.parse_modbus_header()
|
||||
data = self._recv_buffer[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
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.ifc.rx_peek()[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
|
||||
if self.ctrl.is_req():
|
||||
if self.remoteStream.mb.recv_req(data[hdr_len:],
|
||||
self.msg_forward):
|
||||
rstream = self.ifc.remote.stream
|
||||
if rstream.mb.recv_req(data[hdr_len:], rstream.msg_forward):
|
||||
self.inc_counter('Modbus_Command')
|
||||
else:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
elif self.ctrl.is_ind():
|
||||
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
|
||||
self.modbus_elms = 0
|
||||
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
|
||||
if not self.server_side:
|
||||
logger.warning('Unknown Message')
|
||||
self.inc_counter('Unknown_Msg')
|
||||
return
|
||||
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||
hdr_len:],
|
||||
self.node_id):
|
||||
if update:
|
||||
self._set_mqtt_timestamp(key, self._utc())
|
||||
self.new_data[key] = True
|
||||
self.modbus_elms += 1 # count for unit tests
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def msg_forward(self):
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
def msg_unknown(self):
|
||||
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
|
||||
self.inc_counter('Unknown_Msg')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
self.forward()
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import logging
|
||||
# import gc
|
||||
from async_stream import AsyncStream
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
|
||||
class ConnectionG3P(AsyncStream, SolarmanV5):
|
||||
|
||||
def __init__(self, reader, writer, addr, remote_stream,
|
||||
server_side: bool) -> None:
|
||||
AsyncStream.__init__(self, reader, writer, addr)
|
||||
SolarmanV5.__init__(self, server_side)
|
||||
|
||||
self.remoteStream = remote_stream
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self):
|
||||
AsyncStream.close(self)
|
||||
SolarmanV5.close(self)
|
||||
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||
|
||||
async def async_create_remote(self) -> None:
|
||||
pass
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
pass
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __del__(self):
|
||||
super().__del__()
|
||||
@@ -3,9 +3,9 @@ import struct
|
||||
from typing import Generator
|
||||
|
||||
if __name__ == "app.src.gen3plus.infos_g3p":
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.infos import Infos, Register, ProxyMode
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register
|
||||
from infos import Infos, Register, ProxyMode
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
@@ -14,15 +14,18 @@ class RegisterMap:
|
||||
__slots__ = ()
|
||||
map = {
|
||||
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'eval': 'round(result/60)'}, # noqa: E501
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'eval': 'round(result/60)', 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1}, # 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
|
||||
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
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||
|
||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
|
||||
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||
@@ -56,19 +59,31 @@ class RegisterMap:
|
||||
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', '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
|
||||
0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
|
||||
|
||||
0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
|
||||
class InfosG3P(Infos):
|
||||
def __init__(self):
|
||||
def __init__(self, client_mode: bool):
|
||||
super().__init__()
|
||||
self.client_mode = client_mode
|
||||
self.set_db_def_value(Register.MANUFACTURER, 'TSUN')
|
||||
self.set_db_def_value(Register.EQUIPMENT_MODEL, 'TSOL-MSxx00')
|
||||
self.set_db_def_value(Register.CHIP_TYPE, 'IGEN TECH')
|
||||
self.set_db_def_value(Register.NO_INPUTS, 4)
|
||||
|
||||
def __hide_topic(self, row: dict) -> bool:
|
||||
if 'dep' in row:
|
||||
mode = row['dep']
|
||||
if self.client_mode:
|
||||
return mode != ProxyMode.CLIENT
|
||||
else:
|
||||
return mode != ProxyMode.SERVER
|
||||
return False
|
||||
|
||||
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
|
||||
sug_area: str = '') \
|
||||
@@ -84,7 +99,10 @@ class InfosG3P(Infos):
|
||||
# iterate over RegisterMap.map and get the register values
|
||||
for row in RegisterMap.map.values():
|
||||
info_id = row['reg']
|
||||
res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||
if self.__hide_topic(row):
|
||||
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
|
||||
else:
|
||||
res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||
if res:
|
||||
yield res
|
||||
|
||||
@@ -103,15 +121,7 @@ class InfosG3P(Infos):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
info_id = row['reg']
|
||||
fmt = row['fmt']
|
||||
res = struct.unpack_from(fmt, buf, addr)
|
||||
result = res[0]
|
||||
if isinstance(result, (bytearray, bytes)):
|
||||
result = result.decode().split('\x00')[0]
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
result = self.__get_value(buf, addr, row)
|
||||
|
||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||
|
||||
@@ -123,5 +133,18 @@ class InfosG3P(Infos):
|
||||
update = False
|
||||
|
||||
if update:
|
||||
self.tracer.log(level, f'[\'{node_id}\']GEN3PLUS: {name}'
|
||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
|
||||
def __get_value(self, buf, idx, row):
|
||||
'''Get a value from buf and interpret as in row'''
|
||||
fmt = row['fmt']
|
||||
res = struct.unpack_from(fmt, buf, idx)
|
||||
result = res[0]
|
||||
if isinstance(result, (bytearray, bytes)):
|
||||
result = result.decode().split('\x00')[0]
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
@@ -1,128 +1,15 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
from config import Config
|
||||
from inverter import Inverter
|
||||
from gen3plus.connection_g3p import ConnectionG3P
|
||||
from aiomqtt import MqttCodeError
|
||||
from infos import Infos
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
# import gc
|
||||
|
||||
# logger = logging.getLogger('conn')
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
if __name__ == "app.src.gen3plus.inverter_g3p":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
|
||||
|
||||
class InverterG3P(Inverter, ConnectionG3P):
|
||||
'''class Inverter is a derivation of an Async_Stream
|
||||
|
||||
The class has some class method for managing common resources like a
|
||||
connection to the MQTT broker or proxy error counter which are common
|
||||
for all inverter connection
|
||||
|
||||
Instances of the class are connections to an inverter and can have an
|
||||
optional link to an remote connection to the TSUN cloud. A remote
|
||||
connection dies with the inverter connection.
|
||||
|
||||
class methods:
|
||||
class_init(): initialize the common resources of the proxy (MQTT
|
||||
broker, Proxy DB, etc). Must be called before the
|
||||
first inverter instance can be created
|
||||
class_close(): release the common resources of the proxy. Should not
|
||||
be called before any instances of the class are
|
||||
destroyed
|
||||
|
||||
methods:
|
||||
server_loop(addr): Async loop method for receiving messages from the
|
||||
inverter (server-side)
|
||||
client_loop(addr): Async loop method for receiving messages from the
|
||||
TSUN cloud (client-side)
|
||||
async_create_remote(): Establish a client connection to the TSUN cloud
|
||||
async_publ_mqtt(): Publish data to MQTT broker
|
||||
close(): Release method which must be called before a instance can be
|
||||
destroyed
|
||||
'''
|
||||
|
||||
def __init__(self, reader, writer, addr):
|
||||
super().__init__(reader, writer, addr, None, True)
|
||||
self.__ha_restarts = -1
|
||||
|
||||
async def async_create_remote(self) -> None:
|
||||
'''Establish a client connection to the TSUN cloud'''
|
||||
tsun = Config.get('solarman')
|
||||
host = tsun['host']
|
||||
port = tsun['port']
|
||||
addr = (host, port)
|
||||
|
||||
try:
|
||||
logging.info(f'Connected to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
||||
False)
|
||||
asyncio.create_task(self.client_loop(addr))
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
logging.info(f'{error}')
|
||||
except Exception:
|
||||
self.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception for {addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
'''publish data to MQTT broker'''
|
||||
# check if new inverter or collector infos are available or when the
|
||||
# home assistant has changed the status back to online
|
||||
try:
|
||||
if (('inverter' in self.new_data and self.new_data['inverter'])
|
||||
or ('collector' in self.new_data and
|
||||
self.new_data['collector'])
|
||||
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||
await self._register_proxy_stat_home_assistant()
|
||||
await self.__register_home_assistant()
|
||||
self.__ha_restarts = self.mqtt.ha_restarts
|
||||
|
||||
for key in self.new_data:
|
||||
await self.__async_publ_mqtt_packet(key)
|
||||
for key in Infos.new_stat_data:
|
||||
await self._async_publ_mqtt_proxy_stat(key)
|
||||
|
||||
except MqttCodeError as error:
|
||||
logging.error(f'Mqtt except: {error}')
|
||||
except Exception:
|
||||
self.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def __async_publ_mqtt_packet(self, key):
|
||||
db = self.db.db
|
||||
if key in db and self.new_data[key]:
|
||||
data_json = json.dumps(db[key])
|
||||
node_id = self.node_id
|
||||
logger_mqtt.debug(f'{key}: {data_json}')
|
||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
self.new_data[key] = False
|
||||
|
||||
async def __register_home_assistant(self) -> None:
|
||||
'''register all our topics at home assistant'''
|
||||
for data_json, component, node_id, id in self.db.ha_confs(
|
||||
self.entity_prfx, self.node_id, self.unique_id,
|
||||
self.sug_area):
|
||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
||||
f" node_id:'{node_id}' {data_json}")
|
||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
||||
f"/{node_id}{id}/config", data_json)
|
||||
|
||||
self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}')
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug(f'InverterG3P.close() l{self.l_addr} | r{self.r_addr}')
|
||||
super().close() # call close handler in the parent class
|
||||
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
||||
|
||||
def __del__(self):
|
||||
logging.debug("InverterG3P.__del__")
|
||||
super().__del__()
|
||||
class InverterG3P(InverterBase):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
client_mode: bool = False):
|
||||
super().__init__(reader, writer, 'solarman',
|
||||
SolarmanV5, client_mode)
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import struct
|
||||
# import json
|
||||
import logging
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||
from app.src.messages import hex_dump_memory, Message
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import hex_dump_memory, Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_memory, Message
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from config import Config
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register
|
||||
# import traceback
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -51,17 +53,35 @@ class Sequence():
|
||||
class SolarmanV5(Message):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
MB_START_TIMEOUT = 40
|
||||
'''start delay for Modbus polling in server mode'''
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
'''regular Modbus polling time in server mode'''
|
||||
MB_CLIENT_DATA_UP = 30
|
||||
'''Data up time in client mode'''
|
||||
HDR_FMT = '<BLLL'
|
||||
'''format string for packing of the header'''
|
||||
|
||||
def __init__(self, server_side: bool):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
|
||||
def __init__(self, addr, ifc: "AsyncIfc",
|
||||
server_side: bool, client_mode: bool):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=8)
|
||||
ifc.rx_set_cb(self.read)
|
||||
ifc.prot_set_timeout_cb(self._timeout)
|
||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||
ifc.prot_set_update_header_cb(self._update_header)
|
||||
|
||||
self.addr = addr
|
||||
self.ifc = ifc
|
||||
self.conn_no = ifc.get_conn_no()
|
||||
self.header_len = 11 # overwrite construcor in class Message
|
||||
self.control = 0
|
||||
self.seq = Sequence(server_side)
|
||||
self.snr = 0
|
||||
self.db = InfosG3P()
|
||||
self.db = InfosG3P(client_mode)
|
||||
self.time_ofs = 0
|
||||
self.forward_at_cmd_resp = False
|
||||
self.no_forwarding = False
|
||||
'''not allowed to connect to TSUN cloud by connection type'''
|
||||
self.switch = {
|
||||
|
||||
0x4210: self.msg_data_ind, # real time data
|
||||
@@ -122,39 +142,93 @@ class SolarmanV5(Message):
|
||||
if 'at_acl' in g3p_cnf: # pragma: no cover
|
||||
self.at_acl = g3p_cnf['at_acl']
|
||||
|
||||
self.node_id = 'G3P' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
||||
'''timer value for next Modbus polling request'''
|
||||
self.modbus_polling = False
|
||||
self.sensor_list = 0x0000
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.debug('Solarman.close()')
|
||||
# we have refernces to methods of this class in self.switch
|
||||
if self.server_side:
|
||||
# set inverter state to offline, if output power is very low
|
||||
logging.debug('close power: '
|
||||
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
|
||||
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
|
||||
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
|
||||
self.new_data['env'] = True
|
||||
|
||||
# we have references to methods of this class in self.switch
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = self.STATE_CLOSED
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
self.ifc.rx_set_cb(None)
|
||||
self.ifc.prot_set_timeout_cb(None)
|
||||
self.ifc.prot_set_init_new_client_conn_cb(None)
|
||||
self.ifc.prot_set_update_header_cb(None)
|
||||
self.ifc = None
|
||||
super().close()
|
||||
|
||||
async def send_start_cmd(self, snr: int, host: str,
|
||||
start_timeout=MB_CLIENT_DATA_UP):
|
||||
self.no_forwarding = True
|
||||
self.snr = snr
|
||||
self.__set_serial_no(snr)
|
||||
self.mb_timeout = start_timeout
|
||||
self.db.set_db_def_value(Register.IP_ADDRESS, host)
|
||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||
self.mb_timeout)
|
||||
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
||||
120)
|
||||
self.new_data['controller'] = True
|
||||
|
||||
self.state = State.up
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
def new_state_up(self):
|
||||
if self.state is not State.up:
|
||||
self.state = State.up
|
||||
if (self.modbus_polling):
|
||||
self.mb_timer.start(self.mb_first_timeout)
|
||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||
self.mb_timeout)
|
||||
|
||||
def __set_config_parms(self, inv: dict):
|
||||
'''init connection with params from the configuration'''
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
self.modbus_polling = inv['modbus_polling']
|
||||
self.sensor_list = inv['sensor_list']
|
||||
|
||||
def __set_serial_no(self, snr: int):
|
||||
'''check the serial number and configure the inverter connection'''
|
||||
serial_no = str(snr)
|
||||
if self.unique_id == serial_no:
|
||||
logger.debug(f'SerialNo: {serial_no}')
|
||||
else:
|
||||
found = False
|
||||
inverters = Config.get('inverters')
|
||||
# logger.debug(f'Inverters: {inverters}')
|
||||
|
||||
for inv in inverters.values():
|
||||
for key, inv in inverters.items():
|
||||
# logger.debug(f'key: {key} -> {inv}')
|
||||
if (type(inv) is dict and 'monitor_sn' in inv
|
||||
and inv['monitor_sn'] == snr):
|
||||
found = True
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
self.__set_config_parms(inv)
|
||||
self.db.set_pv_module_details(inv)
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
|
||||
if not found:
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
|
||||
break
|
||||
else:
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||
@@ -162,52 +236,54 @@ class SolarmanV5(Message):
|
||||
self.unique_id = None
|
||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
||||
return
|
||||
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||
logger.warning(f'SerialNo {serial_no} not known but accepted!')
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
def read(self) -> None:
|
||||
def read(self) -> float:
|
||||
'''process all received messages in the _recv_buffer'''
|
||||
self._read()
|
||||
while True:
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self.ifc.rx_peek(),
|
||||
self.ifc.rx_len())
|
||||
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
||||
if self.header_valid and self.ifc.rx_len() >= \
|
||||
(self.header_len + self.data_len+2):
|
||||
self.__process_complete_received_msg()
|
||||
self.__flush_recv_msg()
|
||||
else:
|
||||
return 0 # wait 0s before sending a response
|
||||
|
||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||
self.data_len+2):
|
||||
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
||||
self._recv_buffer, self.header_len+self.data_len+2)
|
||||
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||
+ self.data_len + 2):
|
||||
self.__set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
def __process_complete_received_msg(self):
|
||||
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:')
|
||||
# self._recv_buffer, self.header_len +
|
||||
# self.data_len+2)
|
||||
if self.__trailer_is_ok(self.ifc.rx_peek(), self.header_len
|
||||
+ self.data_len + 2):
|
||||
if self.state == State.init:
|
||||
self.state = State.received
|
||||
self.__set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
|
||||
def forward(self, buffer, buflen) -> None:
|
||||
'''add the actual receive msg to the forwarding queue'''
|
||||
if self.no_forwarding:
|
||||
return
|
||||
tsun = Config.get('solarman')
|
||||
if tsun['enabled']:
|
||||
self._forward_buffer = buffer[:buflen]
|
||||
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
|
||||
buffer, buflen)
|
||||
self.ifc.fwd_add(buffer[:buflen])
|
||||
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
|
||||
|
||||
self.__parse_header(self._forward_buffer,
|
||||
len(self._forward_buffer))
|
||||
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||
f' Ctl: {int(self.control):#04x}'
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
return
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
# self.__build_header(0x91)
|
||||
# self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
|
||||
# f'{len(contact_mail)+1}p',
|
||||
# contact_name, contact_mail)
|
||||
|
||||
# self.__finish_send_msg()
|
||||
return False
|
||||
|
||||
'''
|
||||
@@ -250,23 +326,29 @@ class SolarmanV5(Message):
|
||||
self.snr = result[4]
|
||||
|
||||
if start != 0xA5:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid start byte from'
|
||||
f' {self.addr}:', buf, buf_len)
|
||||
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
# erase broken recv buffer
|
||||
self._recv_buffer = bytearray()
|
||||
self.ifc.rx_clear()
|
||||
return
|
||||
self.header_valid = True
|
||||
return
|
||||
|
||||
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
|
||||
crc = buf[self.data_len+11]
|
||||
stop = buf[self.data_len+12]
|
||||
if stop != 0x15:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid stop byte from '
|
||||
f'{self.addr}:', buf, buf_len)
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
if len(self._recv_buffer) > (self.data_len+13):
|
||||
if self.ifc.rx_len() > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
if next_start != 0xa5:
|
||||
# erase broken recv buffer
|
||||
self._recv_buffer = bytearray()
|
||||
self.ifc.rx_clear()
|
||||
|
||||
return False
|
||||
|
||||
@@ -282,33 +364,38 @@ class SolarmanV5(Message):
|
||||
|
||||
def __build_header(self, ctrl) -> None:
|
||||
'''build header for new transmit message'''
|
||||
self.send_msg_ofs = len(self._send_buffer)
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
|
||||
self._send_buffer += struct.pack(
|
||||
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr)
|
||||
self.ifc.tx_add(struct.pack(
|
||||
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr))
|
||||
fnc = self.switch.get(ctrl, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'tx') +
|
||||
f' Ctl: {int(ctrl):#04x} Msg: {fnc.__name__!r}')
|
||||
|
||||
def __finish_send_msg(self) -> None:
|
||||
'''finish the transmit message, set lenght and checksum'''
|
||||
_len = len(self._send_buffer) - self.send_msg_ofs
|
||||
struct.pack_into('<H', self._send_buffer, self.send_msg_ofs+1, _len-11)
|
||||
check = sum(self._send_buffer[self.send_msg_ofs+1:self.send_msg_ofs +
|
||||
_len]) & 0xff
|
||||
self._send_buffer += struct.pack('<BB', check, 0x15) # crc & stop
|
||||
_len = self.ifc.tx_len() - self.send_msg_ofs
|
||||
struct.pack_into('<H', self.ifc.tx_peek(), self.send_msg_ofs+1,
|
||||
_len-11)
|
||||
check = sum(self.ifc.tx_peek()[
|
||||
self.send_msg_ofs+1:self.send_msg_ofs + _len]) & 0xff
|
||||
self.ifc.tx_add(struct.pack('<BB', check, 0x15)) # crc & stop
|
||||
|
||||
def _update_header(self, _forward_buffer):
|
||||
'''update header for message before forwarding,
|
||||
set sequence and checksum'''
|
||||
_len = len(_forward_buffer)
|
||||
struct.pack_into('<H', _forward_buffer, 1,
|
||||
_len-13)
|
||||
struct.pack_into('<H', _forward_buffer, 5,
|
||||
self.seq.get_send())
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('<BH', _forward_buffer, ofs)
|
||||
data_len = result[1] # len of variable id string
|
||||
|
||||
check = sum(_forward_buffer[1:_len-2]) & 0xff
|
||||
struct.pack_into('<B', _forward_buffer, _len-2, check)
|
||||
struct.pack_into('<H', _forward_buffer, ofs+5,
|
||||
self.seq.get_send())
|
||||
|
||||
check = sum(_forward_buffer[ofs+1:ofs+data_len+11]) & 0xff
|
||||
struct.pack_into('<B', _forward_buffer, ofs+data_len+11, check)
|
||||
ofs += (13 + data_len)
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||
@@ -323,91 +410,107 @@ class SolarmanV5(Message):
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
self._recv_buffer = self._recv_buffer[(self.header_len +
|
||||
self.data_len+2):]
|
||||
self.ifc.rx_get(self.header_len + self.data_len+2)
|
||||
self.header_valid = False
|
||||
|
||||
def __send_ack_rsp(self, msgtype, ftype, ack=1):
|
||||
self.__build_header(msgtype)
|
||||
self._send_buffer += struct.pack('<BBLL', ftype, ack,
|
||||
self._timestamp(),
|
||||
self._heartbeat())
|
||||
self.ifc.tx_add(struct.pack('<BBLL', ftype, ack,
|
||||
self._timestamp(),
|
||||
self._heartbeat()))
|
||||
self.__finish_send_msg()
|
||||
|
||||
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||
if self.state != self.STATE_UP:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
self.__build_header(0x4510)
|
||||
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||
0x2b0, 0, 0, 0)
|
||||
self._send_buffer += pdu
|
||||
self.ifc.tx_add(struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||
self.sensor_list, 0, 0, 0))
|
||||
self.ifc.tx_add(pdu)
|
||||
self.__finish_send_msg()
|
||||
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
||||
self._send_buffer, len(self._send_buffer))
|
||||
self.writer.write(self._send_buffer)
|
||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
|
||||
if 1 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
||||
|
||||
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||
cmd.startswith(tuple(self.at_acl[connection]['block']))
|
||||
|
||||
async def send_at_cmd(self, AT_cmd: str) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
async def send_at_cmd(self, at_cmd: str) -> None:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
AT_cmd = AT_cmd.strip()
|
||||
at_cmd = at_cmd.strip()
|
||||
|
||||
if self.at_cmd_forbidden(cmd=AT_cmd, connection='mqtt'):
|
||||
data_json = f'\'{AT_cmd}\' is forbidden'
|
||||
if self.at_cmd_forbidden(cmd=at_cmd, connection='mqtt'):
|
||||
data_json = f'\'{at_cmd}\' is forbidden'
|
||||
node_id = self.node_id
|
||||
key = 'at_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
asyncio.ensure_future(
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
|
||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
self.forward_at_cmd_resp = False
|
||||
self.__build_header(0x4510)
|
||||
self._send_buffer += struct.pack(f'<BHLLL{len(AT_cmd)}sc', self.AT_CMD,
|
||||
2, 0, 0, 0, AT_cmd.encode('utf-8'),
|
||||
b'\r')
|
||||
self.ifc.tx_add(struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
|
||||
0x0002, 0, 0, 0,
|
||||
at_cmd.encode('utf-8'), b'\r'))
|
||||
self.__finish_send_msg()
|
||||
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
|
||||
try:
|
||||
await self.async_write('Send AT Command:')
|
||||
self.ifc.tx_flush()
|
||||
except Exception:
|
||||
self._send_buffer = bytearray(0)
|
||||
self.ifc.tx_clear()
|
||||
|
||||
def __forward_msg(self):
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||
self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2)
|
||||
|
||||
def __build_model_name(self):
|
||||
db = self.db
|
||||
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
Rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||
Model = None
|
||||
if MaxPow == 2000:
|
||||
if Rated == 800 or Rated == 600:
|
||||
Model = f'TSOL-MS{MaxPow}({Rated})'
|
||||
max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||
rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||
model = None
|
||||
if max_pow == 2000:
|
||||
if rated == 800 or rated == 600:
|
||||
model = f'TSOL-MS{max_pow}({rated})'
|
||||
else:
|
||||
Model = f'TSOL-MS{MaxPow}'
|
||||
elif MaxPow == 1800 or MaxPow == 1600:
|
||||
Model = f'TSOL-MS{MaxPow}'
|
||||
if Model:
|
||||
logger.info(f'Model: {Model}')
|
||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
|
||||
model = f'TSOL-MS{max_pow}'
|
||||
elif max_pow == 1800 or max_pow == 1600:
|
||||
model = f'TSOL-MS{max_pow}'
|
||||
if model:
|
||||
logger.info(f'Model: {model}')
|
||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
||||
|
||||
def __process_data(self, ftype):
|
||||
def __process_data(self, ftype, ts):
|
||||
inv_update = False
|
||||
msg_type = self.control >> 8
|
||||
for key, update in self.db.parse(self._recv_buffer, msg_type, ftype,
|
||||
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype,
|
||||
self.node_id):
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, ts)
|
||||
self.new_data[key] = True
|
||||
|
||||
if inv_update:
|
||||
@@ -421,46 +524,57 @@ class SolarmanV5(Message):
|
||||
self.__forward_msg()
|
||||
|
||||
def msg_dev_ind(self):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
result = struct.unpack_from('<BLLL', data, 0)
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
||||
ftype = result[0] # always 2
|
||||
# total = result[1]
|
||||
total = result[1]
|
||||
tim = result[2]
|
||||
res = result[3] # always zero
|
||||
logger.info(f'frame type:{ftype:02x}'
|
||||
f' timer:{tim:08x}s null:{res}')
|
||||
# if self.time_ofs:
|
||||
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
|
||||
self.__process_data(ftype)
|
||||
if self.time_ofs:
|
||||
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
ts = total + self.time_ofs
|
||||
else:
|
||||
ts = None
|
||||
self.__process_data(ftype, ts)
|
||||
self.sensor_list = int(self.db.get_db_value(Register.SENSOR_LIST, 0),
|
||||
16)
|
||||
self.__forward_msg()
|
||||
self.__send_ack_rsp(0x1110, ftype)
|
||||
|
||||
def msg_data_ind(self):
|
||||
data = self._recv_buffer
|
||||
data = self.ifc.rx_peek()
|
||||
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
|
||||
ftype = result[0] # 1 or 0x81
|
||||
# total = result[2]
|
||||
sensor = result[1]
|
||||
total = result[2]
|
||||
tim = result[3]
|
||||
if 1 == ftype:
|
||||
self.time_ofs = result[4]
|
||||
unkn = result[5]
|
||||
cnt = result[6]
|
||||
if sensor != self.sensor_list:
|
||||
logger.warning(f'Unexpected Sensor-List:{sensor:04x}'
|
||||
f' (!={self.sensor_list:04x})')
|
||||
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
|
||||
f' ??: {unkn:04x} cnt:{cnt}')
|
||||
# if self.time_ofs:
|
||||
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
if self.time_ofs:
|
||||
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
ts = total + self.time_ofs
|
||||
else:
|
||||
ts = None
|
||||
|
||||
self.__process_data(ftype)
|
||||
self.__process_data(ftype, ts)
|
||||
self.__forward_msg()
|
||||
self.__send_ack_rsp(0x1210, ftype)
|
||||
self.state = self.STATE_UP
|
||||
self.new_state_up()
|
||||
|
||||
def msg_sync_start(self):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
result = struct.unpack_from('<BLLL', data, 0)
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
||||
ftype = result[0]
|
||||
total = result[1]
|
||||
self.time_ofs = result[3]
|
||||
@@ -472,46 +586,49 @@ class SolarmanV5(Message):
|
||||
self.__send_ack_rsp(0x1310, ftype)
|
||||
|
||||
def msg_command_req(self):
|
||||
data = self._recv_buffer[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
data = self.ifc.rx_peek()[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
result = struct.unpack_from('<B', data, 0)
|
||||
ftype = result[0]
|
||||
if ftype == self.AT_CMD:
|
||||
AT_cmd = data[15:].decode()
|
||||
if self.at_cmd_forbidden(cmd=AT_cmd, connection='tsun'):
|
||||
at_cmd = data[15:].decode()
|
||||
if self.at_cmd_forbidden(cmd=at_cmd, connection='tsun'):
|
||||
self.inc_counter('AT_Command_Blocked')
|
||||
return
|
||||
self.inc_counter('AT_Command')
|
||||
self.forward_at_cmd_resp = True
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
if self.remoteStream.mb.recv_req(data[15:],
|
||||
self.__forward_msg()):
|
||||
rstream = self.ifc.remote.stream
|
||||
if rstream.mb.recv_req(data[15:],
|
||||
rstream.__forward_msg):
|
||||
self.inc_counter('Modbus_Command')
|
||||
else:
|
||||
logger.error('Invalid Modbus Msg')
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
return
|
||||
|
||||
self.__forward_msg()
|
||||
|
||||
async def publish_mqtt(self, key, data):
|
||||
await self.mqtt.publish(key, data) # pragma: no cover
|
||||
def publish_mqtt(self, key, data): # pragma: no cover
|
||||
asyncio.ensure_future(
|
||||
self.mqtt.publish(key, data))
|
||||
|
||||
def get_cmd_rsp_log_lvl(self) -> int:
|
||||
ftype = self._recv_buffer[self.header_len]
|
||||
ftype = self.ifc.rx_peek()[self.header_len]
|
||||
if ftype == self.AT_CMD:
|
||||
if self.forward_at_cmd_resp:
|
||||
return logging.INFO
|
||||
return logging.DEBUG
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
if self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
elif ftype == self.MB_RTU_CMD \
|
||||
and self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
|
||||
return logging.WARNING
|
||||
|
||||
def msg_command_rsp(self):
|
||||
data = self._recv_buffer[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
data = self.ifc.rx_peek()[self.header_len:
|
||||
self.header_len+self.data_len]
|
||||
ftype = data[0]
|
||||
if ftype == self.AT_CMD:
|
||||
if not self.forward_at_cmd_resp:
|
||||
@@ -519,43 +636,45 @@ class SolarmanV5(Message):
|
||||
node_id = self.node_id
|
||||
key = 'at_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
asyncio.ensure_future(
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
valid = data[1]
|
||||
modbus_msg_len = self.data_len - 14
|
||||
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||
if valid == 1 and modbus_msg_len > 4:
|
||||
# logger.info(f'first byte modbus:{data[14]}')
|
||||
inv_update = False
|
||||
self.modbus_elms = 0
|
||||
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
||||
self.node_id):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self.new_data[key] = True
|
||||
|
||||
if inv_update:
|
||||
self.__build_model_name()
|
||||
self.__modbus_command_rsp(data)
|
||||
return
|
||||
self.__forward_msg()
|
||||
|
||||
def __modbus_command_rsp(self, data):
|
||||
'''precess MODBUS RTU response'''
|
||||
valid = data[1]
|
||||
modbus_msg_len = self.data_len - 14
|
||||
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||
if valid == 1 and modbus_msg_len > 4:
|
||||
# logger.info(f'first byte modbus:{data[14]}')
|
||||
inv_update = False
|
||||
self.modbus_elms = 0
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
||||
self.node_id):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, self._timestamp())
|
||||
self.new_data[key] = True
|
||||
if inv_update:
|
||||
self.__build_model_name()
|
||||
|
||||
def msg_hbeat_ind(self):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<B', data, 0)
|
||||
ftype = result[0]
|
||||
|
||||
self.__forward_msg()
|
||||
self.__send_ack_rsp(0x1710, ftype)
|
||||
self.state = self.STATE_UP
|
||||
self.new_state_up()
|
||||
|
||||
def msg_sync_end(self):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
result = struct.unpack_from('<BLLL', data, 0)
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
||||
ftype = result[0]
|
||||
total = result[1]
|
||||
self.time_ofs = result[3]
|
||||
@@ -567,7 +686,7 @@ class SolarmanV5(Message):
|
||||
self.__send_ack_rsp(0x1810, ftype)
|
||||
|
||||
def msg_response(self):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<BBLL', data, 0)
|
||||
ftype = result[0] # always 2
|
||||
valid = result[1] == 1 # status
|
||||
|
||||
486
app/src/infos.py
486
app/src/infos.py
@@ -5,12 +5,19 @@ from enum import Enum
|
||||
from typing import Generator
|
||||
|
||||
|
||||
class ProxyMode(Enum):
|
||||
SERVER = 1
|
||||
CLIENT = 2
|
||||
|
||||
|
||||
class Register(Enum):
|
||||
COLLECTOR_FW_VERSION = 1
|
||||
CHIP_TYPE = 2
|
||||
CHIP_MODEL = 3
|
||||
TRACE_URL = 4
|
||||
LOGGER_URL = 5
|
||||
MAC_ADDR = 6
|
||||
COLLECTOR_SNR = 7
|
||||
PRODUCT_NAME = 20
|
||||
MANUFACTURER = 21
|
||||
VERSION = 22
|
||||
@@ -18,6 +25,7 @@ class Register(Enum):
|
||||
EQUIPMENT_MODEL = 24
|
||||
NO_INPUTS = 25
|
||||
MAX_DESIGNED_POWER = 26
|
||||
OUTPUT_COEFFICIENT = 27
|
||||
INVERTER_CNT = 50
|
||||
UNKNOWN_SNR = 51
|
||||
UNKNOWN_MSG = 52
|
||||
@@ -89,6 +97,8 @@ class Register(Enum):
|
||||
CONNECT_COUNT = 405
|
||||
HEARTBEAT_INTERVAL = 406
|
||||
IP_ADDRESS = 407
|
||||
POLLING_INTERVAL = 408
|
||||
SENSOR_LIST = 409
|
||||
EVENT_401 = 500
|
||||
EVENT_402 = 501
|
||||
EVENT_403 = 502
|
||||
@@ -105,6 +115,9 @@ class Register(Enum):
|
||||
EVENT_414 = 513
|
||||
EVENT_415 = 514
|
||||
EVENT_416 = 515
|
||||
TS_INPUT = 600
|
||||
TS_GRID = 601
|
||||
TS_TOTAL = 602
|
||||
VALUE_1 = 9000
|
||||
TEST_REG1 = 10000
|
||||
TEST_REG2 = 10001
|
||||
@@ -120,16 +133,16 @@ class ClrAtMidnight:
|
||||
return
|
||||
|
||||
prfx += f'{keys[0]}'
|
||||
dict = cls.db
|
||||
if prfx not in dict:
|
||||
dict[prfx] = {}
|
||||
dict = dict[prfx]
|
||||
db_dict = cls.db
|
||||
if prfx not in db_dict:
|
||||
db_dict[prfx] = {}
|
||||
db_dict = db_dict[prfx]
|
||||
|
||||
for key in keys[1:-1]:
|
||||
if key not in dict:
|
||||
dict[key] = {}
|
||||
dict = dict[key]
|
||||
dict[keys[-1]] = 0
|
||||
if key not in db_dict:
|
||||
db_dict[key] = {}
|
||||
db_dict = db_dict[key]
|
||||
db_dict[keys[-1]] = 0
|
||||
|
||||
@classmethod
|
||||
def elm(cls) -> Generator[tuple[str, dict], None, None]:
|
||||
@@ -139,10 +152,21 @@ class ClrAtMidnight:
|
||||
|
||||
|
||||
class Infos:
|
||||
LIGHTNING = 'mdi:lightning-bolt'
|
||||
COUNTER = 'mdi:counter'
|
||||
GAUGE = 'mdi:gauge'
|
||||
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
|
||||
SOLAR_POWER = 'mdi:solar-power'
|
||||
WIFI = 'mdi:wifi'
|
||||
UPDATE = 'mdi:update'
|
||||
DAILY_GEN = 'Daily Generation'
|
||||
TOTAL_GEN = 'Total Generation'
|
||||
FMT_INT = '| int'
|
||||
FMT_FLOAT = '| float'
|
||||
FMT_STRING_SEC = '| string + " s"'
|
||||
stat = {}
|
||||
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||
version = os.getenv('VERSION', 'unknown')
|
||||
|
||||
new_stat_data = {}
|
||||
|
||||
@classmethod
|
||||
@@ -166,8 +190,8 @@ class Infos:
|
||||
|
||||
__info_devs = {
|
||||
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
||||
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION}, # noqa: E501
|
||||
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION}, # noqa: E501
|
||||
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION, 'mac': Register.MAC_ADDR, 'sn': Register.COLLECTOR_SNR}, # noqa: E501
|
||||
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
|
||||
'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
|
||||
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
||||
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'mdl': Register.PV3_MODEL, 'mf': Register.PV3_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501
|
||||
@@ -177,15 +201,32 @@ class Infos:
|
||||
}
|
||||
|
||||
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
||||
__status_type_val_tpl = "{%set inv_status = ['n/a', 'Online', 'Offline'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
|
||||
__status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
|
||||
__rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
__designed_power_val_tpl = '''
|
||||
{% if 'Max_Designed_Power' in value_json and
|
||||
value_json['Max_Designed_Power'] != None %}
|
||||
{% if value_json['Max_Designed_Power'] | int(0xffff) < 0x8000 %}
|
||||
{{value_json['Max_Designed_Power']|string() +' W'}}
|
||||
{% else %}
|
||||
n/a
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
|
||||
__info_defs = {
|
||||
# collector values used for device registration:
|
||||
Register.COLLECTOR_FW_VERSION: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.CHIP_MODEL: {'name': ['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.TRACE_URL: {'name': ['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], '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.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.MAC_ADDR: {'name': ['collector', 'MAC-Addr'], 'singleton': False, 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.COLLECTOR_SNR: {'name': ['collector', 'Serial_Number'], 'singleton': False, 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
|
||||
|
||||
# inverter values used for device registration:
|
||||
Register.PRODUCT_NAME: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
@@ -194,9 +235,9 @@ class Infos:
|
||||
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
|
||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
@@ -211,19 +252,19 @@ class Infos:
|
||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# proxy:
|
||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
||||
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': '| int', 'name': 'AT Command Blocked', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': '| int', 'name': 'Modbus Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
|
||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': FMT_INT, 'name': 'Unknown Msg Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': FMT_INT, 'name': 'Invalid Data Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': FMT_INT, 'name': 'Internal Error', 'icon': COUNTER, 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
||||
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': FMT_INT, 'name': 'Unknown Control Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': FMT_INT, 'name': 'OTA Start Cmd', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': FMT_INT, 'name': 'Internal SW Exception', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': FMT_INT, 'name': 'Invalid Message Format', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': FMT_INT, 'name': 'AT Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': FMT_INT, 'name': 'AT Command Blocked', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': FMT_INT, 'name': 'Modbus Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
||||
|
||||
# events
|
||||
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
@@ -244,57 +285,62 @@ class Infos:
|
||||
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# grid measures:
|
||||
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:counter'}}, # noqa: E501
|
||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': FMT_FLOAT, 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': FMT_FLOAT, 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
||||
|
||||
# input measures:
|
||||
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
|
||||
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
# total:
|
||||
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||
Register.TS_TOTAL: {'name': ['total', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': FMT_FLOAT, 'name': DAILY_GEN, 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
||||
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||
|
||||
# controller:
|
||||
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| int', 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
|
||||
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
|
||||
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -305,7 +351,7 @@ class Infos:
|
||||
def info_defs(self) -> dict:
|
||||
return self.__info_defs
|
||||
|
||||
def dev_value(self, idx: str | int) -> str | int | float | None:
|
||||
def dev_value(self, idx: str | int) -> str | int | float | dict | None:
|
||||
'''returns the stored device value from our database
|
||||
|
||||
idx:int ==> lookup the value in the database and return it as str,
|
||||
@@ -318,32 +364,36 @@ class Infos:
|
||||
elif idx in self.info_defs:
|
||||
row = self.info_defs[idx]
|
||||
if 'singleton' in row and row['singleton']:
|
||||
dict = self.stat
|
||||
db_dict = self.stat
|
||||
else:
|
||||
dict = self.db
|
||||
db_dict = self.db
|
||||
|
||||
keys = row['name']
|
||||
|
||||
for key in keys:
|
||||
if key not in dict:
|
||||
if key not in db_dict:
|
||||
return None # value not found in the database
|
||||
dict = dict[key]
|
||||
return dict # value of the reqeusted entry
|
||||
db_dict = db_dict[key]
|
||||
return db_dict # value of the reqeusted entry
|
||||
|
||||
return None # unknwon idx, not in info_defs
|
||||
|
||||
def inc_counter(self, counter: str) -> None:
|
||||
@classmethod
|
||||
def inc_counter(cls, counter: str) -> None:
|
||||
'''inc proxy statistic counter'''
|
||||
dict = self.stat['proxy']
|
||||
dict[counter] += 1
|
||||
db_dict = cls.stat['proxy']
|
||||
db_dict[counter] += 1
|
||||
cls.new_stat_data['proxy'] = True
|
||||
|
||||
def dec_counter(self, counter: str) -> None:
|
||||
@classmethod
|
||||
def dec_counter(cls, counter: str) -> None:
|
||||
'''dec proxy statistic counter'''
|
||||
dict = self.stat['proxy']
|
||||
dict[counter] -= 1
|
||||
db_dict = cls.stat['proxy']
|
||||
db_dict[counter] -= 1
|
||||
cls.new_stat_data['proxy'] = True
|
||||
|
||||
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
||||
-> Generator[tuple[dict, str], None, None]:
|
||||
-> Generator[tuple[str, str, str, str], None, None]:
|
||||
'''Generator function yields json register struct for home-assistant
|
||||
auto configuration and the unique entity string, for all proxy
|
||||
registers
|
||||
@@ -363,6 +413,20 @@ class Infos:
|
||||
|
||||
def ha_conf(self, key, ha_prfx, node_id, snr, singleton: bool,
|
||||
sug_area: str = '') -> tuple[str, str, str, str] | None:
|
||||
'''Method to build json register struct for home-assistant
|
||||
auto configuration and the unique entity string, for all proxy
|
||||
registers
|
||||
|
||||
arguments:
|
||||
key ==> index of info_defs dict which reference the topic
|
||||
ha_prfx:str ==> MQTT prefix for the home assistant 'stat_t string
|
||||
node_id:str ==> node id of the inverter, used to build unique entity
|
||||
snr:str ==> serial number of the inverter, used to build unique
|
||||
entity strings
|
||||
singleton ==> bool to allow/disaalow proxy topics which are common
|
||||
for all invters
|
||||
sug_area ==> area name for home assistant
|
||||
'''
|
||||
if key not in self.info_defs:
|
||||
return None
|
||||
row = self.info_defs[key]
|
||||
@@ -372,7 +436,148 @@ class Infos:
|
||||
return None
|
||||
elif singleton:
|
||||
return None
|
||||
|
||||
# check if we have details for home assistant
|
||||
if 'ha' in row:
|
||||
return self.__ha_conf(row, key, ha_prfx, node_id, snr, sug_area)
|
||||
return None
|
||||
|
||||
def __ha_conf(self, row, key, ha_prfx, node_id, snr,
|
||||
sug_area: str) -> tuple[str, str, str, str] | None:
|
||||
ha = row['ha']
|
||||
if 'comp' in ha:
|
||||
component = ha['comp']
|
||||
else:
|
||||
component = 'sensor'
|
||||
attr = self.__build_attr(row, key, ha_prfx, node_id, snr)
|
||||
if 'dev' in ha:
|
||||
device = self.info_devs[ha['dev']]
|
||||
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
||||
return None
|
||||
attr['dev'] = self.__build_dev(device, key, ha, snr,
|
||||
sug_area)
|
||||
attr['o'] = self.__build_origin()
|
||||
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for {key} "
|
||||
"missing 'dev' value for ha register")
|
||||
return json.dumps(attr), component, node_id, attr['uniq_id']
|
||||
|
||||
def __build_attr(self, row, key, ha_prfx, node_id, snr):
|
||||
attr = {}
|
||||
ha = row['ha']
|
||||
if 'name' in ha:
|
||||
attr['name'] = ha['name']
|
||||
else:
|
||||
attr['name'] = row['name'][-1]
|
||||
prfx = ha_prfx + node_id
|
||||
attr['stat_t'] = prfx + row['name'][0]
|
||||
attr['dev_cla'] = ha['dev_cla']
|
||||
attr['stat_cla'] = ha['stat_cla']
|
||||
attr['uniq_id'] = ha['id']+snr
|
||||
if 'val_tpl' in ha:
|
||||
attr['val_tpl'] = ha['val_tpl']
|
||||
elif 'fmt' in ha:
|
||||
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for {key} do"
|
||||
" not have a 'val_tpl' nor a 'fmt' value")
|
||||
# add unit_of_meas only, if status_class isn't none. If
|
||||
# status_cla is None we want a number format and not line
|
||||
# graph in home assistant. A unit will change the number
|
||||
# format to a line graph
|
||||
if 'unit' in row and attr['stat_cla'] is not None:
|
||||
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
||||
if 'icon' in ha:
|
||||
attr['ic'] = ha['icon'] # icon for the entity
|
||||
if 'nat_prc' in ha: # pragma: no cover
|
||||
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
||||
if 'ent_cat' in ha:
|
||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||
# enabled_by_default is deactivated, since it avoid the via
|
||||
# setup of the devices. It seems, that there is a bug in home
|
||||
# assistant. tested with 'Home Assistant 2023.10.4'
|
||||
# if 'en' in ha: # enabled_by_default
|
||||
# attr['en'] = ha['en']
|
||||
return attr
|
||||
|
||||
def __build_dev(self, device, key, ha, snr, sug_area):
|
||||
dev = {}
|
||||
singleton = 'singleton' in device and device['singleton']
|
||||
# the same name for 'name' and 'suggested area', so we get
|
||||
# dedicated devices in home assistant with short value
|
||||
# name and headline
|
||||
if (sug_area == '' or singleton):
|
||||
dev['name'] = device['name']
|
||||
dev['sa'] = device['name']
|
||||
else:
|
||||
dev['name'] = device['name']+' - '+sug_area
|
||||
dev['sa'] = device['name']+' - '+sug_area
|
||||
self.__add_via_dev(dev, device, key, snr)
|
||||
for key in ('mdl', 'mf', 'sw', 'hw', 'sn'): # add optional
|
||||
# values fpr 'modell', 'manufacturer', 'sw version' and
|
||||
# 'hw version'
|
||||
if key in device:
|
||||
data = self.dev_value(device[key])
|
||||
if data is not None:
|
||||
dev[key] = data
|
||||
if singleton:
|
||||
dev['ids'] = [f"{ha['dev']}"]
|
||||
else:
|
||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||
self.__add_connection(dev, device)
|
||||
return dev
|
||||
|
||||
def __add_connection(self, dev, device):
|
||||
if 'mac' in device:
|
||||
mac_str = self.dev_value(device['mac'])
|
||||
if mac_str is not None:
|
||||
if 12 == len(mac_str):
|
||||
mac_str = ':'.join(mac_str[i:i+2] for i in range(0, 12, 2))
|
||||
dev['cns'] = [["mac", f"{mac_str}"]]
|
||||
|
||||
def __add_via_dev(self, dev, device, key, snr):
|
||||
if 'via' in device: # add the link to the parent device
|
||||
via = device['via']
|
||||
if via in self.info_devs:
|
||||
via_dev = self.info_devs[via]
|
||||
if 'singleton' in via_dev and via_dev['singleton']:
|
||||
dev['via_device'] = via
|
||||
else:
|
||||
dev['via_device'] = f"{via}_{snr}"
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for "
|
||||
f"{key} has an invalid via value: "
|
||||
f"{via}")
|
||||
|
||||
def __build_origin(self):
|
||||
origin = {}
|
||||
origin['name'] = self.app_name
|
||||
origin['sw'] = self.version
|
||||
return origin
|
||||
|
||||
def ha_remove(self, key, node_id, snr) -> tuple[str, str, str, str] | None:
|
||||
'''Method to build json unregister struct for home-assistant
|
||||
to remove topics per auto configuration. Only for inverer topics.
|
||||
|
||||
arguments:
|
||||
key ==> index of info_defs dict which reference the topic
|
||||
node_id:str ==> node id of the inverter, used to build unique entity
|
||||
snr:str ==> serial number of the inverter, used to build unique
|
||||
entity strings
|
||||
|
||||
hint:
|
||||
the returned tuple must have the same format as self.ha_conf()
|
||||
'''
|
||||
if key not in self.info_defs:
|
||||
return None
|
||||
row = self.info_defs[key]
|
||||
|
||||
if 'singleton' in row and row['singleton']:
|
||||
return None
|
||||
|
||||
# check if we have details for home assistant
|
||||
if 'ha' in row:
|
||||
@@ -382,91 +587,12 @@ class Infos:
|
||||
else:
|
||||
component = 'sensor'
|
||||
attr = {}
|
||||
if 'name' in ha:
|
||||
attr['name'] = ha['name']
|
||||
else:
|
||||
attr['name'] = row['name'][-1]
|
||||
attr['stat_t'] = prfx + row['name'][0]
|
||||
attr['dev_cla'] = ha['dev_cla']
|
||||
attr['stat_cla'] = ha['stat_cla']
|
||||
attr['uniq_id'] = ha['id']+snr
|
||||
if 'val_tpl' in ha:
|
||||
attr['val_tpl'] = ha['val_tpl']
|
||||
elif 'fmt' in ha:
|
||||
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for {key} do"
|
||||
" not have a 'val_tpl' nor a 'fmt' value")
|
||||
# add unit_of_meas only, if status_class isn't none. If
|
||||
# status_cla is None we want a number format and not line
|
||||
# graph in home assistant. A unit will change the number
|
||||
# format to a line graph
|
||||
if 'unit' in row and attr['stat_cla'] is not None:
|
||||
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
||||
if 'icon' in ha:
|
||||
attr['ic'] = ha['icon'] # icon for the entity
|
||||
if 'nat_prc' in ha: # pragma: no cover
|
||||
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
||||
if 'ent_cat' in ha:
|
||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||
# enabled_by_default is deactivated, since it avoid the via
|
||||
# setup of the devices. It seems, that there is a bug in home
|
||||
# assistant. tested with 'Home Assistant 2023.10.4'
|
||||
# if 'en' in ha: # enabled_by_default
|
||||
# attr['en'] = ha['en']
|
||||
if 'dev' in ha:
|
||||
device = self.info_devs[ha['dev']]
|
||||
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
||||
return None
|
||||
dev = {}
|
||||
# the same name for 'name' and 'suggested area', so we get
|
||||
# dedicated devices in home assistant with short value
|
||||
# name and headline
|
||||
if (sug_area == '' or
|
||||
('singleton' in device and device['singleton'])):
|
||||
dev['name'] = device['name']
|
||||
dev['sa'] = device['name']
|
||||
else:
|
||||
dev['name'] = device['name']+' - '+sug_area
|
||||
dev['sa'] = device['name']+' - '+sug_area
|
||||
if 'via' in device: # add the link to the parent device
|
||||
via = device['via']
|
||||
if via in self.info_devs:
|
||||
via_dev = self.info_devs[via]
|
||||
if 'singleton' in via_dev and via_dev['singleton']:
|
||||
dev['via_device'] = via
|
||||
else:
|
||||
dev['via_device'] = f"{via}_{snr}"
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for "
|
||||
f"{key} has an invalid via value: "
|
||||
f"{via}")
|
||||
for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
|
||||
# values fpr 'modell', 'manufacturer', 'sw version' and
|
||||
# 'hw version'
|
||||
if key in device:
|
||||
data = self.dev_value(device[key])
|
||||
if data is not None:
|
||||
dev[key] = data
|
||||
if 'singleton' in device and device['singleton']:
|
||||
dev['ids'] = [f"{ha['dev']}"]
|
||||
else:
|
||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||
attr['dev'] = dev
|
||||
origin = {}
|
||||
origin['name'] = self.app_name
|
||||
origin['sw'] = self.version
|
||||
attr['o'] = origin
|
||||
else:
|
||||
self.inc_counter('Internal_Error')
|
||||
logging.error(f"Infos.info_defs: the row for {key} "
|
||||
"missing 'dev' value for ha register")
|
||||
return json.dumps(attr), component, node_id, attr['uniq_id']
|
||||
uniq_id = ha['id']+snr
|
||||
|
||||
return json.dumps(attr), component, node_id, uniq_id
|
||||
return None
|
||||
|
||||
def _key_obj(self, id: Register) -> list:
|
||||
def _key_obj(self, id: Register) -> tuple:
|
||||
d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG,
|
||||
'unit': ''})
|
||||
if 'ha' in d and 'must_incr' in d['ha']:
|
||||
@@ -478,21 +604,21 @@ class Infos:
|
||||
|
||||
def update_db(self, keys: list, must_incr: bool, result):
|
||||
name = ''
|
||||
dict = self.db
|
||||
db_dict = self.db
|
||||
for key in keys[:-1]:
|
||||
if key not in dict:
|
||||
dict[key] = {}
|
||||
dict = dict[key]
|
||||
if key not in db_dict:
|
||||
db_dict[key] = {}
|
||||
db_dict = db_dict[key]
|
||||
name += key + '.'
|
||||
if keys[-1] not in dict:
|
||||
if keys[-1] not in db_dict:
|
||||
update = (not must_incr or result > 0)
|
||||
else:
|
||||
if must_incr:
|
||||
update = dict[keys[-1]] < result
|
||||
update = db_dict[keys[-1]] < result
|
||||
else:
|
||||
update = dict[keys[-1]] != result
|
||||
update = db_dict[keys[-1]] != result
|
||||
if update:
|
||||
dict[keys[-1]] = result
|
||||
db_dict[keys[-1]] = result
|
||||
name += keys[-1]
|
||||
return name, update
|
||||
|
||||
@@ -546,13 +672,13 @@ class Infos:
|
||||
return True
|
||||
|
||||
if 'gte' in dep:
|
||||
return not value >= dep['gte']
|
||||
return value < dep['gte']
|
||||
elif 'less_eq' in dep:
|
||||
return not value <= dep['less_eq']
|
||||
return value > dep['less_eq']
|
||||
return True
|
||||
|
||||
def set_pv_module_details(self, inv: dict) -> None:
|
||||
map = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
|
||||
pvs = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
|
||||
'pv2': {'manufacturer': Register.PV2_MANUFACTURER, 'model': Register.PV2_MODEL}, # noqa: E501
|
||||
'pv3': {'manufacturer': Register.PV3_MANUFACTURER, 'model': Register.PV3_MODEL}, # noqa: E501
|
||||
'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501
|
||||
@@ -560,7 +686,7 @@ class Infos:
|
||||
'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501
|
||||
}
|
||||
|
||||
for key, reg in map.items():
|
||||
for key, reg in pvs.items():
|
||||
if key in inv:
|
||||
if 'manufacturer' in inv[key]:
|
||||
self.set_db_def_value(reg['manufacturer'],
|
||||
|
||||
183
app/src/inverter_base.py
Normal file
183
app/src/inverter_base.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import weakref
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
import gc
|
||||
from aiomqtt import MqttCodeError
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
if __name__ == "app.src.inverter_base":
|
||||
from app.src.inverter_ifc import InverterIfc
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.async_stream import StreamPtr
|
||||
from app.src.async_stream import AsyncStreamClient
|
||||
from app.src.async_stream import AsyncStreamServer
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from inverter_ifc import InverterIfc
|
||||
from proxy import Proxy
|
||||
from async_stream import StreamPtr
|
||||
from async_stream import AsyncStreamClient
|
||||
from async_stream import AsyncStreamServer
|
||||
from config import Config
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
class InverterBase(InverterIfc, Proxy):
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
config_id: str, prot_class,
|
||||
client_mode: bool = False):
|
||||
Proxy.__init__(self)
|
||||
self._registry.append(weakref.ref(self))
|
||||
self.addr = writer.get_extra_info('peername')
|
||||
self.config_id = config_id
|
||||
self.prot_class = prot_class
|
||||
self.__ha_restarts = -1
|
||||
self.remote = StreamPtr(None)
|
||||
ifc = AsyncStreamServer(reader, writer,
|
||||
self.async_publ_mqtt,
|
||||
self.create_remote,
|
||||
self.remote)
|
||||
|
||||
self.local = StreamPtr(
|
||||
self.prot_class(self.addr, ifc, True, client_mode), ifc
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
logging.debug(f'InverterBase.__exit__() {self.addr}')
|
||||
self.__del_remote()
|
||||
|
||||
self.local.stream.close()
|
||||
self.local.stream = None
|
||||
self.local.ifc.close()
|
||||
self.local.ifc = None
|
||||
|
||||
# now explicitly call garbage collector to release unreachable objects
|
||||
unreachable_obj = gc.collect()
|
||||
logging.debug(
|
||||
f'InverterBase.__exit: freed unreachable obj: {unreachable_obj}')
|
||||
|
||||
def __del_remote(self):
|
||||
if self.remote.stream:
|
||||
self.remote.stream.close()
|
||||
self.remote.stream = None
|
||||
|
||||
if self.remote.ifc:
|
||||
self.remote.ifc.close()
|
||||
self.remote.ifc = None
|
||||
|
||||
async def disc(self, shutdown_started=False) -> None:
|
||||
if self.remote.stream:
|
||||
self.remote.stream.shutdown_started = shutdown_started
|
||||
if self.remote.ifc:
|
||||
await self.remote.ifc.disc()
|
||||
if self.local.stream:
|
||||
self.local.stream.shutdown_started = shutdown_started
|
||||
if self.local.ifc:
|
||||
await self.local.ifc.disc()
|
||||
|
||||
def healthy(self) -> bool:
|
||||
logging.debug('InverterBase healthy()')
|
||||
|
||||
if self.local.ifc and not self.local.ifc.healthy():
|
||||
return False
|
||||
if self.remote.ifc and not self.remote.ifc.healthy():
|
||||
return False
|
||||
return True
|
||||
|
||||
async def create_remote(self) -> None:
|
||||
'''Establish a client connection to the TSUN cloud'''
|
||||
|
||||
tsun = Config.get(self.config_id)
|
||||
host = tsun['host']
|
||||
port = tsun['port']
|
||||
addr = (host, port)
|
||||
stream = self.local.stream
|
||||
|
||||
try:
|
||||
logging.info(f'[{stream.node_id}] Connect to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
ifc = AsyncStreamClient(
|
||||
reader, writer, self.local, self.__del_remote)
|
||||
|
||||
self.remote.ifc = ifc
|
||||
if hasattr(stream, 'id_str'):
|
||||
self.remote.stream = self.prot_class(
|
||||
addr, ifc, server_side=False,
|
||||
client_mode=False, id_str=stream.id_str)
|
||||
else:
|
||||
self.remote.stream = self.prot_class(
|
||||
addr, ifc, server_side=False,
|
||||
client_mode=False)
|
||||
|
||||
logging.info(f'[{self.remote.stream.node_id}:'
|
||||
f'{self.remote.stream.conn_no}] '
|
||||
f'Connected to {addr}')
|
||||
asyncio.create_task(self.remote.ifc.client_loop(addr))
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
logging.info(f'{error}')
|
||||
except Exception:
|
||||
Infos.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception for {addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
'''publish data to MQTT broker'''
|
||||
stream = self.local.stream
|
||||
if not stream or not stream.unique_id:
|
||||
return
|
||||
# check if new inverter or collector infos are available or when the
|
||||
# home assistant has changed the status back to online
|
||||
try:
|
||||
if (('inverter' in stream.new_data and stream.new_data['inverter'])
|
||||
or ('collector' in stream.new_data and
|
||||
stream.new_data['collector'])
|
||||
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||
await self._register_proxy_stat_home_assistant()
|
||||
await self.__register_home_assistant(stream)
|
||||
self.__ha_restarts = self.mqtt.ha_restarts
|
||||
|
||||
for key in stream.new_data:
|
||||
await self.__async_publ_mqtt_packet(stream, key)
|
||||
for key in Infos.new_stat_data:
|
||||
await Proxy._async_publ_mqtt_proxy_stat(key)
|
||||
|
||||
except MqttCodeError as error:
|
||||
logging.error(f'Mqtt except: {error}')
|
||||
except Exception:
|
||||
Infos.inc_counter('SW_Exception')
|
||||
logging.error(
|
||||
f"Inverter: Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def __async_publ_mqtt_packet(self, stream, key):
|
||||
db = stream.db.db
|
||||
if key in db and stream.new_data[key]:
|
||||
data_json = json.dumps(db[key])
|
||||
node_id = stream.node_id
|
||||
logger_mqtt.debug(f'{key}: {data_json}')
|
||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
stream.new_data[key] = False
|
||||
|
||||
async def __register_home_assistant(self, stream) -> None:
|
||||
'''register all our topics at home assistant'''
|
||||
for data_json, component, node_id, id in stream.db.ha_confs(
|
||||
self.entity_prfx, stream.node_id, stream.unique_id,
|
||||
stream.sug_area):
|
||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
||||
f" node_id:'{node_id}' {data_json}")
|
||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
||||
f"/{node_id}{id}/config", data_json)
|
||||
|
||||
stream.db.reg_clr_at_midnight(f'{self.entity_prfx}{stream.node_id}')
|
||||
40
app/src/inverter_ifc.py
Normal file
40
app/src/inverter_ifc.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
if __name__ == "app.src.inverter_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
class InverterIfc(metaclass=AbstractIterMeta):
|
||||
_registry = []
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
config_id: str, prot_class,
|
||||
client_mode: bool):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def __enter__(self):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def healthy(self) -> bool:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
async def disc(self, shutdown_started=False) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
async def create_remote(self) -> None:
|
||||
pass # pragma: no cover
|
||||
9
app/src/iter_registry.py
Normal file
9
app/src/iter_registry.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class AbstractIterMeta(ABCMeta):
|
||||
def __iter__(cls):
|
||||
for ref in cls._registry:
|
||||
obj = ref()
|
||||
if obj is not None:
|
||||
yield obj
|
||||
@@ -1,5 +1,5 @@
|
||||
[loggers]
|
||||
keys=root,tracer,mesg,conn,data,mqtt
|
||||
keys=root,tracer,mesg,conn,data,mqtt,asyncio
|
||||
|
||||
[handlers]
|
||||
keys=console_handler,file_handler_name1,file_handler_name2
|
||||
@@ -24,6 +24,12 @@ handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=mqtt
|
||||
|
||||
[logger_asyncio]
|
||||
level=INFO
|
||||
handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=asyncio
|
||||
|
||||
[logger_data]
|
||||
level=DEBUG
|
||||
handlers=file_handler_name1
|
||||
|
||||
@@ -1,65 +1,97 @@
|
||||
import logging
|
||||
import weakref
|
||||
from typing import Callable
|
||||
from enum import Enum
|
||||
|
||||
|
||||
if __name__ == "app.src.messages":
|
||||
from app.src.infos import Infos
|
||||
from app.src.protocol_ifc import ProtocolIfc
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
else: # pragma: no cover
|
||||
from infos import Infos
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
|
||||
def hex_dump_memory(level, info, data, num):
|
||||
def __hex_val(n, data, data_len):
|
||||
line = ''
|
||||
for j in range(n-16, n):
|
||||
if j >= data_len:
|
||||
break
|
||||
line += '%02x ' % abs(data[j])
|
||||
return line
|
||||
|
||||
|
||||
def __asc_val(n, data, data_len):
|
||||
line = ''
|
||||
for j in range(n-16, n):
|
||||
if j >= data_len:
|
||||
break
|
||||
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
|
||||
line += '%c' % c
|
||||
return line
|
||||
|
||||
|
||||
def hex_dump(data, data_len) -> list:
|
||||
n = 0
|
||||
lines = []
|
||||
|
||||
for i in range(0, data_len, 16):
|
||||
line = ' '
|
||||
line += '%04x | ' % (i)
|
||||
n += 16
|
||||
line += __hex_val(n, data, data_len)
|
||||
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
|
||||
line += __asc_val(n, data, data_len)
|
||||
lines.append(line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def hex_dump_str(data, data_len):
|
||||
lines = hex_dump(data, data_len)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def hex_dump_memory(level, info, data, data_len):
|
||||
lines = []
|
||||
lines.append(info)
|
||||
tracer = logging.getLogger('tracer')
|
||||
if not tracer.isEnabledFor(level):
|
||||
return
|
||||
|
||||
for i in range(0, num, 16):
|
||||
line = ' '
|
||||
line += '%04x | ' % (i)
|
||||
n += 16
|
||||
|
||||
for j in range(n-16, n):
|
||||
if j >= len(data):
|
||||
break
|
||||
line += '%02x ' % abs(data[j])
|
||||
|
||||
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
|
||||
|
||||
for j in range(n-16, n):
|
||||
if j >= len(data):
|
||||
break
|
||||
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
|
||||
line += '%c' % c
|
||||
|
||||
lines.append(line)
|
||||
lines += hex_dump(data, data_len)
|
||||
|
||||
tracer.log(level, '\n'.join(lines))
|
||||
|
||||
|
||||
class IterRegistry(type):
|
||||
def __iter__(cls):
|
||||
for ref in cls._registry:
|
||||
obj = ref()
|
||||
if obj is not None:
|
||||
yield obj
|
||||
class State(Enum):
|
||||
'''state of the logical connection'''
|
||||
init = 0
|
||||
'''just created'''
|
||||
received = 1
|
||||
'''at least one packet received'''
|
||||
up = 2
|
||||
'''at least one cmd-rsp transaction'''
|
||||
pend = 3
|
||||
'''inverter transaction pending, don't send MODBUS cmds'''
|
||||
closed = 4
|
||||
'''connection closed'''
|
||||
|
||||
|
||||
class Message(metaclass=IterRegistry):
|
||||
_registry = []
|
||||
STATE_INIT = 0
|
||||
STATE_UP = 2
|
||||
STATE_CLOSED = 3
|
||||
class Message(ProtocolIfc):
|
||||
MAX_START_TIME = 400
|
||||
'''maximum time without a received msg in sec'''
|
||||
MAX_INV_IDLE_TIME = 120
|
||||
'''maximum time without a received msg from the inverter in sec'''
|
||||
MAX_DEF_IDLE_TIME = 360
|
||||
'''maximum default time without a received msg in sec'''
|
||||
|
||||
def __init__(self, server_side: bool, send_modbus_cb:
|
||||
Callable[[bytes, int, str], None], mb_timeout):
|
||||
Callable[[bytes, int, str], None], mb_timeout: int):
|
||||
self._registry.append(weakref.ref(self))
|
||||
|
||||
self.server_side = server_side
|
||||
@@ -72,13 +104,20 @@ class Message(metaclass=IterRegistry):
|
||||
self.header_len = 0
|
||||
self.data_len = 0
|
||||
self.unique_id = 0
|
||||
self.node_id = ''
|
||||
self._node_id = ''
|
||||
self.sug_area = ''
|
||||
self._recv_buffer = bytearray(0)
|
||||
self._send_buffer = bytearray(0)
|
||||
self._forward_buffer = bytearray(0)
|
||||
self.new_data = {}
|
||||
self.state = self.STATE_INIT
|
||||
self.state = State.init
|
||||
self.shutdown_started = False
|
||||
|
||||
@property
|
||||
def node_id(self):
|
||||
return self._node_id
|
||||
|
||||
@node_id.setter
|
||||
def node_id(self, value):
|
||||
self._node_id = value
|
||||
self.ifc.set_node_id(value)
|
||||
|
||||
'''
|
||||
Empty methods, that have to be implemented in any child class which
|
||||
@@ -88,18 +127,39 @@ class Message(metaclass=IterRegistry):
|
||||
# to our _recv_buffer
|
||||
return # pragma: no cover
|
||||
|
||||
def _update_header(self, _forward_buffer):
|
||||
'''callback for updating the header of the forward buffer'''
|
||||
return # pragma: no cover
|
||||
def _set_mqtt_timestamp(self, key, ts: float | None):
|
||||
if key not in self.new_data or \
|
||||
not self.new_data[key]:
|
||||
if key == 'grid':
|
||||
info_id = Register.TS_GRID
|
||||
elif key == 'input':
|
||||
info_id = Register.TS_INPUT
|
||||
elif key == 'total':
|
||||
info_id = Register.TS_TOTAL
|
||||
else:
|
||||
return
|
||||
# tstr = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts))
|
||||
# logger.info(f'update: key: {key} ts:{tstr}'
|
||||
self.db.set_db_def_value(info_id, round(ts))
|
||||
|
||||
def _timeout(self) -> int:
|
||||
if self.state == State.init or self.state == State.received:
|
||||
to = self.MAX_START_TIME
|
||||
elif self.state == State.up and \
|
||||
self.server_side and self.modbus_polling:
|
||||
to = self.MAX_INV_IDLE_TIME
|
||||
else:
|
||||
to = self.MAX_DEF_IDLE_TIME
|
||||
return to
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
if self.mb:
|
||||
del self.mb
|
||||
self.mb.close()
|
||||
self.mb = None
|
||||
pass # pragma: no cover
|
||||
# pragma: no cover
|
||||
|
||||
def inc_counter(self, counter: str) -> None:
|
||||
self.db.inc_counter(counter)
|
||||
|
||||
@@ -39,10 +39,12 @@ class Modbus():
|
||||
'''Modbus function code: Write Single Register'''
|
||||
|
||||
__crc_tab = []
|
||||
map = {
|
||||
mb_reg_mapping = {
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
# 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
|
||||
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf:1X}'"}, # noqa: E501
|
||||
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
@@ -104,9 +106,16 @@ class Modbus():
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.req_pend = False
|
||||
self.tim = None
|
||||
self.node_id = ''
|
||||
|
||||
def __del__(self):
|
||||
logging.debug(f'Modbus __del__:\n {self.counter}')
|
||||
def close(self):
|
||||
"""free the queue and erase the callback handlers"""
|
||||
logging.debug('Modbus close:')
|
||||
self.__stop_timer()
|
||||
self.rsp_handler = None
|
||||
self.snd_handler = None
|
||||
while not self.que.empty():
|
||||
self.que.get_nowait()
|
||||
|
||||
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||
log_lvl=logging.DEBUG) -> None:
|
||||
@@ -126,7 +135,7 @@ class Modbus():
|
||||
if self.que.qsize() == 1:
|
||||
self.__send_next_from_que()
|
||||
|
||||
def recv_req(self, buf: bytearray,
|
||||
def recv_req(self, buf: bytes,
|
||||
rsp_handler: Callable[[None], None] = None) -> bool:
|
||||
"""Add the received Modbus RTU request to the tx queue
|
||||
|
||||
@@ -151,7 +160,7 @@ class Modbus():
|
||||
|
||||
return True
|
||||
|
||||
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \
|
||||
def recv_resp(self, info_db, buf: bytes, node_id: str) -> \
|
||||
Generator[tuple[str, bool, int | float | str], None, None]:
|
||||
"""Generator which check and parse a received MODBUS response.
|
||||
|
||||
@@ -168,56 +177,20 @@ class Modbus():
|
||||
5: No MODBUS request pending
|
||||
"""
|
||||
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||
if not self.req_pend:
|
||||
self.err = 5
|
||||
return
|
||||
if not self.__check_crc(buf):
|
||||
logger.error('Modbus resp: CRC error')
|
||||
self.err = 1
|
||||
return
|
||||
if buf[0] != self.last_addr:
|
||||
logger.info(f'Modbus resp: Wrong addr {buf[0]}')
|
||||
self.err = 2
|
||||
return
|
||||
self.node_id = node_id
|
||||
|
||||
fcode = buf[1]
|
||||
if fcode != self.last_fcode:
|
||||
logger.info(f'Modbus: Wrong fcode {fcode} != {self.last_fcode}')
|
||||
self.err = 3
|
||||
data_available = self.last_addr == self.INV_ADDR and \
|
||||
(fcode == 3 or fcode == 4)
|
||||
|
||||
if self.__resp_error_check(buf, data_available):
|
||||
return
|
||||
if self.last_addr == self.INV_ADDR and \
|
||||
(fcode == 3 or fcode == 4):
|
||||
|
||||
if data_available:
|
||||
elmlen = buf[2] >> 1
|
||||
if elmlen != self.last_len:
|
||||
logger.info(f'Modbus: len error {elmlen} != {self.last_len}')
|
||||
self.err = 4
|
||||
return
|
||||
first_reg = self.last_reg # save last_reg before sending next pdu
|
||||
self.__stop_timer() # stop timer and send next pdu
|
||||
|
||||
for i in range(0, elmlen):
|
||||
addr = first_reg+i
|
||||
if addr in self.map:
|
||||
row = self.map[addr]
|
||||
info_id = row['reg']
|
||||
fmt = row['fmt']
|
||||
val = struct.unpack_from(fmt, buf, 3+2*i)
|
||||
result = val[0]
|
||||
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
|
||||
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||
|
||||
if keys:
|
||||
name, update = info_db.update_db(keys, must_incr,
|
||||
result)
|
||||
yield keys[0], update, result
|
||||
if update:
|
||||
info_db.tracer.log(level,
|
||||
f'[\'{node_id}\']MODBUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
yield from self.__process_data(info_db, buf, first_reg, elmlen)
|
||||
else:
|
||||
self.__stop_timer()
|
||||
|
||||
@@ -226,6 +199,64 @@ class Modbus():
|
||||
self.rsp_handler()
|
||||
self.__send_next_from_que()
|
||||
|
||||
def __resp_error_check(self, buf: bytes, data_available: bool) -> bool:
|
||||
'''Check the MODBUS response for errors, returns True if one accure'''
|
||||
if not self.req_pend:
|
||||
self.err = 5
|
||||
return True
|
||||
if not self.__check_crc(buf):
|
||||
logger.error(f'[{self.node_id}] Modbus resp: CRC error')
|
||||
self.err = 1
|
||||
return True
|
||||
if buf[0] != self.last_addr:
|
||||
logger.info(f'[{self.node_id}] Modbus resp: Wrong addr {buf[0]}')
|
||||
self.err = 2
|
||||
return True
|
||||
fcode = buf[1]
|
||||
if fcode != self.last_fcode:
|
||||
logger.info(f'[{self.node_id}] Modbus: Wrong fcode {fcode}'
|
||||
f' != {self.last_fcode}')
|
||||
self.err = 3
|
||||
return True
|
||||
if data_available:
|
||||
elmlen = buf[2] >> 1
|
||||
if elmlen != self.last_len:
|
||||
logger.info(f'[{self.node_id}] Modbus: len error {elmlen}'
|
||||
f' != {self.last_len}')
|
||||
self.err = 4
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __get_value(self, buf: bytes, idx: int, row: dict):
|
||||
'''get a value from the received buffer'''
|
||||
val = struct.unpack_from(row['fmt'], buf, idx)
|
||||
result = val[0]
|
||||
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
|
||||
'''Generator over received registers, updates the db'''
|
||||
for i in range(0, elmlen):
|
||||
addr = first_reg+i
|
||||
if addr in self.mb_reg_mapping:
|
||||
row = self.mb_reg_mapping[addr]
|
||||
info_id = row['reg']
|
||||
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||
if keys:
|
||||
result = self.__get_value(buf, 3+2*i, row)
|
||||
name, update = info_db.update_db(keys, must_incr,
|
||||
result)
|
||||
yield keys[0], update, result
|
||||
if update:
|
||||
info_db.tracer.log(level,
|
||||
f'[{self.node_id}] MODBUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
|
||||
'''
|
||||
MODBUS response timer
|
||||
'''
|
||||
@@ -241,6 +272,7 @@ class Modbus():
|
||||
# logging.debug(f'Modbus stop timer {self}')
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = None
|
||||
|
||||
def __timeout_cb(self) -> None:
|
||||
'''Rsponse timeout handler retransmit pdu or send next pdu'''
|
||||
@@ -252,7 +284,10 @@ class Modbus():
|
||||
self.__start_timer()
|
||||
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
|
||||
else:
|
||||
logger.info(f'Modbus timeout {self}')
|
||||
logger.info(f'[{self.node_id}] Modbus timeout '
|
||||
f'(FCode: {self.last_fcode} '
|
||||
f'Reg: 0x{self.last_reg:04x}, '
|
||||
f'{self.last_len})')
|
||||
self.counter['timeouts'] += 1
|
||||
self.__send_next_from_que()
|
||||
|
||||
@@ -281,11 +316,11 @@ class Modbus():
|
||||
'''
|
||||
Helper function for CRC-16 handling
|
||||
'''
|
||||
def __check_crc(self, msg: bytearray) -> bool:
|
||||
def __check_crc(self, msg: bytes) -> bool:
|
||||
'''Check CRC-16 and returns True if valid'''
|
||||
return 0 == self.__calc_crc(msg)
|
||||
|
||||
def __calc_crc(self, buffer: bytearray) -> int:
|
||||
def __calc_crc(self, buffer: bytes) -> int:
|
||||
'''Build CRC-16 for buffer and returns it'''
|
||||
crc = CRC_INIT
|
||||
|
||||
|
||||
91
app/src/modbus_tcp.py
Normal file
91
app/src/modbus_tcp.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import logging
|
||||
import traceback
|
||||
import asyncio
|
||||
|
||||
if __name__ == "app.src.modbus_tcp":
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
|
||||
class ModbusConn():
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.addr = (host, port)
|
||||
self.inverter = None
|
||||
|
||||
async def __aenter__(self) -> 'InverterG3P':
|
||||
'''Establish a client connection to the TSUN cloud'''
|
||||
connection = asyncio.open_connection(self.host, self.port)
|
||||
reader, writer = await connection
|
||||
self.inverter = InverterG3P(reader, writer,
|
||||
client_mode=True)
|
||||
self.inverter.__enter__()
|
||||
stream = self.inverter.local.stream
|
||||
logging.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||
f'Connected to {self.addr}')
|
||||
Infos.inc_counter('Inverter_Cnt')
|
||||
await self.inverter.local.ifc.publish_outstanding_mqtt()
|
||||
return self.inverter
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
Infos.dec_counter('Inverter_Cnt')
|
||||
await self.inverter.local.ifc.publish_outstanding_mqtt()
|
||||
self.inverter.__exit__(exc_type, exc, tb)
|
||||
|
||||
|
||||
class ModbusTcp():
|
||||
|
||||
def __init__(self, loop, tim_restart=10) -> None:
|
||||
self.tim_restart = tim_restart
|
||||
|
||||
inverters = Config.get('inverters')
|
||||
# logging.info(f'Inverters: {inverters}')
|
||||
|
||||
for inv in inverters.values():
|
||||
if (type(inv) is dict
|
||||
and 'monitor_sn' in inv
|
||||
and 'client_mode' in inv):
|
||||
client = inv['client_mode']
|
||||
# logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
|
||||
loop.create_task(self.modbus_loop(client['host'],
|
||||
client['port'],
|
||||
inv['monitor_sn']))
|
||||
|
||||
async def modbus_loop(self, host, port, snr: int) -> None:
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
while True:
|
||||
try:
|
||||
async with ModbusConn(host, port) as inverter:
|
||||
stream = inverter.local.stream
|
||||
await stream.send_start_cmd(snr, host)
|
||||
await stream.ifc.loop()
|
||||
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||
f'Connection closed - Shutdown: '
|
||||
f'{stream.shutdown_started}')
|
||||
if stream.shutdown_started:
|
||||
return
|
||||
del inverter # decrease ref counter after the with block
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
logging.debug(f'Inv-conn:{error}')
|
||||
|
||||
except OSError as error:
|
||||
if error.errno == 113: # pragma: no cover
|
||||
logging.debug(f'os-error:{error}')
|
||||
else:
|
||||
logging.info(f'os-error: {error}')
|
||||
|
||||
except Exception:
|
||||
logging.error(
|
||||
f"ModbusTcpCreate: Exception for {(host, port)}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
await asyncio.sleep(self.tim_restart)
|
||||
160
app/src/mqtt.py
160
app/src/mqtt.py
@@ -2,26 +2,40 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from config import Config
|
||||
from singleton import Singleton
|
||||
if __name__ == "app.src.mqtt":
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import Message
|
||||
from app.src.config import Config
|
||||
from app.src.singleton import Singleton
|
||||
else: # pragma: no cover
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from config import Config
|
||||
from singleton import Singleton
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
class Mqtt(metaclass=Singleton):
|
||||
__client = None
|
||||
__cb_MqttIsUp = None
|
||||
__cb_mqtt_is_up = None
|
||||
|
||||
def __init__(self, cb_MqttIsUp):
|
||||
def __init__(self, cb_mqtt_is_up):
|
||||
logger_mqtt.debug('MQTT: __init__')
|
||||
if cb_MqttIsUp:
|
||||
self.__cb_MqttIsUp = cb_MqttIsUp
|
||||
if cb_mqtt_is_up:
|
||||
self.__cb_mqtt_is_up = cb_mqtt_is_up
|
||||
loop = asyncio.get_event_loop()
|
||||
self.task = loop.create_task(self.__loop())
|
||||
self.ha_restarts = 0
|
||||
|
||||
ha = Config.get('ha')
|
||||
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
|
||||
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
|
||||
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
|
||||
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
|
||||
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
|
||||
|
||||
@property
|
||||
def ha_restarts(self):
|
||||
return self._ha_restarts
|
||||
@@ -30,15 +44,13 @@ class Mqtt(metaclass=Singleton):
|
||||
def ha_restarts(self, value):
|
||||
self._ha_restarts = value
|
||||
|
||||
def __del__(self):
|
||||
logger_mqtt.debug('MQTT: __del__')
|
||||
|
||||
async def close(self) -> None:
|
||||
logger_mqtt.debug('MQTT: close')
|
||||
self.task.cancel()
|
||||
try:
|
||||
await self.task
|
||||
except Exception as e:
|
||||
|
||||
except (asyncio.CancelledError, Exception) as e:
|
||||
logging.debug(f"Mqtt.close: exception: {e} ...")
|
||||
|
||||
async def publish(self, topic: str, payload: str | bytes | bytearray
|
||||
@@ -48,7 +60,6 @@ class Mqtt(metaclass=Singleton):
|
||||
|
||||
async def __loop(self) -> None:
|
||||
mqtt = Config.get('mqtt')
|
||||
ha = Config.get('ha')
|
||||
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
|
||||
f'{mqtt["port"]} '
|
||||
f'user:{mqtt["user"]}')
|
||||
@@ -58,51 +69,24 @@ class Mqtt(metaclass=Singleton):
|
||||
password=mqtt['passwd'])
|
||||
|
||||
interval = 5 # Seconds
|
||||
ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||
mb_rated_topic = "tsun/+/rated_load" # fixme
|
||||
mb_reads_topic = "tsun/+/modbus_read_regs" # fixme
|
||||
mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme
|
||||
mb_at_cmd_topic = "tsun/+/at_cmd" # fixme
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with self.__client:
|
||||
logger_mqtt.info('MQTT broker connection established')
|
||||
|
||||
if self.__cb_MqttIsUp:
|
||||
await self.__cb_MqttIsUp()
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
# async with self.__client.messages() as messages:
|
||||
await self.__client.subscribe(ha_status_topic)
|
||||
await self.__client.subscribe(mb_rated_topic)
|
||||
await self.__client.subscribe(mb_reads_topic)
|
||||
await self.__client.subscribe(mb_inputs_topic)
|
||||
await self.__client.subscribe(mb_at_cmd_topic)
|
||||
await self.__client.subscribe(self.ha_status_topic)
|
||||
await self.__client.subscribe(self.mb_rated_topic)
|
||||
await self.__client.subscribe(self.mb_out_coeff_topic)
|
||||
await self.__client.subscribe(self.mb_reads_topic)
|
||||
await self.__client.subscribe(self.mb_inputs_topic)
|
||||
await self.__client.subscribe(self.mb_at_cmd_topic)
|
||||
|
||||
async for message in self.__client.messages:
|
||||
if message.topic.matches(ha_status_topic):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
await self.__cb_MqttIsUp()
|
||||
|
||||
if message.topic.matches(mb_rated_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
1, 0x2008)
|
||||
|
||||
if message.topic.matches(mb_reads_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_REGS, 2)
|
||||
|
||||
if message.topic.matches(mb_inputs_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_INPUTS, 2)
|
||||
|
||||
if message.topic.matches(mb_at_cmd_topic):
|
||||
await self.at_cmd(message)
|
||||
await self.dispatch_msg(message)
|
||||
|
||||
except aiomqtt.MqttError:
|
||||
if Config.is_default('mqtt'):
|
||||
@@ -126,46 +110,76 @@ class Mqtt(metaclass=Singleton):
|
||||
f"Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def dispatch_msg(self, message):
|
||||
if message.topic.matches(self.ha_status_topic):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
if message.topic.matches(self.mb_rated_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
1, 0x2008)
|
||||
|
||||
if message.topic.matches(self.mb_out_coeff_topic):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 1024/100)
|
||||
if val < 0 or val > 1024:
|
||||
logger_mqtt.error('out_coeff: value must be in'
|
||||
'the range 0..100,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
0, 0x202c, val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if message.topic.matches(self.mb_reads_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_REGS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_inputs_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_INPUTS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_at_cmd_topic):
|
||||
await self.at_cmd(message)
|
||||
|
||||
def each_inverter(self, message, func_name: str):
|
||||
topic = str(message.topic)
|
||||
node_id = topic.split('/')[1] + '/'
|
||||
found = False
|
||||
for m in Message:
|
||||
if m.server_side and (m.node_id == node_id):
|
||||
found = True
|
||||
logger_mqtt.debug(f'Found: {node_id}')
|
||||
fnc = getattr(m, func_name, None)
|
||||
if callable(fnc):
|
||||
yield fnc
|
||||
else:
|
||||
logger_mqtt.warning(f'Cmd not supported by: {node_id}')
|
||||
break
|
||||
|
||||
if not found:
|
||||
else:
|
||||
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
||||
|
||||
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||
topic = str(message.topic)
|
||||
node_id = topic.split('/')[1] + '/'
|
||||
# refactor into a loop over a table
|
||||
payload = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info(f'InvCnf: {node_id}:{payload}')
|
||||
for m in Message:
|
||||
if m.server_side and (m.node_id == node_id):
|
||||
logger_mqtt.info(f'Found: {node_id}')
|
||||
fnc = getattr(m, "send_modbus_cmd", None)
|
||||
res = payload.split(',')
|
||||
if params != len(res):
|
||||
logger_mqtt.error(f'Parameter expected: {params}, '
|
||||
f'got: {len(res)}')
|
||||
return
|
||||
|
||||
if callable(fnc):
|
||||
if params == 1:
|
||||
val = int(payload)
|
||||
elif params == 2:
|
||||
addr = int(res[0], base=16)
|
||||
val = int(res[1]) # lenght
|
||||
await fnc(func, addr, val, logging.INFO)
|
||||
for fnc in self.each_inverter(message, "send_modbus_cmd"):
|
||||
res = payload.split(',')
|
||||
if params > 0 and params != len(res):
|
||||
logger_mqtt.error(f'Parameter expected: {params}, '
|
||||
f'got: {len(res)}')
|
||||
return
|
||||
if params == 1:
|
||||
val = int(payload)
|
||||
elif params == 2:
|
||||
addr = int(res[0], base=16)
|
||||
val = int(res[1]) # lenght
|
||||
await fnc(func, addr, val, logging.INFO)
|
||||
|
||||
async def at_cmd(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
|
||||
35
app/src/my_timer.py
Normal file
35
app/src/my_timer.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from itertools import count
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, cb, id_str: str = ''):
|
||||
self.__timeout_cb = cb
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.tim = None
|
||||
self.id_str = id_str
|
||||
self.exp_count = count(0)
|
||||
|
||||
def start(self, timeout: float) -> None:
|
||||
'''Start timer with timeout seconds'''
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = self.loop.call_later(timeout, self.__timeout)
|
||||
logging.debug(f'[{self.id_str}]Start timer')
|
||||
|
||||
def stop(self) -> None:
|
||||
'''Stop timer'''
|
||||
logging.debug(f'[{self.id_str}]Stop timer')
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = None
|
||||
|
||||
def __timeout(self) -> None:
|
||||
'''timer expired handler'''
|
||||
logging.debug(f'[{self.id_str}]Timer expired')
|
||||
self.__timeout_cb(next(self.exp_count))
|
||||
|
||||
def close(self) -> None:
|
||||
self.stop()
|
||||
self.__timeout_cb = None
|
||||
21
app/src/protocol_ifc.py
Normal file
21
app/src/protocol_ifc.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
if __name__ == "app.src.protocol_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
from async_ifc import AsyncIfc
|
||||
|
||||
|
||||
class ProtocolIfc(metaclass=AbstractIterMeta):
|
||||
_registry = []
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||
client_mode: bool = False, id_str=b''):
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
pass # pragma: no cover
|
||||
@@ -1,18 +1,45 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
from config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
# logger = logging.getLogger('conn')
|
||||
if __name__ == "app.src.proxy":
|
||||
from app.src.config import Config
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
class Inverter():
|
||||
class Proxy():
|
||||
'''class Proxy is a baseclass
|
||||
|
||||
The class has some class method for managing common resources like a
|
||||
connection to the MQTT broker or proxy error counter which are common
|
||||
for all inverter connection
|
||||
|
||||
Instances of the class are connections to an inverter and can have an
|
||||
optional link to an remote connection to the TSUN cloud. A remote
|
||||
connection dies with the inverter connection.
|
||||
|
||||
class methods:
|
||||
class_init(): initialize the common resources of the proxy (MQTT
|
||||
broker, Proxy DB, etc). Must be called before the
|
||||
first inverter instance can be created
|
||||
class_close(): release the common resources of the proxy. Should not
|
||||
be called before any instances of the class are
|
||||
destroyed
|
||||
|
||||
methods:
|
||||
create_remote(): Establish a client connection to the TSUN cloud
|
||||
async_publ_mqtt(): Publish data to MQTT broker
|
||||
'''
|
||||
@classmethod
|
||||
def class_init(cls) -> None:
|
||||
logging.debug('Inverter.class_init')
|
||||
logging.debug('Proxy.class_init')
|
||||
# initialize the proxy statistics
|
||||
Infos.static_init()
|
||||
cls.db_stat = Infos()
|
||||
@@ -34,7 +61,7 @@ class Inverter():
|
||||
# reset at midnight when you restart the proxy just before
|
||||
# midnight!
|
||||
inverters = Config.get('inverters')
|
||||
# logger.debug(f'Inverters: {inverters}')
|
||||
# logger.debug(f'Proxys: {inverters}')
|
||||
for inv in inverters.values():
|
||||
if (type(inv) is dict):
|
||||
node_id = inv['node_id']
|
||||
@@ -72,8 +99,8 @@ class Inverter():
|
||||
Infos.new_stat_data[key] = False
|
||||
|
||||
@classmethod
|
||||
def class_close(cls, loop) -> None:
|
||||
logging.debug('Inverter.class_close')
|
||||
def class_close(cls, loop) -> None: # pragma: no cover
|
||||
logging.debug('Proxy.class_close')
|
||||
logging.info('Close MQTT Task')
|
||||
loop.run_until_complete(cls.mqtt.close())
|
||||
cls.mqtt = None
|
||||
@@ -3,8 +3,6 @@ import json
|
||||
from mqtt import Mqtt
|
||||
from aiocron import crontab
|
||||
from infos import ClrAtMidnight
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
@@ -21,9 +19,6 @@ class Schedule:
|
||||
|
||||
crontab('0 0 * * *', func=cls.atmidnight, start=True)
|
||||
|
||||
# every minute
|
||||
crontab('* * * * *', func=cls.regular_modbus_cmds, start=True)
|
||||
|
||||
@classmethod
|
||||
async def atmidnight(cls) -> None:
|
||||
'''Clear daily counters at midnight'''
|
||||
@@ -33,15 +28,3 @@ class Schedule:
|
||||
logger_mqtt.debug(f'{key}: {data}')
|
||||
data_json = json.dumps(data)
|
||||
await cls.mqtt.publish(f"{key}", data_json)
|
||||
|
||||
@classmethod
|
||||
async def regular_modbus_cmds(cls):
|
||||
for m in Message:
|
||||
if m.server_side:
|
||||
fnc = getattr(m, "send_modbus_cmd", None)
|
||||
if callable(fnc):
|
||||
await fnc(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
|
||||
if 0 == (cls.count % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
await fnc(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
||||
cls.count += 1
|
||||
|
||||
@@ -2,57 +2,115 @@ import logging
|
||||
import asyncio
|
||||
import signal
|
||||
import os
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
from messages import Message
|
||||
from inverter import Inverter
|
||||
from proxy import Proxy
|
||||
from inverter_ifc import InverterIfc
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from scheduler import Schedule
|
||||
from config import Config
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
proxy_is_up = False
|
||||
|
||||
|
||||
async def handle_client(reader, writer):
|
||||
@routes.get('/')
|
||||
async def hello(request):
|
||||
return web.Response(text="Hello, world")
|
||||
|
||||
|
||||
@routes.get('/-/ready')
|
||||
async def ready(request):
|
||||
if proxy_is_up:
|
||||
status = 200
|
||||
text = 'Is ready'
|
||||
else:
|
||||
status = 503
|
||||
text = 'Not ready'
|
||||
return web.Response(status=status, text=text)
|
||||
|
||||
|
||||
@routes.get('/-/healthy')
|
||||
async def healthy(request):
|
||||
|
||||
if proxy_is_up:
|
||||
# logging.info('web reqeust healthy()')
|
||||
for inverter in InverterIfc:
|
||||
try:
|
||||
res = inverter.healthy()
|
||||
if not res:
|
||||
return web.Response(status=503, text="I have a problem")
|
||||
except Exception as err:
|
||||
logging.info(f'Exception:{err}')
|
||||
|
||||
return web.Response(status=200, text="I'm fine")
|
||||
|
||||
|
||||
async def webserver(addr, port):
|
||||
'''coro running our webserver'''
|
||||
app = web.Application()
|
||||
app.add_routes(routes)
|
||||
runner = web.AppRunner(app)
|
||||
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, addr, port)
|
||||
await site.start()
|
||||
logging.info(f'HTTP server listen on port: {port}')
|
||||
|
||||
try:
|
||||
# Normal interaction with aiohttp
|
||||
while True:
|
||||
await asyncio.sleep(3600) # sleep forever
|
||||
except asyncio.CancelledError:
|
||||
logging.info('HTTP server cancelled')
|
||||
await runner.cleanup()
|
||||
logging.debug('HTTP cleanup done')
|
||||
|
||||
|
||||
async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
||||
'''Handles a new incoming connection and starts an async loop'''
|
||||
|
||||
addr = writer.get_extra_info('peername')
|
||||
await InverterG3(reader, writer, addr).server_loop(addr)
|
||||
with inv_class(reader, writer) as inv:
|
||||
await inv.local.ifc.server_loop()
|
||||
|
||||
|
||||
async def handle_client_v2(reader, writer):
|
||||
'''Handles a new incoming connection and starts an async loop'''
|
||||
|
||||
addr = writer.get_extra_info('peername')
|
||||
await InverterG3P(reader, writer, addr).server_loop(addr)
|
||||
|
||||
|
||||
async def handle_shutdown(loop):
|
||||
async def handle_shutdown(web_task):
|
||||
'''Close all TCP connections and stop the event loop'''
|
||||
|
||||
logging.info('Shutdown due to SIGTERM')
|
||||
global proxy_is_up
|
||||
proxy_is_up = False
|
||||
|
||||
#
|
||||
# first, disc all open TCP connections gracefully
|
||||
#
|
||||
for stream in Message:
|
||||
try:
|
||||
await asyncio.wait_for(stream.disc(), 2)
|
||||
except Exception:
|
||||
pass
|
||||
logging.info('Disconnecting done')
|
||||
for inverter in InverterIfc:
|
||||
await inverter.disc(True)
|
||||
|
||||
logging.info('Proxy disconnecting done')
|
||||
|
||||
#
|
||||
# second, close all open TCP connections
|
||||
# second, cancel the web server
|
||||
#
|
||||
for stream in Message:
|
||||
stream.close()
|
||||
web_task.cancel()
|
||||
await web_task
|
||||
|
||||
#
|
||||
# at last, we stop the loop
|
||||
# now cancel all remaining (pending) tasks
|
||||
#
|
||||
pending = asyncio.all_tasks()
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
#
|
||||
# at last, start a coro for stopping the loop
|
||||
#
|
||||
logging.debug("Stop event loop")
|
||||
loop.stop()
|
||||
|
||||
logging.info('Shutdown complete')
|
||||
|
||||
|
||||
def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
@@ -84,16 +142,30 @@ if __name__ == "__main__":
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
logging.getLogger('data').setLevel(log_level)
|
||||
logging.getLogger('tracer').setLevel(log_level)
|
||||
logging.getLogger('asyncio').setLevel(log_level)
|
||||
# logging.getLogger('mqtt').setLevel(log_level)
|
||||
|
||||
# read config file
|
||||
Config.class_init()
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
Inverter.class_init()
|
||||
# read config file
|
||||
ConfigErr = Config.class_init()
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
Proxy.class_init()
|
||||
Schedule.start()
|
||||
ModbusTcp(loop)
|
||||
|
||||
#
|
||||
# Create tasks for our listening servers. These must be tasks! If we call
|
||||
# start_server directly out of our main task, the eventloop will be blocked
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
for inv_class, port in [(InverterG3, 5005), (InverterG3P, 10000)]:
|
||||
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
|
||||
handle_client(r, w, i),
|
||||
'0.0.0.0', port))
|
||||
web_task = loop.create_task(webserver('0.0.0.0', 8127))
|
||||
|
||||
#
|
||||
# Register some UNIX Signal handler for a gracefully server shutdown
|
||||
@@ -102,22 +174,18 @@ if __name__ == "__main__":
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
lambda loop=loop: asyncio.create_task(
|
||||
handle_shutdown(loop)))
|
||||
|
||||
#
|
||||
# Create taska for our listening servera. These must be tasks! If we call
|
||||
# start_server directly out of our main task, the eventloop will be blocked
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
||||
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
||||
handle_shutdown(web_task)))
|
||||
|
||||
loop.set_debug(log_level == logging.DEBUG)
|
||||
try:
|
||||
if ConfigErr is None:
|
||||
proxy_is_up = True
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
Inverter.class_close(loop)
|
||||
logging.info('Close event loop')
|
||||
logging.info("Event loop is stopped")
|
||||
Proxy.class_close(loop)
|
||||
logging.debug('Close event loop')
|
||||
loop.close()
|
||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
_instances = WeakValueDictionary()
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
# logger_mqtt.debug('singleton: __call__')
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton,
|
||||
cls).__call__(*args, **kwargs)
|
||||
instance = super(Singleton,
|
||||
cls).__call__(*args, **kwargs)
|
||||
cls._instances[cls] = instance
|
||||
|
||||
return cls._instances[cls]
|
||||
|
||||
532
app/tests/test_async_stream.py
Normal file
532
app/tests/test_async_stream.py
Normal file
@@ -0,0 +1,532 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
import gc
|
||||
import time
|
||||
|
||||
from app.src.infos import Infos
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from app.src.messages import Message
|
||||
from app.tests.test_modbus_tcp import FakeReader, FakeWriter
|
||||
from app.tests.test_inverter_base import config_conn, patch_open_connection
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
# initialize the proxy statistics
|
||||
Infos.static_init()
|
||||
|
||||
class FakeProto(Message):
|
||||
def __init__(self, server_side):
|
||||
super().__init__(server_side, None, 10)
|
||||
self.conn_no = 0
|
||||
|
||||
def fake_reader_fwd():
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
reader.on_recv.set()
|
||||
return reader
|
||||
|
||||
def test_timeout_cb():
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
def timeout():
|
||||
return 13
|
||||
|
||||
ifc = AsyncStreamClient(reader, writer, None, None)
|
||||
assert 360 == ifc._AsyncStream__timeout()
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
assert 13 == ifc._AsyncStream__timeout()
|
||||
ifc.prot_set_timeout_cb(None)
|
||||
assert 360 == ifc._AsyncStream__timeout()
|
||||
|
||||
# call healthy outside the contexter manager (__exit__() was called)
|
||||
assert ifc.healthy()
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
def test_health():
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
ifc = AsyncStreamClient(reader, writer, None, None)
|
||||
ifc.proc_start = time.time()
|
||||
assert ifc.healthy()
|
||||
ifc.proc_start = time.time() -10
|
||||
assert not ifc.healthy()
|
||||
ifc.proc_start = None
|
||||
assert ifc.healthy()
|
||||
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_cb():
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 0.1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1 # check that the closed method would not be called
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, None)
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 0
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
def app_read():
|
||||
nonlocal ifc
|
||||
ifc.proc_start -= 3
|
||||
return 0.01 # async wait of 0.01
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
ifc.proc_max = 0
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
ifc.rx_set_cb(app_read)
|
||||
await ifc.client_loop('')
|
||||
print('End loop')
|
||||
assert ifc.proc_max >= 3
|
||||
assert 13 == ifc.rx_len()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
def app_read():
|
||||
nonlocal ifc
|
||||
ifc.proc_start -= 3
|
||||
return 0.01 # async wait of 0.01
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
ifc.proc_max = 10
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
ifc.rx_set_cb(app_read)
|
||||
ifc.tx_add(b'test-data-resp')
|
||||
assert 14 == ifc.tx_len()
|
||||
await ifc.client_loop('')
|
||||
print('End loop')
|
||||
assert ifc.proc_max >= 3
|
||||
assert 13 == ifc.rx_len()
|
||||
assert 0 == ifc.tx_len()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publ_mqtt_cb():
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 0.1
|
||||
async def publ_mqtt():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(reader, writer, publ_mqtt, None, None)
|
||||
assert ifc.async_publ_mqtt
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.server_loop()
|
||||
assert cnt == 3 # 2 calls in server_loop() and 1 in loop()
|
||||
assert ifc.async_publ_mqtt
|
||||
ifc.close() # clears the closed callback
|
||||
assert not ifc.async_publ_mqtt
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_remote_cb():
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 0.1
|
||||
async def create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(reader, writer, None, create_remote, None)
|
||||
assert ifc.create_remote
|
||||
await ifc.create_remote()
|
||||
assert cnt == 1
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.server_loop()
|
||||
assert not ifc.create_remote
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sw_exception():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_SW_EXCEPT
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.client_loop('')
|
||||
print('End loop')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_os_error():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_OS_ERROR
|
||||
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
def timeout():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
ifc.prot_set_timeout_cb(timeout)
|
||||
await ifc.client_loop('')
|
||||
print('End loop')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
class TestType():
|
||||
FWD_NO_EXCPT = 1
|
||||
FWD_SW_EXCPT = 2
|
||||
FWD_TIMEOUT = 3
|
||||
FWD_OS_ERROR = 4
|
||||
FWD_OS_ERROR_NO_STREAM = 5
|
||||
FWD_RUNTIME_ERROR = 6
|
||||
FWD_RUNTIME_ERROR_NO_STREAM = 7
|
||||
|
||||
def create_remote(remote, test_type, with_close_hdr:bool = False):
|
||||
def update_hdr(buf):
|
||||
return
|
||||
def callback():
|
||||
if test_type == TestType.FWD_SW_EXCPT:
|
||||
remote.unknown_var += 1
|
||||
elif test_type == TestType.FWD_TIMEOUT:
|
||||
raise TimeoutError
|
||||
elif test_type == TestType.FWD_OS_ERROR:
|
||||
raise ConnectionRefusedError
|
||||
elif test_type == TestType.FWD_OS_ERROR_NO_STREAM:
|
||||
remote.stream = None
|
||||
raise ConnectionRefusedError
|
||||
elif test_type == TestType.FWD_RUNTIME_ERROR:
|
||||
raise RuntimeError("Peer closed")
|
||||
elif test_type == TestType.FWD_RUNTIME_ERROR_NO_STREAM:
|
||||
remote.stream = None
|
||||
raise RuntimeError("Peer closed")
|
||||
|
||||
def close():
|
||||
return
|
||||
if with_close_hdr:
|
||||
close_hndl = close
|
||||
else:
|
||||
close_hndl = None
|
||||
|
||||
remote.ifc = AsyncStreamClient(
|
||||
FakeReader(), FakeWriter(), StreamPtr(None), close_hndl)
|
||||
remote.ifc.prot_set_update_header_cb(update_hdr)
|
||||
remote.ifc.prot_set_init_new_client_conn_cb(callback)
|
||||
remote.stream = FakeProto(False)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote, ifc
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg2 ')
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_with_conn():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 0
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_no_conn():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_sw_except():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_SW_EXCPT)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_os_error():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_os_error2():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR, True)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_os_error3():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR_NO_STREAM)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_runtime_error():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_runtime_error2():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR, True)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_runtime_error3():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR_NO_STREAM, True)
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamServer(fake_reader_fwd(), FakeWriter(), None, _create_remote, remote)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
43
app/tests/test_byte_fifo.py
Normal file
43
app/tests/test_byte_fifo.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# test_with_pytest.py
|
||||
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
|
||||
def test_fifo():
|
||||
read = ByteFifo()
|
||||
assert 0 == len(read)
|
||||
read += b'12'
|
||||
assert 2 == len(read)
|
||||
read += bytearray("34", encoding='UTF8')
|
||||
assert 4 == len(read)
|
||||
assert b'12' == read.peek(2)
|
||||
assert 4 == len(read)
|
||||
assert b'1234' == read.peek()
|
||||
assert 4 == len(read)
|
||||
assert b'12' == read.get(2)
|
||||
assert 2 == len(read)
|
||||
assert b'34' == read.get()
|
||||
assert 0 == len(read)
|
||||
|
||||
def test_fifo_fmt():
|
||||
read = ByteFifo()
|
||||
read += b'1234'
|
||||
assert b'1234' == read.peek()
|
||||
assert " 0000 | 31 32 33 34 | 1234" == f'{read}'
|
||||
|
||||
def test_fifo_observer():
|
||||
read = ByteFifo()
|
||||
|
||||
def _read():
|
||||
assert b'1234' == read.get(4)
|
||||
|
||||
read += b'12'
|
||||
assert 2 == len(read)
|
||||
read()
|
||||
read.reg_trigger(_read)
|
||||
read += b'34'
|
||||
assert 4 == len(read)
|
||||
read()
|
||||
assert 0 == len(read)
|
||||
assert b'' == read.peek(2)
|
||||
assert b'' == read.get(2)
|
||||
assert 0 == len(read)
|
||||
@@ -7,11 +7,11 @@ class TstConfig(Config):
|
||||
|
||||
@classmethod
|
||||
def set(cls, cnf):
|
||||
cls.config = cnf
|
||||
cls.act_config = cnf
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict:
|
||||
return cls.config
|
||||
return cls.act_config
|
||||
|
||||
|
||||
def test_empty_config():
|
||||
@@ -20,7 +20,7 @@ def test_empty_config():
|
||||
Config.conf_schema.validate(cnf)
|
||||
assert False
|
||||
except SchemaMissingKeyError:
|
||||
assert True
|
||||
pass
|
||||
|
||||
def test_default_config():
|
||||
with open("app/config/default_config.toml", "rb") as f:
|
||||
@@ -28,10 +28,35 @@ def test_default_config():
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
assert True
|
||||
except:
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688}}}
|
||||
|
||||
def test_full_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
@@ -41,14 +66,13 @@ def test_full_config():
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {'node_id': '', 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
|
||||
'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||
'R170000000000001': {'modbus_polling': True, 'node_id': '', 'sensor_list': 0, 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'sensor_list': 0x1511, 'suggested_area': ''}}}
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
assert True
|
||||
except:
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||
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': True, 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': '', 'sensor_list': 0}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 5393}}}
|
||||
|
||||
def test_mininum_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
@@ -63,10 +87,9 @@ def test_mininum_config():
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
assert True
|
||||
except:
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}}}
|
||||
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': True, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}}}
|
||||
|
||||
def test_read_empty():
|
||||
cnf = {}
|
||||
@@ -74,7 +97,37 @@ def test_read_empty():
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
@@ -96,7 +149,37 @@ def test_read_cnf1():
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
cnf = TstConfig.get('solarman')
|
||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
@@ -109,7 +192,37 @@ def test_read_cnf2():
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
|
||||
def test_read_cnf3():
|
||||
@@ -126,7 +239,37 @@ def test_read_cnf4():
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
|
||||
def test_read_cnf5():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import json
|
||||
import json, math
|
||||
import logging
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.infos import Infos
|
||||
@@ -77,7 +77,7 @@ def test_table_definition():
|
||||
|
||||
|
||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||
pass
|
||||
pass # sideeffect is calling generator i.ha_proxy_confs()
|
||||
|
||||
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||
assert val == 0
|
||||
@@ -123,6 +123,30 @@ def test_table_definition():
|
||||
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||
assert val == 3
|
||||
|
||||
def test_table_remove():
|
||||
i = Infos()
|
||||
i.static_init() # initialize counter
|
||||
|
||||
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||
assert val == 0
|
||||
|
||||
# for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||
# pass
|
||||
test = 0
|
||||
for reg in Register:
|
||||
res = i.ha_remove(reg, node_id="garagendach/", snr='123') # noqa: E501
|
||||
if reg == Register.INVERTER_STATUS:
|
||||
test += 1
|
||||
assert res == ('{}', 'sensor', 'garagendach/', 'inv_status_123')
|
||||
elif reg == Register.COLLECT_INTERVAL:
|
||||
test += 1
|
||||
assert res == ('{}', 'sensor', 'garagendach/', 'data_collect_intval_123')
|
||||
|
||||
assert test == 2
|
||||
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||
assert val == 0
|
||||
|
||||
|
||||
def test_clr_at_midnight():
|
||||
i = Infos()
|
||||
i.static_init() # initialize counter
|
||||
@@ -198,24 +222,24 @@ def test_get_value():
|
||||
|
||||
i.set_db_def_value(Register.PV2_VOLTAGE, 30.3)
|
||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||
assert 30.3 == i.get_db_value(Register.PV2_VOLTAGE, None)
|
||||
assert math.isclose(30.3,i.get_db_value(Register.PV2_VOLTAGE, None), rel_tol=1e-09, abs_tol=1e-09)
|
||||
|
||||
def test_update_value():
|
||||
i = Infos()
|
||||
assert None == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||
|
||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||
name, update = i.update_db(keys, True, 30)
|
||||
_, update = i.update_db(keys, True, 30)
|
||||
assert update == True
|
||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||
|
||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||
name, update = i.update_db(keys, True, 30)
|
||||
_, update = i.update_db(keys, True, 30)
|
||||
assert update == False
|
||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||
|
||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||
name, update = i.update_db(keys, False, 29)
|
||||
_, update = i.update_db(keys, False, 29)
|
||||
assert update == True
|
||||
assert 29 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, json
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.gen3.infos_g3 import InfosG3
|
||||
import pytest, json, math
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
|
||||
|
||||
@pytest.fixture
|
||||
def ContrDataSeq(): # Get Time Request message
|
||||
def contr_data_seq(): # Get Time Request message
|
||||
msg = b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
||||
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
|
||||
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
|
||||
@@ -14,7 +14,7 @@ def ContrDataSeq(): # Get Time Request message
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def Contr2DataSeq(): # Get Time Request message
|
||||
def contr2_data_seq(): # Get Time Request message
|
||||
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52'
|
||||
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x32\x30\x00'
|
||||
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54'
|
||||
@@ -94,19 +94,19 @@ def Contr2DataSeq(): # Get Time Request message
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvDataSeq(): # Data indication from the controller
|
||||
def inv_data_seq(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvalidDataSeq(): # Data indication from the controller
|
||||
def invalid_data_seq(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x64\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvDataSeq2(): # Data indication from the controller
|
||||
def inv_data_seq2(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||
@@ -141,7 +141,83 @@ def InvDataSeq2(): # Data indication from the controller
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvDataSeq2_Zero(): # Data indication from the controller
|
||||
def inv_data_new(): # Data indication from DSP V5.0.17
|
||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
||||
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\x91\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01'
|
||||
msg += b'\x95\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\x80\x53\x00\x00\x00\x00\x01\x90\x41\x00\x00'
|
||||
msg += b'\x01\x94\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x96'
|
||||
msg += b'\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\xa0\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\xf0\x41\x00\x00\x01\xf1\x53\x00\x00\x00\x00\x01'
|
||||
msg += b'\xf4\x53\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf8\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00'
|
||||
msg += b'\x00\x00\x00\x01\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00\x00\x00'
|
||||
msg += b'\x00\x01\x53\x00\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x00\x58'
|
||||
msg += b'\x41\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x41\x00\x00\x02\x59\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00\x02\x44'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x45\x53\x00\x00\x00\x00\x02\x60\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x60\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x20\x41\x00\x00\x02\x24\x53\x00\x00\x00\x00\x02\x24\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\x26\x53\x00\x00\x00\x00\x02\x40\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x80\x41\x00\x00\x02\x81'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x84\x53\x00\x00\x00\x00\x02\x85\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x80\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\xc8\x42\x00\x00\x00\x00\x48\x42\x00\x00\x00\x00\x80'
|
||||
msg += b'\x42\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x01\x20\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\x84\x53\x00\x10\x00\x00\x02\x40\x46\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x04\x04\x46\x02\x00\x46\x02\x00\x00\x04\x00\x46\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x05\x04\x42\x00\x00\x00\x05\x50\x42\x00\x00\x00'
|
||||
msg += b'\x00\x14\x42\x00\x00\x00\x00\x00\x46\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\xa4\x46\x00\x00\x00\x00\x00\x00\x01\x00\x46\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x01\x44\x46\x00\x00\x00\x00\x00\x00\x02\x00\x46\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00\x08\x90\x46\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x08\x54\x46\x00\x00\x00\x00\x00\x00\x09\x20'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x08\x00\x46\x00\x00\x00\x00\x00\x00\x08\x84\x46\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x08\x40\x46\x00\x00\x00\x00\x00\x00\x09\x04\x46\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x0a\x10\x46\x00\x00\x00\x00\x00\x00\x0c\x14\x46'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x00\x00\x00\x00\x00\x00\x0c'
|
||||
msg += b'\x24\x42\x00\x00\x00\x0d\x00\x42\x00\x00\x00\x00\x04\x42\x00\x00'
|
||||
msg += b'\x00\x00\x00\x42\x00\x00\x00\x00\x44\x42\x00\x00\x00\x00\x10\x42'
|
||||
msg += b'\x00\x00\x00\x01\x14\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00'
|
||||
msg += b'\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x40\x53\x00\x00\x00\x00\x10'
|
||||
msg += b'\x04\x53\x00\x00\x00\x00\x11\x00\x53\x00\x00\x00\x00\x11\x84\x53'
|
||||
msg += b'\x00\x00\x00\x00\x10\x50\x53\xff\xff\x00\x00\x10\x14\x53\x03\x20'
|
||||
msg += b'\x00\x00\x10\x00\x53\x00\x00\x00\x00\x11\x24\x53\x00\x00\x00\x00'
|
||||
msg += b'\x03\x00\x53\x00\x00\x00\x00\x03\x64\x53\x00\x00\x00\x00\x04\x50'
|
||||
msg += b'\x53\x00\x00\x00\x00\x00\x34\x53\x00\x00\x00\x00\x00\x00\x42\x02'
|
||||
msg += b'\x00\x00\x01\x04\x42\x00\x00\x00\x21\x00\x42\x00\x00\x00\x21\x44'
|
||||
msg += b'\x42\x00\x00\x00\x22\x10\x53\x00\x00\x00\x00\x28\x14\x42\x01\x00'
|
||||
msg += b'\x00\x28\xa0\x46\x42\x48\x00\x00\x00\x00\x29\x04\x42\x00\x00\x00'
|
||||
msg += b'\x29\x40\x42\x00\x00\x00\x28\x04\x46\x42\x10\x00\x00\x00\x00\x28'
|
||||
msg += b'\x00\x42\x00\x00\x00\x28\x84\x42\x00\x00\x00\x28\x50\x42\x00\x00'
|
||||
msg += b'\x00\x29\x14\x42\x00\x00\x00\x2a\x00\x42\x00\x00\x00\x2c\x24\x46'
|
||||
msg += b'\x42\x10\x00\x00\x00\x00\x2c\x80\x42\x00\x00\x00\x2c\x44\x53\x00'
|
||||
msg += b'\x02\x00\x00\x2d\x00\x42\x00\x00\x00\x20\x04\x46\x42\x4d\x00\x00'
|
||||
msg += b'\x00\x00\x20\x10\x42\x00\x00\x00\x20\x54\x42\x00\x00\x00\x20\x20'
|
||||
msg += b'\x42\x00\x00\x00\x21\x04\x53\x00\x01\x00\x00\x22\x00\x42\x00\x00'
|
||||
msg += b'\x00\x30\x04\x42\x00\x00\x00\x30\x40\x53\x00\x00\x00\x00\x30\x04'
|
||||
msg += b'\x53\x00\x00\x00\x00\x31\x10\x42\x00\x00\x00\x31\x94\x53\x00\x04'
|
||||
msg += b'\x00\x00\x30\x00\x53\x00\x00\x00\x00\x30\x24\x53\x00\x00\x00\x00'
|
||||
msg += b'\x30\x00\x53\x00\x00\x00\x00\x31\x04\x53\x00\x00\x00\x00\x31\x80'
|
||||
msg += b'\x53\x00\x00\x00\x00\x32\x44\x53\x00\x00\x00\x00\x30\x00\x53\x00'
|
||||
msg += b'\x00\x00\x00\x30\x80\x53\x00\x00\x00\x00\x30\x00\x53\x00\x00\x00'
|
||||
msg += b'\x00\x30\x80\x53\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03\x00'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inv_data_seq2_zero(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||
@@ -176,37 +252,37 @@ def InvDataSeq2_Zero(): # Data indication from the controller
|
||||
return msg
|
||||
|
||||
|
||||
def test_parse_control(ContrDataSeq):
|
||||
def test_parse_control(contr_data_seq):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (ContrDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (contr_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||
|
||||
def test_parse_control2(Contr2DataSeq):
|
||||
def test_parse_control2(contr2_data_seq):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (Contr2DataSeq):
|
||||
pass
|
||||
for key, result in i.parse (contr2_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||
|
||||
def test_parse_inverter(InvDataSeq):
|
||||
def test_parse_inverter(inv_data_seq):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (InvDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (inv_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||
|
||||
def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
||||
def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (ContrDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (contr_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
for key, result in i.parse (InvDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (inv_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{
|
||||
@@ -214,7 +290,7 @@ def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
||||
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||
|
||||
|
||||
def test_build_ha_conf1(ContrDataSeq):
|
||||
def test_build_ha_conf1(contr_data_seq):
|
||||
i = InfosG3()
|
||||
i.static_init() # initialize counter
|
||||
|
||||
@@ -249,7 +325,11 @@ def test_build_ha_conf1(ContrDataSeq):
|
||||
|
||||
assert tests==4
|
||||
|
||||
def test_build_ha_conf2(contr_data_seq):
|
||||
i = InfosG3()
|
||||
i.static_init() # initialize counter
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
@@ -268,28 +348,28 @@ def test_build_ha_conf1(ContrDataSeq):
|
||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
assert tests==5
|
||||
assert tests==1
|
||||
|
||||
def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
||||
def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (ContrDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (InvDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (InvDataSeq2):
|
||||
pass
|
||||
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()
|
||||
for key, result in i.parse (inv_data_seq2):
|
||||
pass # side effect in calling i.parse()
|
||||
|
||||
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 == 'out_power_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
if id == 'daily_gen_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter - roof", "sa": "Micro Inverter - roof", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "sn": "T170000000000001", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv1_123':
|
||||
@@ -308,50 +388,72 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
||||
tests +=1
|
||||
assert tests==5
|
||||
|
||||
def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
||||
def test_build_ha_conf4(contr_data_seq, inv_data_seq):
|
||||
i = InfosG3()
|
||||
for key, result in i.parse (contr_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
for key, result in i.parse (inv_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||
if id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:56"]]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
assert tests==1
|
||||
|
||||
i.set_db_def_value(Register.MAC_ADDR, "00:a0:57:12:34:57")
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||
if id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:57"]]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
assert tests==1
|
||||
|
||||
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||
i = InfosG3()
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2):
|
||||
for key, update in i.parse (inv_data_seq2):
|
||||
if key == 'total' or key == 'inverter' or key == 'env':
|
||||
assert update == True
|
||||
tests +=1
|
||||
assert tests==5
|
||||
assert tests==8
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2):
|
||||
if key == 'total':
|
||||
for key, update in i.parse (inv_data_seq2):
|
||||
if key == 'total' or key == 'env':
|
||||
assert update == False
|
||||
tests +=1
|
||||
elif key == 'env':
|
||||
assert update == False
|
||||
tests +=1
|
||||
|
||||
assert tests==3
|
||||
assert tests==4
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "Max_Designed_Power": -1, "Output_Coefficient": 100.0, "No_Inputs": 2})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2_Zero):
|
||||
for key, update in i.parse (inv_data_seq2_zero):
|
||||
if key == 'total':
|
||||
assert update == False
|
||||
tests +=1
|
||||
elif key == 'env':
|
||||
assert update == True
|
||||
tests +=1
|
||||
|
||||
assert tests==3
|
||||
assert tests==4
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
||||
|
||||
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
||||
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
||||
i = InfosG3()
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2_Zero):
|
||||
for key, update in i.parse (inv_data_seq2_zero):
|
||||
if key == 'total':
|
||||
assert update == False
|
||||
tests +=1
|
||||
@@ -359,40 +461,52 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
||||
assert update == True
|
||||
tests +=1
|
||||
|
||||
assert tests==3
|
||||
assert tests==4
|
||||
assert json.dumps(i.db['total']) == json.dumps({})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2_Zero):
|
||||
if key == 'total':
|
||||
for key, update in i.parse (inv_data_seq2_zero):
|
||||
if key == 'total' or key == 'env':
|
||||
assert update == False
|
||||
tests +=1
|
||||
elif key == 'env':
|
||||
assert update == False
|
||||
tests +=1
|
||||
|
||||
assert tests==3
|
||||
assert tests==4
|
||||
assert json.dumps(i.db['total']) == json.dumps({})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2):
|
||||
if key == 'total':
|
||||
assert update == True
|
||||
for key, update in i.parse (inv_data_seq2):
|
||||
if key == 'total' or key == 'env':
|
||||
tests +=1
|
||||
elif key == 'env':
|
||||
assert update == True
|
||||
tests +=1
|
||||
|
||||
assert tests==3
|
||||
assert tests==4
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 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}})
|
||||
|
||||
def test_new_data_types(inv_data_new):
|
||||
i = InfosG3()
|
||||
tests = 0
|
||||
for key, update in i.parse (inv_data_new):
|
||||
if key == 'events':
|
||||
tests +=1
|
||||
elif key == 'inverter':
|
||||
assert update == True
|
||||
tests +=1
|
||||
elif key == 'input':
|
||||
assert update == False
|
||||
tests +=1
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_invalid_data_type(InvalidDataSeq):
|
||||
assert tests==15
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
||||
|
||||
def test_invalid_data_type(invalid_data_seq):
|
||||
i = InfosG3()
|
||||
i.static_init() # initialize counter
|
||||
|
||||
@@ -400,9 +514,21 @@ def test_invalid_data_type(InvalidDataSeq):
|
||||
assert val == 0
|
||||
|
||||
|
||||
for key, result in i.parse (InvalidDataSeq):
|
||||
pass
|
||||
for key, result in i.parse (invalid_data_seq):
|
||||
pass # side effect in calling i.parse()
|
||||
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
||||
|
||||
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||
assert val == 1
|
||||
|
||||
def test_result_eval(inv_data_seq2: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
|
||||
|
||||
i = InfosG3()
|
||||
|
||||
for _, _ in i.parse (inv_data_seq2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
del RegisterMap.map[0x00000514]['eval'] # remove eval
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
|
||||
# test_with_pytest.py
|
||||
import pytest, json
|
||||
import pytest, json, math, random
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.gen3plus.infos_g3p import RegisterMap
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def str_test_ip():
|
||||
ip = ".".join(str(random.randint(1, 254)) for _ in range(4))
|
||||
print(f'random_ip: {ip}')
|
||||
return ip
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def bytes_test_ip(str_test_ip):
|
||||
ip = bytes(str.encode(str_test_ip))
|
||||
l = len(ip)
|
||||
if l < 16:
|
||||
ip = ip + bytearray(16-l)
|
||||
print(f'random_ip: {ip}')
|
||||
return ip
|
||||
|
||||
@pytest.fixture
|
||||
def DeviceData(): # 0x4110 ftype: 0x02
|
||||
def device_data(bytes_test_ip): # 0x4110 ftype: 0x02
|
||||
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54' + bytes_test_ip
|
||||
msg += b'\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'
|
||||
@@ -24,7 +39,7 @@ def DeviceData(): # 0x4110 ftype: 0x02
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InverterData(): # 0x4210 ftype: 0x01
|
||||
def inverter_data(): # 0x4210 ftype: 0x01
|
||||
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xb0\x02\xbc\xc8'
|
||||
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||
msg += b'\x59\x31\x37\x45\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x45'
|
||||
@@ -56,34 +71,34 @@ def InverterData(): # 0x4210 ftype: 0x01
|
||||
|
||||
|
||||
def test_default_db():
|
||||
i = InfosG3P()
|
||||
i = InfosG3P(client_mode=False)
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00"},
|
||||
"inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 4},
|
||||
"collector": {"Chip_Type": "IGEN TECH"},
|
||||
})
|
||||
|
||||
def test_parse_4110(DeviceData: bytes):
|
||||
i = InfosG3P()
|
||||
def test_parse_4110(str_test_ip, device_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
for key, update in i.parse (DeviceData, 0x41, 2):
|
||||
pass
|
||||
for key, update in i.parse (device_data, 0x41, 2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49"},
|
||||
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||
'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", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||
})
|
||||
|
||||
def test_parse_4210(InverterData: bytes):
|
||||
i = InfosG3P()
|
||||
def test_parse_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (InverterData, 0x42, 1):
|
||||
pass
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"controller": {"Power_On_Time": 2051},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4},
|
||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
||||
@@ -94,7 +109,7 @@ def test_parse_4210(InverterData: bytes):
|
||||
})
|
||||
|
||||
def test_build_ha_conf1():
|
||||
i = InfosG3P()
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.static_init() # initialize counter
|
||||
|
||||
tests = 0
|
||||
@@ -116,8 +131,19 @@ def test_build_ha_conf1():
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv2_123':
|
||||
assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!!
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv3_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv3_123", "val_tpl": "{{ (value_json['pv3']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV3", "sa": "Module PV3", "via_device": "inverter_123", "ids": ["input_pv3_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv4_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv4_123", "val_tpl": "{{ (value_json['pv4']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV4", "sa": "Module PV4", "via_device": "inverter_123", "ids": ["input_pv4_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
@@ -126,9 +152,13 @@ def test_build_ha_conf1():
|
||||
elif id == 'inv_count_456':
|
||||
assert False
|
||||
|
||||
assert tests==4
|
||||
assert tests==7
|
||||
|
||||
def test_build_ha_conf2():
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.static_init() # initialize counter
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
@@ -138,8 +168,11 @@ def test_build_ha_conf1():
|
||||
elif id == 'power_pv1_123':
|
||||
assert False
|
||||
elif id == 'power_pv2_123':
|
||||
assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!!
|
||||
|
||||
assert False
|
||||
elif id == 'power_pv3_123':
|
||||
assert False
|
||||
elif id == 'power_pv4_123':
|
||||
assert False
|
||||
elif id == 'signal_123':
|
||||
assert False
|
||||
elif id == 'inv_count_456':
|
||||
@@ -147,30 +180,103 @@ def test_build_ha_conf1():
|
||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
assert tests==5
|
||||
assert tests==1
|
||||
|
||||
def test_exception_and_eval(InverterData: bytes):
|
||||
def test_build_ha_conf3():
|
||||
i = InfosG3P(client_mode=True)
|
||||
i.static_init() # initialize counter
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "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", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'daily_gen_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv1_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv2_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv3_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv3_123", "val_tpl": "{{ (value_json['pv3']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV3", "sa": "Module PV3", "via_device": "inverter_123", "ids": ["input_pv3_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv4_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv4_123", "val_tpl": "{{ (value_json['pv4']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV4", "sa": "Module PV4", "via_device": "inverter_123", "ids": ["input_pv4_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({})
|
||||
tests +=1
|
||||
elif id == 'inv_count_456':
|
||||
assert False
|
||||
|
||||
assert tests==7
|
||||
|
||||
def test_build_ha_conf4():
|
||||
i = InfosG3P(client_mode=True)
|
||||
i.static_init() # initialize counter
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
assert False
|
||||
elif id == 'daily_gen_123':
|
||||
assert False
|
||||
elif id == 'power_pv1_123':
|
||||
assert False
|
||||
elif id == 'power_pv2_123':
|
||||
assert False
|
||||
elif id == 'power_pv3_123':
|
||||
assert False
|
||||
elif id == 'power_pv4_123':
|
||||
assert False
|
||||
elif id == 'signal_123':
|
||||
assert False
|
||||
elif id == 'inv_count_456':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
assert tests==1
|
||||
|
||||
def test_exception_and_eval(inverter_data: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
||||
# map PV1_VOLTAGE to invalid register
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
|
||||
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
||||
Backup = RegisterMap.map[0x420100de]
|
||||
backup = RegisterMap.map[0x420100de]
|
||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
||||
|
||||
i = InfosG3P()
|
||||
i = InfosG3P(client_mode=False)
|
||||
# i.db.clear()
|
||||
|
||||
for key, update in i.parse (InverterData, 0x42, 1):
|
||||
pass
|
||||
assert 12.2222 == round (i.get_db_value(Register.INVERTER_TEMP, 0),4)
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
del RegisterMap.map[0x420100d8]['eval'] # remove eval
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = Backup # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
|
||||
for key, update in i.parse (InverterData, 0x42, 1):
|
||||
pass
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
304
app/tests/test_inverter_base.py
Normal file
304
app/tests/test_inverter_base.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
import gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.talent import Talent
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.async_stream import AsyncStream, AsyncStreamClient
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_healthy():
|
||||
with patch.object(AsyncStream, 'healthy') as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_unhealthy():
|
||||
def new_healthy(self):
|
||||
return False
|
||||
with patch.object(AsyncStream, 'healthy', new_healthy) as conn:
|
||||
yield conn
|
||||
@pytest.fixture
|
||||
def patch_unhealthy_remote():
|
||||
def new_healthy(self):
|
||||
return False
|
||||
with patch.object(AsyncStreamClient, 'healthy', new_healthy) as conn:
|
||||
yield conn
|
||||
|
||||
def test_inverter_iter():
|
||||
InverterBase._registry.clear()
|
||||
cnt = 0
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
for inv in InverterBase:
|
||||
assert inv == inverter
|
||||
cnt += 1
|
||||
del inv
|
||||
del inverter
|
||||
assert cnt == 1
|
||||
|
||||
for inv in InverterBase:
|
||||
assert False
|
||||
|
||||
def test_method_calls(patch_healthy):
|
||||
spy = patch_healthy
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
# call healthy inside the contexter manager
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy()
|
||||
del inv
|
||||
spy.assert_called_once()
|
||||
|
||||
# outside context manager the health function of AsyncStream is not reachable
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy()
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
spy.assert_called_once() # counter don't increase and keep one!
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
def test_unhealthy(patch_unhealthy):
|
||||
_ = patch_unhealthy
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
# call healthy inside the contexter manager
|
||||
assert not inverter.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
def test_unhealthy_remote(patch_unhealthy_remote):
|
||||
_ = patch_unhealthy
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
# call healthy inside the contexter manager
|
||||
assert not inverter.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
# call healthy inside the contexter manager
|
||||
assert inverter.healthy()
|
||||
|
||||
# call healthy outside the contexter manager (__exit__() was called)
|
||||
assert inverter.healthy()
|
||||
del inverter
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
_ = patch_unhealthy_remote
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
assert not inverter.remote.ifc.healthy()
|
||||
# call healthy inside the contexter manager
|
||||
assert not inverter.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_disc(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream
|
||||
# call disc inside the contexter manager
|
||||
await inverter.disc()
|
||||
|
||||
# call disc outside the contexter manager (__exit__() was called)
|
||||
await inverter.disc()
|
||||
del inverter
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
226
app/tests/test_inverter_g3.py
Normal file
226
app/tests/test_inverter_g3.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys,gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.gen3.inverter_g3 import InverterG3
|
||||
from app.src.async_stream import AsyncStream
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@pytest.fixture
|
||||
def patch_healthy():
|
||||
with patch.object(AsyncStream, 'healthy') as conn:
|
||||
yield conn
|
||||
|
||||
def test_method_calls(patch_healthy):
|
||||
spy = patch_healthy
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
InverterBase._registry.clear()
|
||||
|
||||
with InverterG3(reader, writer) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
for inv in InverterBase:
|
||||
inv.healthy()
|
||||
del inv
|
||||
spy.assert_called_once()
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream
|
||||
del inverter
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_except(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
del inverter
|
||||
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_publish(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
stream = inverter.local.stream
|
||||
await inverter.async_publ_mqtt() # check call with invalid unique_id
|
||||
stream._Talent__set_serial_no(serial_no= "123344")
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == False
|
||||
|
||||
stream.new_data['env'] = True
|
||||
stream.db.db['env'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['env'] == False
|
||||
|
||||
Infos.new_stat_data['proxy'] = True
|
||||
await inverter.async_publ_mqtt()
|
||||
assert Infos.new_stat_data['proxy'] == False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
_ = patch_mqtt_err
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._Talent__set_serial_no(serial_no= "123344")
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
_ = patch_mqtt_except
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._Talent__set_serial_no(serial_no= "123344")
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == True
|
||||
196
app/tests/test_inverter_g3p.py
Normal file
196
app/tests/test_inverter_g3p.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
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
|
||||
|
||||
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():
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
InverterBase._registry.clear()
|
||||
|
||||
with InverterG3P(reader, writer, client_mode=False) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_except(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_publish(config_conn, patch_open_connection):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
await inverter.async_publ_mqtt() # check call with invalid unique_id
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == False
|
||||
|
||||
stream.new_data['env'] = True
|
||||
stream.db.db['env'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['env'] == False
|
||||
|
||||
Infos.new_stat_data['proxy'] = True
|
||||
await inverter.async_publ_mqtt()
|
||||
assert Infos.new_stat_data['proxy'] == False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
_ = patch_mqtt_err
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except):
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
_ = patch_mqtt_except
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
Proxy.class_init()
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
assert stream.new_data['inverter'] == True
|
||||
@@ -5,9 +5,8 @@ from app.src.modbus import Modbus
|
||||
from app.src.infos import Infos, Register
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
pytestmark = pytest.mark.asyncio(scope="module")
|
||||
|
||||
class TestHelper(Modbus):
|
||||
class ModbusTestHelper(Modbus):
|
||||
def __init__(self):
|
||||
super().__init__(self.send_cb)
|
||||
self.db = Infos()
|
||||
@@ -32,10 +31,15 @@ def test_modbus_crc():
|
||||
assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8')
|
||||
|
||||
assert 0x5c75 == mb._Modbus__calc_crc(b'\x01\x03\x08\x01\x2c\x00\x2c\x02\x2c\x2c\x46')
|
||||
|
||||
msg = b'\x01\x03\x28\x51'
|
||||
msg += b'\x0e\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00'
|
||||
msg += b'\x08\x00\x00\x00\x00\x01\x59\x01\x21\x03\xe6\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
|
||||
assert 0 == mb._Modbus__calc_crc(msg)
|
||||
|
||||
def test_build_modbus_pdu():
|
||||
'''Check building and sending a MODBUS RTU'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,6,0x2000,0x12)
|
||||
assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07'
|
||||
assert mb._Modbus__check_crc(mb.pdu)
|
||||
@@ -47,7 +51,7 @@ def test_build_modbus_pdu():
|
||||
|
||||
def test_recv_req():
|
||||
'''Receive a valid request, which must transmitted'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07')
|
||||
assert mb.last_fcode == 6
|
||||
assert mb.last_reg == 0x2000
|
||||
@@ -56,7 +60,7 @@ def test_recv_req():
|
||||
|
||||
def test_recv_req_crc_err():
|
||||
'''Receive a request with invalid CRC, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert not mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08')
|
||||
assert mb.pdu == None
|
||||
assert mb.last_fcode == 0
|
||||
@@ -66,13 +70,13 @@ def test_recv_req_crc_err():
|
||||
|
||||
def test_recv_resp_crc_err():
|
||||
'''Receive a response with invalid CRC, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
# simulate a transmitted request
|
||||
mb.req_pend = True
|
||||
mb.last_addr = 1
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg == 0x300e
|
||||
mb.last_len == 2
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
# check matching response, but with CRC error
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
|
||||
@@ -86,13 +90,13 @@ def test_recv_resp_crc_err():
|
||||
|
||||
def test_recv_resp_invalid_addr():
|
||||
'''Receive a response with wrong server addr, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.req_pend = True
|
||||
# simulate a transmitted request
|
||||
mb.last_addr = 1
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg == 0x300e
|
||||
mb.last_len == 2
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
|
||||
# check not matching response, with wrong server addr
|
||||
call = 0
|
||||
@@ -109,7 +113,7 @@ def test_recv_resp_invalid_addr():
|
||||
|
||||
def test_recv_recv_fcode():
|
||||
'''Receive a response with wrong function code, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,4,0x300e,2)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -130,7 +134,7 @@ def test_recv_recv_fcode():
|
||||
|
||||
def test_recv_resp_len():
|
||||
'''Receive a response with wrong data length, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x300e,3)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -152,7 +156,7 @@ def test_recv_resp_len():
|
||||
|
||||
def test_recv_unexpect_resp():
|
||||
'''Receive a response when we havb't sent a request'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert not mb.req_pend
|
||||
|
||||
# check unexpected response, which must be dropped
|
||||
@@ -167,13 +171,13 @@ def test_recv_unexpect_resp():
|
||||
|
||||
def test_parse_resp():
|
||||
'''Receive matching response and parse the values'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
|
||||
call = 0
|
||||
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
@@ -191,7 +195,7 @@ def test_parse_resp():
|
||||
assert not mb.req_pend
|
||||
|
||||
def test_queue():
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3022,4)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -210,7 +214,7 @@ def test_queue():
|
||||
|
||||
def test_queue2():
|
||||
'''Check queue handling for build_msg() calls'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
mb.build_msg(1,6,0x2008,4)
|
||||
assert mb.que.qsize() == 1
|
||||
@@ -222,7 +226,7 @@ def test_queue2():
|
||||
assert mb.send_calls == 1
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
@@ -242,7 +246,7 @@ def test_queue2():
|
||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||
pass
|
||||
pass # call generator mb.recv_resp()
|
||||
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.send_calls == 3
|
||||
@@ -258,7 +262,7 @@ def test_queue2():
|
||||
|
||||
def test_queue3():
|
||||
'''Check queue handling for recv_req() calls'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert mb.recv_req(b'\x01\x03\x30\x07\x00\x06{\t', mb.resp_handler)
|
||||
assert mb.recv_req(b'\x01\x06\x20\x08\x00\x04\x02\x0b', mb.resp_handler)
|
||||
assert mb.que.qsize() == 1
|
||||
@@ -272,7 +276,7 @@ def test_queue3():
|
||||
assert mb.recv_responses == 0
|
||||
|
||||
call = 0
|
||||
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
@@ -293,7 +297,7 @@ def test_queue3():
|
||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||
pass
|
||||
pass # no code in loop is OK; calling the generator is the purpose
|
||||
assert 0 == mb.err
|
||||
assert mb.recv_responses == 2
|
||||
|
||||
@@ -315,7 +319,7 @@ def test_queue3():
|
||||
async def test_timeout():
|
||||
'''Test MODBUS response timeout and RTU retransmitting'''
|
||||
assert asyncio.get_running_loop()
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.max_retries = 2
|
||||
mb.timeout = 0.1 # 100ms timeout for fast testing, expect a time resolution of at least 10ms
|
||||
assert asyncio.get_running_loop() == mb.loop
|
||||
@@ -359,13 +363,11 @@ async def test_timeout():
|
||||
assert mb.retry_cnt == 0
|
||||
assert mb.send_calls == 4
|
||||
|
||||
# assert mb.counter == {}
|
||||
|
||||
def test_recv_unknown_data():
|
||||
'''Receive a response with an unknwon register'''
|
||||
mb = TestHelper()
|
||||
assert 0x9000 not in mb.map
|
||||
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
||||
mb = ModbusTestHelper()
|
||||
assert 0x9000 not in mb.mb_reg_mapping
|
||||
mb.mb_reg_mapping[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
||||
|
||||
mb.build_msg(1,3,0x9000,2)
|
||||
|
||||
@@ -377,4 +379,17 @@ def test_recv_unknown_data():
|
||||
assert 0 == call
|
||||
assert not mb.req_pend
|
||||
|
||||
del mb.map[0x9000]
|
||||
del mb.mb_reg_mapping[0x9000]
|
||||
|
||||
def test_close():
|
||||
'''Check queue handling for build_msg() calls'''
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
mb.build_msg(1,6,0x2008,4)
|
||||
assert mb.que.qsize() == 1
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
assert mb.que.qsize() == 2
|
||||
assert mb.que.empty() == False
|
||||
mb.close()
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.que.empty() == True
|
||||
|
||||
381
app/tests/test_modbus_tcp.py
Normal file
381
app/tests/test_modbus_tcp.py
Normal file
@@ -0,0 +1,381 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from aiomqtt import MqttCodeError
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.messages import Message, State
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.modbus_tcp import ModbusConn, ModbusTcp
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
# initialize the proxy statistics
|
||||
Infos.static_init()
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def module_init():
|
||||
Singleton._instances.clear()
|
||||
yield
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_port():
|
||||
return 1883
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_hostname():
|
||||
# if getenv("GITHUB_ACTIONS") == "true":
|
||||
# return 'mqtt'
|
||||
# else:
|
||||
return 'test.mosquitto.org'
|
||||
|
||||
@pytest.fixture
|
||||
def config_conn(test_hostname, test_port):
|
||||
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': ''
|
||||
},
|
||||
'inverters':{
|
||||
'allow_all': True,
|
||||
"R170000000000001":{
|
||||
'node_id': 'inv_1'
|
||||
},
|
||||
"Y170000000000001":{
|
||||
'node_id': 'inv_2',
|
||||
'monitor_sn': 2000000000,
|
||||
'modbus_polling': True,
|
||||
'suggested_area': "",
|
||||
'sensor_list': 0x2b0,
|
||||
'client_mode':{
|
||||
'host': '192.168.0.1',
|
||||
'port': 8899
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FakeReader():
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_13_BYTES = 3
|
||||
RD_TEST_SW_EXCEPT = 4
|
||||
RD_TEST_OS_ERROR = 5
|
||||
|
||||
def __init__(self):
|
||||
self.on_recv = asyncio.Event()
|
||||
self.test = self.RD_TEST_0_BYTES
|
||||
|
||||
async def read(self, max_len: int):
|
||||
print(f'fakeReader test: {self.test}')
|
||||
await self.on_recv.wait()
|
||||
if self.test == self.RD_TEST_0_BYTES:
|
||||
return b''
|
||||
elif self.test == self.RD_TEST_13_BYTES:
|
||||
print('fakeReader return 13 bytes')
|
||||
self.test = self.RD_TEST_0_BYTES
|
||||
return b'test-data-req'
|
||||
elif self.test == self.RD_TEST_TIMEOUT:
|
||||
raise TimeoutError
|
||||
elif self.test == self.RD_TEST_SW_EXCEPT:
|
||||
self.test = self.RD_TEST_0_BYTES
|
||||
self.unknown_var += 1
|
||||
elif self.test == self.RD_TEST_OS_ERROR:
|
||||
self.test = self.RD_TEST_0_BYTES
|
||||
raise ConnectionRefusedError
|
||||
|
||||
def feed_eof(self):
|
||||
return
|
||||
|
||||
|
||||
class FakeWriter():
|
||||
def __init__(self, conn='remote.intern'):
|
||||
self.conn = conn
|
||||
self.closing = False
|
||||
def write(self, buf: bytes):
|
||||
return
|
||||
async def drain(self):
|
||||
await asyncio.sleep(0)
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return self.conn
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
def is_closing(self):
|
||||
return self.closing
|
||||
def close(self):
|
||||
self.closing = True
|
||||
async def wait_closed(self):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open():
|
||||
async def new_conn(conn):
|
||||
await asyncio.sleep(0)
|
||||
return FakeReader(), FakeWriter(conn)
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
return new_conn(f'{host}:{port}')
|
||||
|
||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_timeout():
|
||||
def new_open(host: str, port: int):
|
||||
raise TimeoutError
|
||||
|
||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_value_error():
|
||||
def new_open(host: str, port: int):
|
||||
raise ValueError
|
||||
|
||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_conn_abort():
|
||||
def new_open(host: str, port: int):
|
||||
raise ConnectionAbortedError
|
||||
|
||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_no_mqtt():
|
||||
with patch.object(Mqtt, 'publish') as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_mqtt_err():
|
||||
def new_publish(self, key, data):
|
||||
raise MqttCodeError(None)
|
||||
|
||||
with patch.object(Mqtt, 'publish', new_publish) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def patch_mqtt_except():
|
||||
def new_publish(self, key, data):
|
||||
raise ValueError("Test")
|
||||
|
||||
with patch.object(Mqtt, 'publish', new_publish) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_conn(patch_open):
|
||||
_ = patch_open
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
async with ModbusConn('test.local', 1234) as inverter:
|
||||
stream = inverter.local.stream
|
||||
assert stream.node_id == 'G3P'
|
||||
assert stream.addr == ('test.local:1234')
|
||||
assert type(stream.ifc._reader) is FakeReader
|
||||
assert type(stream.ifc._writer) is FakeWriter
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
|
||||
del inverter
|
||||
|
||||
for _ in InverterBase:
|
||||
assert False
|
||||
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_no_cnf():
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
loop = asyncio.get_event_loop()
|
||||
ModbusTcp(loop)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_timeout(config_conn, patch_open_timeout):
|
||||
_ = config_conn
|
||||
_ = patch_open_timeout
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
loop = asyncio.get_event_loop()
|
||||
ModbusTcp(loop)
|
||||
await asyncio.sleep(0.01)
|
||||
for m in Message:
|
||||
if (m.node_id == 'inv_2'):
|
||||
assert False
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_value_err(config_conn, patch_open_value_error):
|
||||
_ = config_conn
|
||||
_ = patch_open_value_error
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
loop = asyncio.get_event_loop()
|
||||
ModbusTcp(loop)
|
||||
await asyncio.sleep(0.01)
|
||||
for m in Message:
|
||||
if (m.node_id == 'inv_2'):
|
||||
assert False
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_conn_abort(config_conn, patch_open_conn_abort):
|
||||
_ = config_conn
|
||||
_ = patch_open_conn_abort
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
loop = asyncio.get_event_loop()
|
||||
ModbusTcp(loop)
|
||||
await asyncio.sleep(0.01)
|
||||
for m in Message:
|
||||
if (m.node_id == 'inv_2'):
|
||||
assert False
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
|
||||
_ = config_conn
|
||||
_ = patch_open
|
||||
_ = patch_no_mqtt
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
ModbusTcp(asyncio.get_event_loop())
|
||||
await asyncio.sleep(0.01)
|
||||
test = 0
|
||||
for m in Message:
|
||||
if (m.node_id == 'inv_2'):
|
||||
test += 1
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
|
||||
m.shutdown_started = True
|
||||
m.ifc._reader.on_recv.set()
|
||||
del m
|
||||
|
||||
assert 1 == test
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
|
||||
_ = config_conn
|
||||
_ = patch_open
|
||||
_ = patch_no_mqtt
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
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.ifc._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.ifc._reader.on_recv.set()
|
||||
del m
|
||||
|
||||
assert 2 == test
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_err(config_conn, patch_mqtt_err, patch_open):
|
||||
_ = config_conn
|
||||
_ = patch_open
|
||||
_ = patch_mqtt_err
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
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.ifc._reader.on_recv.set()
|
||||
await asyncio.sleep(0.1)
|
||||
assert m.state == State.closed
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
m.shutdown_started = True
|
||||
m.ifc._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
|
||||
assert asyncio.get_running_loop()
|
||||
Proxy.class_init()
|
||||
|
||||
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.ifc._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.ifc._reader.on_recv.set()
|
||||
del m
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
||||
255
app/tests/test_mqtt.py
Normal file
255
app/tests/test_mqtt.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from app.src.async_stream import AsyncIfcImpl
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def module_init():
|
||||
Singleton._instances.clear()
|
||||
yield
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_port():
|
||||
return 1883
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_hostname():
|
||||
# if getenv("GITHUB_ACTIONS") == "true":
|
||||
# return 'mqtt'
|
||||
# else:
|
||||
return 'test.mosquitto.org'
|
||||
|
||||
@pytest.fixture
|
||||
def config_mqtt_conn(test_hostname, test_port):
|
||||
Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def config_no_conn(test_port):
|
||||
Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def spy_at_cmd():
|
||||
conn = SolarmanV5(('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
|
||||
conn.node_id = 'inv_2/'
|
||||
with patch.object(conn, 'send_at_cmd', wraps=conn.send_at_cmd) as wrapped_conn:
|
||||
yield wrapped_conn
|
||||
conn.close()
|
||||
|
||||
@pytest.fixture
|
||||
def spy_modbus_cmd():
|
||||
conn = SolarmanV5(('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
|
||||
conn.node_id = 'inv_1/'
|
||||
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
|
||||
yield wrapped_conn
|
||||
conn.close()
|
||||
|
||||
@pytest.fixture
|
||||
def spy_modbus_cmd_client():
|
||||
conn = SolarmanV5(('test.local', 1234), server_side=False, client_mode= False, ifc=AsyncIfcImpl())
|
||||
conn.node_id = 'inv_1/'
|
||||
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
|
||||
yield wrapped_conn
|
||||
conn.close()
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server.
|
||||
"""
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import threading
|
||||
|
||||
c = mqtt.Client()
|
||||
c.loop_start()
|
||||
try:
|
||||
# Just make sure the client connects successfully
|
||||
on_connect = threading.Event()
|
||||
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
||||
c.connect_async(test_hostname, test_port)
|
||||
assert on_connect.wait(5)
|
||||
finally:
|
||||
c.loop_stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
await asyncio.sleep(0)
|
||||
assert not on_connect.is_set()
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert False
|
||||
except Exception:
|
||||
pass
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_modbus_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'100', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'50', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO)
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_modbus_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
# test out of range param
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'-1', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
# test unknown node_id
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_2/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
# test invalid fload param
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
||||
'''don't call function if connnection is not in server mode'''
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_modbus_cmd_client
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ignore_unknown_func(config_mqtt_conn):
|
||||
'''don't dispatch for unknwon function names'''
|
||||
_ = config_mqtt_conn
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
for _ in m.each_inverter(msg, 'unkown_fnc'):
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_at_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_2/at_cmd', payload= b'AT+', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with('AT+')
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
91
app/tests/test_proxy.py
Normal file
91
app/tests/test_proxy.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def module_init():
|
||||
def new_init(cls, cb_mqtt_is_up):
|
||||
pass # empty test methos
|
||||
|
||||
Singleton._instances.clear()
|
||||
with patch.object(Mqtt, '__init__', new_init):
|
||||
yield
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_port():
|
||||
return 1883
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_hostname():
|
||||
# if getenv("GITHUB_ACTIONS") == "true":
|
||||
# return 'mqtt'
|
||||
# else:
|
||||
return 'test.mosquitto.org'
|
||||
|
||||
@pytest.fixture
|
||||
def config_conn(test_hostname, test_port):
|
||||
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': ''
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': True,
|
||||
"R170000000000001":{
|
||||
'node_id': 'inv_1'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inverter_cb(config_conn):
|
||||
_ = config_conn
|
||||
|
||||
with patch.object(Proxy, '_cb_mqtt_is_up', wraps=Proxy._cb_mqtt_is_up) as spy:
|
||||
print('call Proxy.class_init')
|
||||
Proxy.class_init()
|
||||
assert 'homeassistant/' == Proxy.discovery_prfx
|
||||
assert 'tsun/' == Proxy.entity_prfx
|
||||
assert 'test_1/' == Proxy.proxy_node_id
|
||||
await Proxy._cb_mqtt_is_up()
|
||||
spy.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_is_up(config_conn):
|
||||
_ = config_conn
|
||||
|
||||
with patch.object(Mqtt, 'publish') as spy:
|
||||
Proxy.class_init()
|
||||
await Proxy._cb_mqtt_is_up()
|
||||
spy.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_proxy_statt_invalid(config_conn):
|
||||
_ = config_conn
|
||||
|
||||
with patch.object(Mqtt, 'publish') as spy:
|
||||
Proxy.class_init()
|
||||
await Proxy._async_publ_mqtt_proxy_stat('InValId_kEy')
|
||||
spy.assert_not_called()
|
||||
19
app/tests/test_singleton.py
Normal file
19
app/tests/test_singleton.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from app.src.singleton import Singleton
|
||||
|
||||
class Test(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass # is a dummy test class
|
||||
|
||||
def test_singleton_metaclass():
|
||||
Singleton._instances.clear()
|
||||
a = Test()
|
||||
assert 1 == len(Singleton._instances)
|
||||
b = Test()
|
||||
assert 1 == len(Singleton._instances)
|
||||
assert a is b
|
||||
del a
|
||||
assert 1 == len(Singleton._instances)
|
||||
del b
|
||||
assert 0 == len(Singleton._instances)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
||||
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
####### H O M E - A S S I S T A N T #####
|
||||
home-assistant:
|
||||
@@ -34,7 +31,7 @@ services:
|
||||
ports:
|
||||
- 8123:8123
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./homeassistant/config:/config
|
||||
- ${PROJECT_DIR:-./}homeassistant/config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
healthcheck:
|
||||
test: curl --fail http://0.0.0.0:8123/auth/providers || exit 1
|
||||
@@ -56,13 +53,12 @@ services:
|
||||
expose:
|
||||
- 1883
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./mosquitto/config:/mosquitto/config
|
||||
- ${PROJECT_DIR}./mosquitto/data:/mosquitto/data
|
||||
- ${PROJECT_DIR:-./}mosquitto/config:/mosquitto/config
|
||||
- ${PROJECT_DIR:-./}mosquitto/data:/mosquitto/data
|
||||
networks:
|
||||
outside:
|
||||
ipv4_address: 172.28.1.5 # static IP required to receive mDNS traffic
|
||||
|
||||
- outside
|
||||
|
||||
|
||||
|
||||
####### T S U N - P R O X Y ######
|
||||
tsun-proxy:
|
||||
@@ -78,13 +74,18 @@ services:
|
||||
- GID=${GID:-1000}
|
||||
dns:
|
||||
- ${DNS1:-8.8.8.8}
|
||||
- $(DNS2:-4.4.4.4}
|
||||
- ${DNS2:-4.4.4.4}
|
||||
ports:
|
||||
- 5005:5005
|
||||
- 8127:8127
|
||||
- 10000:10000
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./tsun-proxy/log:/home/tsun-proxy/log
|
||||
- ${PROJECT_DIR}./tsun-proxy/config:/home/tsun-proxy/config
|
||||
- ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log
|
||||
- ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:8127/-/healthy || exit 1
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
networks:
|
||||
- outside
|
||||
|
||||
@@ -94,11 +95,4 @@ services:
|
||||
networks:
|
||||
outside:
|
||||
name: home-assistant
|
||||
external: true
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.28.1.0/26
|
||||
ip_range: 172.28.1.32/27
|
||||
gateway: 172.28.1.62
|
||||
|
||||
1
requirements-test.txt
Normal file
1
requirements-test.txt
Normal file
@@ -0,0 +1 @@
|
||||
-r ./app/requirements-test.txt
|
||||
26
sonar-project.properties
Normal file
26
sonar-project.properties
Normal file
@@ -0,0 +1,26 @@
|
||||
sonar.projectKey=s-allius_tsun-gen3-proxy
|
||||
sonar.organization=s-allius
|
||||
|
||||
# This is the name and version displayed in the SonarCloud UI.
|
||||
sonar.projectName=tsun-gen3-proxy
|
||||
#sonar.projectVersion=1.0
|
||||
|
||||
|
||||
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
||||
sonar.sources=app/src/
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.python.version=3.12
|
||||
sonar.tests=system_tests/,app/tests/
|
||||
sonar.exclusions=**/.vscode/**/*
|
||||
# Name your criteria
|
||||
sonar.issue.ignore.multicriteria=e1,e2
|
||||
|
||||
# python:S905 : Remove or refactor this statement; it has no side effects
|
||||
sonar.issue.ignore.multicriteria.e1.ruleKey=python:S905
|
||||
sonar.issue.ignore.multicriteria.e1.resourceKey=app/tests/*.py
|
||||
|
||||
sonar.issue.ignore.multicriteria.e2.ruleKey=python:S905
|
||||
sonar.issue.ignore.multicriteria.e2.resourceKey=systems_tests/*.py
|
||||
@@ -1,8 +1,6 @@
|
||||
# test_with_pytest.py and scapy
|
||||
#
|
||||
import pytest, socket, time
|
||||
#from scapy.all import *
|
||||
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||
|
||||
def get_sn() -> bytes:
|
||||
return b'R170000000000001'
|
||||
@@ -15,31 +13,31 @@ def get_invalid_sn():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactInfo(): # Contact Info message
|
||||
def msg_contact_info(): # Contact Info message
|
||||
return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactResp(): # Contact Response message
|
||||
def msg_contact_resp(): # Contact Response message
|
||||
return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x91\x00\x01'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactInfo2(): # Contact Info message
|
||||
def msg_contact_info2(): # Contact Info message
|
||||
return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactResp2(): # Contact Response message
|
||||
def msg_contact_resp2(): # Contact Response message
|
||||
return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x91\x00\x01'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgTimeStampReq(): # Get Time Request message
|
||||
def msg_timestamp_req(): # Get Time Request message
|
||||
return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgTimeStampResp(): # Get Time Resonse message
|
||||
def msg_timestamp_resp(): # Get Time Resonse message
|
||||
return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContollerInd(): # Data indication from the controller
|
||||
def msg_controller_ind(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x01\x2f\x10'+ get_sn() + b'\x91\x71\x0e\x10\x00\x00\x10'+get_sn()
|
||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x55\x50'
|
||||
msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
||||
@@ -51,7 +49,7 @@ def MsgContollerInd(): # Data indication from the controller
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInvData(): # Data indication from the controller
|
||||
def msg_inv_data(): # Data indication from the controller
|
||||
msg = b'\x00\x00\x00\x8b\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
||||
msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||
@@ -59,7 +57,7 @@ def MsgInvData(): # Data indication from the controller
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInverterInd(): # Data indication from the inverter
|
||||
def msg_inverter_ind(): # Data indication from the inverter
|
||||
msg = b'\x00\x00\x05\x02\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
||||
msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||
@@ -96,7 +94,7 @@ def MsgInverterInd(): # Data indication from the inverter
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgOtaUpdateReq(): # Over the air update request from talent cloud
|
||||
def msg_ota_update_req(): # Over the air update request from talent cloud
|
||||
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
|
||||
msg += b'\x70\x68\x74\x74\x70'
|
||||
msg += b'\x3a\x2f\x2f\x77\x77\x77\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f'
|
||||
@@ -119,10 +117,8 @@ def MsgOtaUpdateReq(): # Over the air update request from talent cloud
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
def client_connection():
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 5005
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
@@ -131,10 +127,8 @@ def ClientConnection():
|
||||
time.sleep(2.5)
|
||||
s.close()
|
||||
|
||||
def tempClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
def tempclient_connection():
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 5005
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
@@ -144,100 +138,99 @@ def tempClientConnection():
|
||||
|
||||
def test_open_close():
|
||||
try:
|
||||
for s in tempClientConnection():
|
||||
pass
|
||||
except:
|
||||
for _ in tempclient_connection():
|
||||
pass # test side effect of generator
|
||||
except Exception:
|
||||
assert False
|
||||
assert True
|
||||
|
||||
def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
def test_send_contact_info1(client_connection, msg_contact_info, msg_contact_resp):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
s.sendall(msg_contact_info)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert data == MsgContactResp
|
||||
assert data == msg_contact_resp
|
||||
|
||||
|
||||
def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
def test_send_contact_info2(client_connection, msg_contact_info2, msg_contact_info, msg_contact_resp):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgContactInfo2)
|
||||
s.sendall(msg_contact_info2)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
assert True
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
s.sendall(msg_contact_info)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert data == MsgContactResp
|
||||
assert data == msg_contact_resp
|
||||
|
||||
def test_send_contact_info3(ClientConnection, MsgContactInfo, MsgContactResp, MsgTimeStampReq):
|
||||
s = ClientConnection
|
||||
def test_send_contact_info3(client_connection, msg_contact_info, msg_contact_resp, msg_timestamp_req):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
s.sendall(msg_contact_info)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert data == MsgContactResp
|
||||
assert data == msg_contact_resp
|
||||
try:
|
||||
s.sendall(MsgTimeStampReq)
|
||||
s.sendall(msg_timestamp_req)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
|
||||
def test_send_contact_resp(ClientConnection, MsgContactResp):
|
||||
s = ClientConnection
|
||||
def test_send_contact_resp(client_connection, msg_contact_resp):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgContactResp)
|
||||
s.sendall(msg_contact_resp)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
assert True
|
||||
pass
|
||||
else:
|
||||
assert data == b''
|
||||
|
||||
def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd):
|
||||
s = ClientConnection
|
||||
def test_send_ctrl_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_controller_ind):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgTimeStampReq)
|
||||
data = s.recv(1024)
|
||||
s.sendall(msg_timestamp_req)
|
||||
_ = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
# assert data == MsgTimeStampResp
|
||||
# assert data == msg_timestamp_resp
|
||||
try:
|
||||
s.sendall(MsgContollerInd)
|
||||
data = s.recv(1024)
|
||||
s.sendall(msg_controller_ind)
|
||||
_ = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd):
|
||||
s = ClientConnection
|
||||
def test_send_inv_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inv_data, msg_inverter_ind):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgTimeStampReq)
|
||||
data = s.recv(1024)
|
||||
s.sendall(msg_timestamp_req)
|
||||
_ = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(32.5)
|
||||
# assert data == MsgTimeStampResp
|
||||
# assert data == msg_timestamp_resp
|
||||
try:
|
||||
s.sendall(MsgInvData)
|
||||
data = s.recv(1024)
|
||||
s.sendall(MsgInverterInd)
|
||||
data = s.recv(1024)
|
||||
s.sendall(msg_inv_data)
|
||||
_ = s.recv(1024)
|
||||
s.sendall(msg_inverter_ind)
|
||||
_ = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
def test_ota_req(ClientConnection, MsgOtaUpdateReq):
|
||||
s = ClientConnection
|
||||
def test_ota_req(client_connection, msg_ota_update_req):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(MsgOtaUpdateReq)
|
||||
data = s.recv(1024)
|
||||
s.sendall(msg_ota_update_req)
|
||||
_ = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
import pytest, socket, time, os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
#from scapy.all import *
|
||||
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
||||
@@ -89,14 +86,29 @@ def MsgDataResp(): # Contact Response message
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInvalidInfo(): # Contact Info message wrong start byte
|
||||
msg = b'\x47\xd4\x00\x10\x41\x00\x01' +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\x3c'
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = 'iot.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 10000
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
@@ -113,10 +125,7 @@ def checkResponse(data, Msg):
|
||||
|
||||
|
||||
def tempClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = 'iot.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 10000
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
@@ -127,11 +136,10 @@ def tempClientConnection():
|
||||
|
||||
def test_open_close():
|
||||
try:
|
||||
for s in tempClientConnection():
|
||||
pass
|
||||
for _ in tempClientConnection():
|
||||
pass # test generator tempClientConnection()
|
||||
except:
|
||||
assert False
|
||||
assert True
|
||||
|
||||
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
@@ -155,4 +163,22 @@ def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
|
||||
# time.sleep(2.5)
|
||||
checkResponse(data, MsgDataResp)
|
||||
|
||||
def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgInvalidInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
checkResponse(data, MsgContactResp)
|
||||
|
||||
|
||||
11
tsun.code-workspace
Normal file
11
tsun.code-workspace
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../wiki"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
Reference in New Issue
Block a user