Compare commits
88 Commits
v0.11.0-rc
...
v0.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446a0d88f0 | ||
|
|
ae4565238b | ||
|
|
abdbdefc18 | ||
|
|
24b092b69e | ||
|
|
4ffe8a395e | ||
|
|
868d3ef312 | ||
|
|
ce050e8ab5 | ||
|
|
354c204ce6 | ||
|
|
d8200b5a35 | ||
|
|
cf1563dd55 | ||
|
|
962f6ee5fb | ||
|
|
9e60ad4bcd | ||
|
|
20503b46ab | ||
|
|
1781dba065 | ||
|
|
26ca006853 | ||
|
|
a93368336a | ||
|
|
772da4fa96 | ||
|
|
338b86964d | ||
|
|
3f4997a0fd | ||
|
|
35952654db | ||
|
|
2220517b55 | ||
|
|
55c403a754 | ||
|
|
3bf245300d | ||
|
|
b28792a654 | ||
|
|
d4ecbfe2b3 | ||
|
|
84dfd17d7b | ||
|
|
7067977d78 | ||
|
|
f227faa16e | ||
|
|
9572fbf609 | ||
|
|
3fddbe9456 | ||
|
|
34c09edb07 | ||
|
|
ef3ed5d287 | ||
|
|
7878c54ecc | ||
|
|
097e0d2104 | ||
|
|
181a22c369 | ||
|
|
89ab2eea3f | ||
|
|
264e09651f | ||
|
|
3bb360d359 | ||
|
|
8731d1bae3 | ||
|
|
4f0918ef83 | ||
|
|
5501288b51 | ||
|
|
851122515e | ||
|
|
badc065b7a | ||
|
|
aea6cc9763 | ||
|
|
92d1e648ae | ||
|
|
879b6608b3 | ||
|
|
b69e7e2242 | ||
|
|
0913fde126 | ||
|
|
bedbe08eeb | ||
|
|
3c81d446dd | ||
|
|
b335881500 | ||
|
|
ac7b02bde9 | ||
|
|
47a89c269f | ||
|
|
be3b4d6df0 | ||
|
|
a5b2b4b7c2 | ||
|
|
668c631018 | ||
|
|
07c989a305 | ||
|
|
28cf875533 | ||
|
|
9bae905c08 | ||
|
|
45b57109a8 | ||
|
|
2c69044bf8 | ||
|
|
3bada76516 | ||
|
|
84231c034c | ||
|
|
d4fd396dcf | ||
|
|
976eaed9ea | ||
|
|
211a958080 | ||
|
|
5ced5ff06a | ||
|
|
78a35b5513 | ||
|
|
9b22fe354c | ||
|
|
a6ad3d4f0d | ||
|
|
4993676614 | ||
|
|
10a18237c7 | ||
|
|
8d67f1745d | ||
|
|
9eb7c7fbe0 | ||
|
|
6c6109d421 | ||
|
|
7d0ea41728 | ||
|
|
ce5bd6eb0a | ||
|
|
6122f40718 | ||
|
|
c5f184a730 | ||
|
|
6da5d2cef6 | ||
|
|
db06d8c8e6 | ||
|
|
3863454a84 | ||
|
|
5775cb1ce3 | ||
|
|
5d61a261b1 | ||
|
|
bbda66e455 | ||
|
|
0c7bf7956d | ||
|
|
6b9c13ddfe | ||
|
|
c956c13d13 |
3
.cover_ghaction_rc
Normal file
3
.cover_ghaction_rc
Normal file
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
@@ -1,3 +1,2 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
9
.env_example
Normal file
9
.env_example
Normal file
@@ -0,0 +1,9 @@
|
||||
# example file for the .env file. The .env set private values
|
||||
# which are needed for builing containers
|
||||
|
||||
# registry for debug an dev container
|
||||
PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
|
||||
|
||||
# registry for official container (preview, rc, rel)
|
||||
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
|
||||
PUBLIC_CR_KEY=
|
||||
27
.github/workflows/python-app.yml
vendored
27
.github/workflows/python-app.yml
vendored
@@ -24,27 +24,16 @@ permissions:
|
||||
contents: read
|
||||
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
|
||||
|
||||
env:
|
||||
TZ: "Europe/Berlin"
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
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
|
||||
@@ -65,13 +54,13 @@ jobs:
|
||||
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 --cov=app/src --cov-report=xml
|
||||
python -m pytest app --cov=app/src --cov-config=.cover_ghaction_rc --cov-report=xml
|
||||
coverage report
|
||||
- name: Analyze with SonarCloud
|
||||
uses: SonarSource/sonarcloud-github-action@v2.2.0
|
||||
if: ${{ env.SONAR_TOKEN != 0 }}
|
||||
uses: SonarSource/sonarqube-scan-action@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
projectBaseDir: .
|
||||
args:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,11 +1,15 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
.venv/**
|
||||
bin/**
|
||||
mosquitto/**
|
||||
homeassistant/**
|
||||
ha_addons/ha_addon/rootfs/home/proxy/*
|
||||
ha_addons/ha_addon/rootfs/requirements.txt
|
||||
tsun_proxy/**
|
||||
Doku/**
|
||||
.DS_Store
|
||||
.coverage
|
||||
.env
|
||||
.venv
|
||||
coverage.xml
|
||||
|
||||
2
.hadolint.yaml
Normal file
2
.hadolint.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
ignored:
|
||||
- SC1091
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13.1
|
||||
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
"app/src",
|
||||
"app/tests",
|
||||
".venv/lib",
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"-vv",
|
||||
"app",
|
||||
"-vvv",
|
||||
"--cov=app/src",
|
||||
"--cov-report=xml",
|
||||
"--cov-report=html",
|
||||
"app",
|
||||
"system_tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"flake8.args": [
|
||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
||||
"--extend-exclude=app/tests/*.py,system_tests/*.py"
|
||||
],
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "s-allius",
|
||||
@@ -18,5 +22,11 @@
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/*.pyi": true
|
||||
}
|
||||
},
|
||||
"python.analysis.typeEvaluation.deprecateTypingAliases": true,
|
||||
"python.autoComplete.extraPaths": [
|
||||
".venv/lib"
|
||||
],
|
||||
"coverage-gutters.coverageBaseDir": "tsun",
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
## [0.12.1] - 2025-01-13
|
||||
|
||||
- addon: bump base image version to v17.1.0
|
||||
- addon: add syntax check to config parameters
|
||||
- addon: bump base image version to v17.0.2
|
||||
|
||||
## [0.12.0] - 2024-12-22
|
||||
|
||||
- add hadolint configuration
|
||||
- detect usage of a local DNS resolver [#37](https://github.com/s-allius/tsun-gen3-proxy/issues/37)
|
||||
- path for logs is now configurable by cli args
|
||||
- configure the number of keeped logfiles by cli args
|
||||
- add DOCS.md and CHANGELOG.md for add-ons
|
||||
- pin library version und update them with renovate
|
||||
- build config.yaml for add-ons by a jinja2 template
|
||||
- use gnu make to build proxy and add-on
|
||||
- make the configuration more flexible, add command line args to control this
|
||||
- fix the python path so we don't need special import paths for unit tests anymore
|
||||
- add emulator mode [#205](https://github.com/s-allius/tsun-gen3-proxy/issues/205)
|
||||
- ignore inverter replays which a older than 1 day [#246](https://github.com/s-allius/tsun-gen3-proxy/issues/246)
|
||||
- support test coverage in vscode
|
||||
- upgrade SonarQube action to version 4
|
||||
- update github action to Ubuntu 24-04
|
||||
- add initial support for home assistant add-ons from @mime24
|
||||
- github action: use ubuntu 24.04 and sonar-scanner-action 4 [#222](https://github.com/s-allius/tsun-gen3-proxy/issues/222)
|
||||
- migrate paho.mqtt CallbackAPIVersion to VERSION2 [#224](https://github.com/s-allius/tsun-gen3-proxy/issues/224)
|
||||
- add PROD_COMPL_TYPE to trace
|
||||
- add SolarmanV5 messages builder
|
||||
- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7)
|
||||
|
||||
## [0.11.1] - 2024-11-20
|
||||
|
||||
- fix pytest setup that can be startet from the rootdir
|
||||
- support python venv environment
|
||||
- add pytest.ini
|
||||
- move common settings from .vscode/settings.json into pytest.ini
|
||||
- add missing requirements
|
||||
- fix import paths for pytests
|
||||
- Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
|
||||
|
||||
## [0.11.0] - 2024-10-13
|
||||
|
||||
- fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196)
|
||||
|
||||
14
Makefile
Normal file
14
Makefile
Normal file
@@ -0,0 +1,14 @@
|
||||
.PHONY: build clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel
|
||||
|
||||
debug dev preview rc rel:
|
||||
$(MAKE) -C app $@
|
||||
|
||||
clean build:
|
||||
$(MAKE) -C ha_addons $@
|
||||
|
||||
addon-dev addon-debug addon-rc addon-rel:
|
||||
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
|
||||
|
||||
check-docker-compose:
|
||||
docker-compose config -q
|
||||
|
||||
54
README.md
54
README.md
@@ -9,13 +9,13 @@
|
||||
<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.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>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=alert_status"><img alt="The quality gate status" 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 alt="No of bugs" 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 alt="No of code-smells" 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>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=coverage"><img alt="Test coverage in percent" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
|
||||
</p>
|
||||
|
||||
# Overview
|
||||
@@ -28,6 +28,9 @@ Through this, the inverter then establishes a connection to the proxy and the pr
|
||||
|
||||
By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily.
|
||||
|
||||
Alternatively you can run the TSUN-Proxy as a Home Assistant Add-on. The installation of this add-on is pretty straightforward and not different in comparison to installing any other custom Home Assistant add-on.
|
||||
Follow the Instructions mentioned in the add-on subdirectory `ha_addons`.
|
||||
|
||||
<br>
|
||||
ℹ️ This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN.
|
||||
<br><br>
|
||||
@@ -65,11 +68,20 @@ Here are some screenshots of how the inverter is displayed in the Home Assistant
|
||||
|
||||
## Requirements
|
||||
|
||||
### for Docker Installation
|
||||
|
||||
- A running Docker engine to host the container
|
||||
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
|
||||
|
||||
### for Home Assistant Add-on Installation
|
||||
|
||||
- Running Home Assistant on Home Assistant OS or Supervised. Container and Core installations doesn't support add-ons.
|
||||
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
|
||||
|
||||
# Getting Started
|
||||
|
||||
## for Docker Installation
|
||||
|
||||
To run the proxy, you first need to create the image. You can do this quite simply as follows:
|
||||
|
||||
```sh
|
||||
@@ -95,8 +107,22 @@ With this information we can customize the `docker run`` statement:
|
||||
docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
||||
```
|
||||
|
||||
## for Home Assistant Add-on Installation
|
||||
|
||||
1. Add the repository URL to the Home Assistant add-on store
|
||||
[![Add repository on my Home Assistant][repository-badge]][repository-url]
|
||||
2. Reload the add-on store page
|
||||
3. Click the "Install" button to install the add-on.
|
||||
|
||||
# Configuration
|
||||
|
||||
```txt
|
||||
❗The following description applies to the Docker installation. When installing the Home Assistant add-on, the
|
||||
configuration is carried out via the Home Assistant UI. Some of the options described below are not required for
|
||||
this. Additionally, creating a config.toml file is not necessary. However, for a general understanding of the
|
||||
configuration and functionality, it is helpful to read the following description.
|
||||
```
|
||||
|
||||
The configuration consists of several parts. First, the container and the proxy itself must be configured, and then the connection of the inverter to the proxy must be set up, which is done differently depending on the inverter generation
|
||||
|
||||
For GEN3PLUS inverters, this can be done easily via the web interface of the inverter. The GEN3 inverters do not have a web interface, so the proxy is integrated via a modified DNS resolution.
|
||||
@@ -112,7 +138,7 @@ No special configuration is required for the Docker container if it is built and
|
||||
|
||||
On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files.
|
||||
|
||||
A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#docker-compose-environment-variables).
|
||||
A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#docker-compose-environment-variables).
|
||||
|
||||
## Proxy Configuration
|
||||
|
||||
@@ -142,7 +168,7 @@ You find more details here: <https://toml.io/en/v1.0.0>
|
||||
### 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
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml
|
||||
###
|
||||
### 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
|
||||
@@ -158,7 +184,7 @@ You find more details here: <https://toml.io/en/v1.0.0>
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account
|
||||
##
|
||||
|
||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||
@@ -175,7 +201,7 @@ mqtt.passwd = ''
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant
|
||||
##
|
||||
|
||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||
@@ -193,7 +219,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only
|
||||
##
|
||||
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
@@ -209,7 +235,7 @@ tsun.port = 5005
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#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'
|
||||
@@ -275,7 +301,7 @@ 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}
|
||||
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
@@ -320,7 +346,6 @@ In this case, you MUST NOT change the port or the host address, as this may caus
|
||||
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)
|
||||
@@ -408,3 +433,6 @@ We're very happy to receive contributions to this project! You can get started b
|
||||
## Changelog
|
||||
|
||||
The changelog lives in [CHANGELOG.md](https://github.com/s-allius/tsun-gen3-proxy/blob/main/CHANGELOG.md). It follows the principles of [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
|
||||
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fs-allius%2Fha-addons
|
||||
|
||||
1
app/.version
Normal file
1
app/.version
Normal file
@@ -0,0 +1 @@
|
||||
0.12.1
|
||||
@@ -4,14 +4,13 @@ ARG GID=1000
|
||||
|
||||
#
|
||||
# first stage for our base image
|
||||
FROM python:3.12-alpine AS base
|
||||
USER root
|
||||
FROM python:3.13-alpine AS base
|
||||
|
||||
COPY --chmod=0700 ./hardening_base.sh .
|
||||
COPY --chmod=0700 ./hardening_base.sh /
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache su-exec && \
|
||||
./hardening_base.sh && \
|
||||
rm ./hardening_base.sh
|
||||
apk add --no-cache su-exec=0.2-r3 && \
|
||||
/hardening_base.sh && \
|
||||
rm /hardening_base.sh
|
||||
|
||||
#
|
||||
# second stage for building wheels packages
|
||||
@@ -19,8 +18,8 @@ FROM base AS builder
|
||||
|
||||
# copy the dependencies file to the root dir and install requirements
|
||||
COPY ./requirements.txt /root/
|
||||
RUN apk add --no-cache build-base && \
|
||||
python -m pip install --no-cache-dir -U pip wheel && \
|
||||
RUN apk add --no-cache build-base=0.5-r3 && \
|
||||
python -m pip install --no-cache-dir pip==24.3.1 wheel==0.45.1 && \
|
||||
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
|
||||
|
||||
|
||||
@@ -50,9 +49,9 @@ VOLUME ["/home/$SERVICE_NAME/log", "/home/$SERVICE_NAME/config"]
|
||||
# and unistall python packages and alpine package manger to reduce attack surface
|
||||
COPY --from=builder /root/wheels /root/wheels
|
||||
COPY --chmod=0700 ./hardening_final.sh .
|
||||
RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
||||
RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && \
|
||||
rm -rf /root/wheels && \
|
||||
python -m pip uninstall --yes setuptools wheel pip && \
|
||||
python -m pip uninstall --yes wheel pip && \
|
||||
apk --purge del apk-tools && \
|
||||
./hardening_final.sh && \
|
||||
rm ./hardening_final.sh
|
||||
|
||||
67
app/Makefile
Normal file
67
app/Makefile
Normal file
@@ -0,0 +1,67 @@
|
||||
#!make
|
||||
include ../.env
|
||||
|
||||
SHELL = /bin/sh
|
||||
IMAGE = tsun-gen3-proxy
|
||||
|
||||
|
||||
# Folders
|
||||
SRC=.
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
DST=rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
export BUILD_DATE := ${shell date -Iminutes}
|
||||
VERSION := $(shell cat $(SRC)/.version)
|
||||
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||
|
||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||
|
||||
|
||||
dev debug:
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
preview rc rel:
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
|
||||
|
||||
.PHONY: debug dev preview rc rel
|
||||
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
51
app/build.sh
51
app/build.sh
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Usage: ./build.sh [dev|rc|rel]
|
||||
# dev: development build
|
||||
# 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 .zprofile
|
||||
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
BUILD_DATE=$(date -Iminutes)
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
VERSION=$(git describe --tags --abbrev=0)
|
||||
VERSION="${VERSION:1}"
|
||||
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 ]] || [[ $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|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
|
||||
docker buildx bake -f app/docker-bake.hcl $1
|
||||
|
||||
echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
|
||||
docker-compose config -q
|
||||
echo
|
||||
echo -e "${GREEN}${BUILD_DATE} => Version: ${VERSION}${NC} finished"
|
||||
echo
|
||||
@@ -15,7 +15,7 @@
|
||||
### 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
|
||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml
|
||||
###
|
||||
### 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
|
||||
@@ -31,7 +31,7 @@
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account
|
||||
##
|
||||
|
||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||
@@ -48,7 +48,7 @@ mqtt.passwd = ''
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant
|
||||
##
|
||||
|
||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||
@@ -66,7 +66,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only
|
||||
##
|
||||
|
||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
@@ -82,7 +82,7 @@ tsun.port = 5005
|
||||
## 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
|
||||
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#solarman-cloud-for-gen3plus-inverter-only
|
||||
##
|
||||
|
||||
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||
@@ -149,7 +149,7 @@ 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}
|
||||
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
@@ -18,7 +18,7 @@ variable "DESCRIPTION" {
|
||||
}
|
||||
|
||||
target "_common" {
|
||||
context = "app"
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
|
||||
657
app/proxy_2.svg
657
app/proxy_2.svg
@@ -4,429 +4,368 @@
|
||||
<!-- 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)">
|
||||
<svg width="539pt" height="2000pt"
|
||||
viewBox="0.00 0.00 538.57 2000.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 1996)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1938 464.348,-1938 464.348,4 -4,4"/>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1996 534.566,-1996 534.566,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>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="98.1981,-1972 -.0661,-1972 -.0661,-1928 104.1981,-1928 104.1981,-1966 98.1981,-1972"/>
|
||||
<polyline fill="none" stroke="#000000" points="98.1981,-1972 98.1981,-1966 "/>
|
||||
<polyline fill="none" stroke="#000000" points="104.1981,-1966 98.1981,-1966 "/>
|
||||
<text text-anchor="middle" x="52.066" y="-1959" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
|
||||
<text text-anchor="middle" x="52.066" y="-1947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
|
||||
<text text-anchor="middle" x="52.066" y="-1935" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3 inverter!</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>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1960 122.066,-1992 238.066,-1992 238.066,-1960 122.066,-1960"/>
|
||||
<text text-anchor="start" x="131.715" y="-1973" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1940 122.066,-1960 238.066,-1960 238.066,-1940 122.066,-1940"/>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1908 122.066,-1940 238.066,-1940 238.066,-1908 122.066,-1908"/>
|
||||
<text text-anchor="start" x="158.676" y="-1921" 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>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1748 135.066,-1780 225.066,-1780 225.066,-1748 135.066,-1748"/>
|
||||
<text text-anchor="start" x="144.7725" y="-1761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1716 135.066,-1748 225.066,-1748 225.066,-1716 135.066,-1716"/>
|
||||
<text text-anchor="start" x="160.8995" y="-1729" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1684 135.066,-1716 225.066,-1716 225.066,-1684 135.066,-1684"/>
|
||||
<text text-anchor="start" x="165.0685" y="-1697" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A1->A14 -->
|
||||
<g id="edge19" class="edge">
|
||||
<g id="edge14" 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"/>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M180.066,-1897.756C180.066,-1862.0883 180.066,-1815.1755 180.066,-1780.3644"/>
|
||||
<polygon fill="none" stroke="#000000" points="176.5661,-1897.9674 180.066,-1907.9674 183.5661,-1897.9674 176.5661,-1897.9674"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-662 179.066,-694 277.066,-694 277.066,-662 179.066,-662"/>
|
||||
<text text-anchor="start" x="204.4505" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-606 179.066,-662 277.066,-662 277.066,-606 179.066,-606"/>
|
||||
<text text-anchor="start" x="218.063" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="188.619" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="193.898" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-550 179.066,-606 277.066,-606 277.066,-550 179.066,-550"/>
|
||||
<text text-anchor="start" x="192.508" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="213.0685" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</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>
|
||||
<polygon fill="none" stroke="#000000" points="400.4026,-320 303.7294,-320 303.7294,-284 400.4026,-284 400.4026,-320"/>
|
||||
<text text-anchor="middle" x="352.066" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</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>
|
||||
<!-- A2->A3 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A2->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M260.3657,-538.4062C268.7304,-516.7744 277.7293,-493.5168 286.066,-472 305.502,-421.8362 328.2143,-363.368 341.2906,-329.7205"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="260.2523,-538.6998 261.8194,-545.7386 255.9247,-549.8923 254.3577,-542.8536 260.2523,-538.6998"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="345.0251,-320.1117 345.5968,-331.0627 343.2138,-324.7721 341.4024,-329.4325 341.4024,-329.4325 341.4024,-329.4325 343.2138,-324.7721 337.2081,-327.8023 345.0251,-320.1117 345.0251,-320.1117"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="285.4601,-320 178.6719,-320 178.6719,-284 285.4601,-284 285.4601,-320"/>
|
||||
<text text-anchor="middle" x="232.066" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</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 -->
|
||||
<!-- A2->A4 -->
|
||||
<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"/>
|
||||
<title>A2->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M229.12,-537.6831C229.9778,-469.0527 231.1375,-376.283 231.7124,-330.2853"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="229.1188,-537.7877 233.0434,-543.8372 228.9687,-549.7868 225.044,-543.7372 229.1188,-537.7877"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="231.839,-320.1609 236.2135,-330.2164 231.7764,-325.1605 231.7139,-330.1601 231.7139,-330.1601 231.7139,-330.1601 231.7764,-325.1605 227.2143,-330.1038 231.839,-320.1609 231.839,-320.1609"/>
|
||||
</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"/>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,-100 246.066,-132 424.066,-132 424.066,-100 246.066,-100"/>
|
||||
<text text-anchor="start" x="290.6175" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,-68 246.066,-100 424.066,-100 424.066,-68 246.066,-68"/>
|
||||
<text text-anchor="start" x="302.837" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,0 246.066,-68 424.066,-68 424.066,0 246.066,0"/>
|
||||
<text text-anchor="start" x="286.7235" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="277.5545" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="255.875" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="320.0685" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</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"/>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M349.8809,-271.6651C347.5364,-239.1181 343.722,-186.1658 340.5509,-142.1431"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="349.898,-271.9044 354.3188,-277.6014 350.7603,-283.8733 346.3395,-278.1763 349.898,-271.9044"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="339.8226,-132.0321 345.0295,-141.6829 340.1818,-137.0192 340.5411,-142.0063 340.5411,-142.0063 340.5411,-142.0063 340.1818,-137.0192 336.0527,-142.3296 339.8226,-132.0321 339.8226,-132.0321"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-82 74.066,-114 212.066,-114 212.066,-82 74.066,-82"/>
|
||||
<text text-anchor="start" x="100.563" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-62 74.066,-82 212.066,-82 212.066,-62 74.066,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-18 74.066,-62 212.066,-62 212.066,-18 74.066,-18"/>
|
||||
<text text-anchor="start" x="96.944" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="83.89" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A9->A2 -->
|
||||
<!-- A4->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A4->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M225.2301,-283.8733C212.4699,-250.0372 184.5329,-175.9573 164.7878,-123.5994"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="161.2018,-114.0904 168.941,-121.8593 162.9661,-118.7688 164.7305,-123.4472 164.7305,-123.4472 164.7305,-123.4472 162.9661,-118.7688 160.5199,-125.0351 161.2018,-114.0904 161.2018,-114.0904"/>
|
||||
<text text-anchor="middle" x="210.9254" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-1114 129.066,-1146 246.066,-1146 246.066,-1114 129.066,-1114"/>
|
||||
<text text-anchor="start" x="156.995" y="-1127" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-1094 129.066,-1114 246.066,-1114 246.066,-1094 129.066,-1094"/>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-822 129.066,-1094 246.066,-1094 246.066,-822 129.066,-822"/>
|
||||
<text text-anchor="start" x="157.002" y="-1075" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="155.332" y="-1063" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="169.2295" y="-1039" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="167.01" y="-1027" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="170.6195" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="166.7295" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="170.8995" y="-991" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="166.735" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="170.8995" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="165.3405" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="167.0105" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="170.3445" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="166.4545" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="170.6245" y="-895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="166.46" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="170.6245" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="162.565" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="138.9455" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="66.066,-652 66.066,-684 159.066,-684 159.066,-652 66.066,-652"/>
|
||||
<text text-anchor="start" x="84.23" y="-665" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="66.066,-560 66.066,-652 159.066,-652 159.066,-560 66.066,-560"/>
|
||||
<text text-anchor="start" x="75.614" y="-633" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="79.503" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="79.228" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="78.662" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="94.7795" y="-585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="88.1155" y="-573" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M151.3775,-811.7434C141.9017,-766.0069 132.2713,-719.5241 124.914,-684.013"/>
|
||||
<polygon fill="none" stroke="#000000" points="148.0039,-812.7126 153.4599,-821.7945 154.8583,-811.2924 148.0039,-812.7126"/>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-390 59.066,-422 161.066,-422 161.066,-390 59.066,-390"/>
|
||||
<text text-anchor="start" x="80.34" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-310 59.066,-390 161.066,-390 161.066,-310 59.066,-310"/>
|
||||
<text text-anchor="start" x="95.619" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="97.849" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="100.063" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="95.619" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="96.174" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-182 59.066,-310 161.066,-310 161.066,-182 59.066,-182"/>
|
||||
<text text-anchor="start" x="81.72" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="97.848" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="95.0685" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="90.62" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="75.3365" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="74.787" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="68.673" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M111.6134,-549.5774C111.3784,-511.9877 111.0852,-465.0771 110.8174,-422.2295"/>
|
||||
<polygon fill="none" stroke="#000000" points="108.1155,-549.9435 111.678,-559.9214 115.1153,-549.8996 108.1155,-549.9435"/>
|
||||
</g>
|
||||
<!-- A7->A8 -->
|
||||
<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>
|
||||
<title>A7->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M167.5272,-185.0204C168.3649,-184.0001 169.2111,-182.9929 170.066,-182 191.4283,-157.1889 219.1964,-135.0276 245.8416,-116.8901"/>
|
||||
<polygon fill="none" stroke="#000000" points="164.637,-183.0361 161.2751,-193.0834 170.1688,-187.3255 164.637,-183.0361"/>
|
||||
</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"/>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M128.2709,-171.8077C131.1447,-151.2556 133.9487,-131.2022 136.3294,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="124.7747,-171.5375 126.856,-181.9259 131.7072,-172.5069 124.7747,-171.5375"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-740 295.066,-772 409.066,-772 409.066,-740 295.066,-740"/>
|
||||
<text text-anchor="start" x="338.174" y="-753" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-600 295.066,-740 409.066,-740 409.066,-600 295.066,-600"/>
|
||||
<text text-anchor="start" x="332.889" y="-721" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="342.063" y="-709" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="304.829" y="-685" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="339.8435" y="-673" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="320.666" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="324.006" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="327.6105" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="325.95" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="338.178" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-472 295.066,-600 409.066,-600 409.066,-472 295.066,-472"/>
|
||||
<text text-anchor="start" x="309.5585" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="311.4985" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="317.3425" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="305.3945" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="307.3395" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="316.5065" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="332.62" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="337.0685" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge9" class="edge">
|
||||
<g id="edge7" 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>
|
||||
<path fill="none" stroke="#000000" d="M352.066,-461.6172C352.066,-412.1611 352.066,-362.7538 352.066,-332.2961"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="352.066,-471.8382 347.5661,-461.8382 352.066,-466.8382 352.0661,-461.8382 352.0661,-461.8382 352.0661,-461.8382 352.066,-466.8382 356.5661,-461.8383 352.066,-471.8382 352.066,-471.8382"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="352.0661,-332.0807 348.066,-326.0808 352.066,-320.0807 356.066,-326.0807 352.0661,-332.0807"/>
|
||||
</g>
|
||||
<!-- A10->A4 -->
|
||||
<g id="edge17" class="edge">
|
||||
<g id="edge9" 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>
|
||||
<path fill="none" stroke="#000000" d="M292.1869,-462.3225C270.8082,-405.3126 249.4091,-348.2482 238.8463,-320.0807"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="295.7553,-471.8382 288.0306,-464.055 293.9997,-467.1566 292.244,-462.4749 292.244,-462.4749 292.244,-462.4749 293.9997,-467.1566 296.4575,-460.8948 295.7553,-471.8382 295.7553,-471.8382"/>
|
||||
<text text-anchor="middle" x="253.125" y="-331.0849" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</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>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-318 432.066,-350 499.066,-350 499.066,-318 432.066,-318"/>
|
||||
<text text-anchor="start" x="448.059" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-298 432.066,-318 499.066,-318 499.066,-298 432.066,-298"/>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-254 432.066,-298 499.066,-298 499.066,-254 432.066,-254"/>
|
||||
<text text-anchor="start" x="441.95" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="449.734" y="-267" 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"/>
|
||||
<!-- A10->A12 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A10->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M405.0919,-471.8382C419.1748,-431.9575 433.5466,-391.2585 444.6898,-359.7024"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="448.0405,-350.2137 448.9539,-361.1415 446.3756,-354.9284 444.7107,-359.6431 444.7107,-359.6431 444.7107,-359.6431 446.3756,-354.9284 440.4675,-358.1447 448.0405,-350.2137 448.0405,-350.2137"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-710 428.066,-742 531.066,-742 531.066,-710 428.066,-710"/>
|
||||
<text text-anchor="start" x="468.728" y="-723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-654 428.066,-710 531.066,-710 531.066,-654 428.066,-654"/>
|
||||
<text text-anchor="start" x="471.5075" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="447.052" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="460.6695" y="-667" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-502 428.066,-654 531.066,-654 531.066,-502 428.066,-502"/>
|
||||
<text text-anchor="start" x="455.4015" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="453.4505" y="-623" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="450.3965" y="-611" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="448.7265" y="-599" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="446.776" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="461.779" y="-575" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="454.56" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="455.9405" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="440.103" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="449.5515" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="437.8885" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge13" class="edge">
|
||||
<g id="edge11" 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"/>
|
||||
<path fill="none" stroke="#000000" d="M473.3644,-491.6786C471.1803,-441.7544 468.8213,-387.8351 467.1788,-350.293"/>
|
||||
<polygon fill="none" stroke="#000000" points="469.8793,-492.0959 473.8131,-501.9334 476.8726,-491.7899 469.8793,-492.0959"/>
|
||||
</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"/>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1524 156.066,-1556 305.066,-1556 305.066,-1524 156.066,-1524"/>
|
||||
<text text-anchor="start" x="210.2835" y="-1537" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1300 156.066,-1524 305.066,-1524 305.066,-1300 156.066,-1300"/>
|
||||
<text text-anchor="start" x="193.8925" y="-1505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="204.45" y="-1493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="205.2845" y="-1481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="212.7795" y="-1469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="191.109" y="-1457" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="205.556" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
|
||||
<text text-anchor="start" x="211.39" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
|
||||
<text text-anchor="start" x="208.8905" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="202.781" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
|
||||
<text text-anchor="start" x="199.722" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
|
||||
<text text-anchor="start" x="206.666" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
|
||||
<text text-anchor="start" x="180.2705" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
|
||||
<text text-anchor="start" x="199.4505" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
|
||||
<text text-anchor="start" x="195.573" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
|
||||
<text text-anchor="start" x="204.451" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
|
||||
<text text-anchor="start" x="193.6185" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
|
||||
<text text-anchor="start" x="184.72" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1196 156.066,-1300 305.066,-1300 305.066,-1196 156.066,-1196"/>
|
||||
<text text-anchor="start" x="179.4505" y="-1281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
|
||||
<text text-anchor="start" x="208.066" y="-1269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
|
||||
<text text-anchor="start" x="180.8335" y="-1257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="165.8255" y="-1245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async> end_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="215.5685" y="-1233" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="201.3965" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="199.7265" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
</g>
|
||||
<!-- A13->A5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A13->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M210.2965,-1195.7758C208.8462,-1182.5547 207.3854,-1169.2373 205.9406,-1156.0662"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="204.8393,-1146.0268 210.403,-1155.4764 205.3846,-1150.997 205.9298,-1155.9672 205.9298,-1155.9672 205.9298,-1155.9672 205.3846,-1150.997 201.4567,-1156.4579 204.8393,-1146.0268 204.8393,-1146.0268"/>
|
||||
<text text-anchor="middle" x="199.9181" y="-1175.6794" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13->A10 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A13->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M260.8183,-1185.9405C281.556,-1057.7747 308.5382,-891.0162 327.7708,-772.1524"/>
|
||||
<polygon fill="none" stroke="#000000" points="257.3528,-1185.4467 259.2105,-1195.8774 264.2629,-1186.5648 257.3528,-1185.4467"/>
|
||||
</g>
|
||||
<!-- A14->A13 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A14->A13</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M188.2401,-1673.8004C192.8037,-1641.3079 198.7631,-1598.8764 204.747,-1556.2713"/>
|
||||
<polygon fill="none" stroke="#000000" points="184.7342,-1673.5986 186.8092,-1683.9883 191.6661,-1674.5723 184.7342,-1673.5986"/>
|
||||
</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>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1826 244.066,-1858 319.066,-1858 319.066,-1826 244.066,-1826"/>
|
||||
<text text-anchor="start" x="263.7835" y="-1839" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1674 244.066,-1826 319.066,-1826 319.066,-1674 244.066,-1674"/>
|
||||
<text text-anchor="start" x="273.2275" y="-1807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="254.056" y="-1783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="255.171" y="-1771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="265.1745" y="-1759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="255.4555" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="263.508" y="-1735" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="275.4575" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="262.1195" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="260.445" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="274.9025" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1606 244.066,-1674 319.066,-1674 319.066,-1606 244.066,-1606"/>
|
||||
<text text-anchor="start" x="255.456" y="-1655" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="258.79" y="-1643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="256.29" y="-1631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="266.5685" y="-1619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</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>
|
||||
<!-- A15->A13 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A15->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M261.5887,-1596.041C259.7128,-1582.9463 257.7908,-1569.5297 255.8664,-1556.0971"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="263.0135,-1605.9867 257.1408,-1596.726 262.3044,-1601.0373 261.5953,-1596.0878 261.5953,-1596.0878 261.5953,-1596.0878 262.3044,-1601.0373 266.0499,-1595.4496 263.0135,-1605.9867 263.0135,-1605.9867"/>
|
||||
<text text-anchor="middle" x="266.8039" y="-1569.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="252.0761" y="-1586.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
@@ -2,11 +2,12 @@
|
||||
// {direction:topDown}
|
||||
// {generate:true}
|
||||
|
||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||
[note: Example of instantiation for a GEN3 inverter!{bg:cornsilk}]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[InverterG3|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3]++->[local:StreamPtr]
|
||||
[InverterG3]++->[remote:StreamPtr]
|
||||
|
||||
[<<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]
|
||||
@@ -19,33 +20,24 @@
|
||||
[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]
|
||||
[Talent|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]<-++[local:StreamPtr]
|
||||
[local:StreamPtr]++->[AsyncStreamServer]
|
||||
[Talent]<-0..1[remote:StreamPtr]
|
||||
[remote:StreamPtr]0..1->[AsyncStreamClient]
|
||||
|
||||
[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]
|
||||
|
||||
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
|
||||
[Message]use->[<<AsyncIfc>>]
|
||||
|
||||
[<<ProtocolIfc>>|_registry|close()]
|
||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||
[<<ProtocolIfc>>]^-.-[Message|node_id|inc_counter();dec_counter()]
|
||||
[<<ProtocolIfc>>]^-.-[Message]
|
||||
[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]
|
||||
[Modbus]<0..1-has[Message]
|
||||
|
||||
364
app/proxy_3.svg
Normal file
364
app/proxy_3.svg
Normal file
@@ -0,0 +1,364 @@
|
||||
<?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="539pt" height="1940pt"
|
||||
viewBox="0.00 0.00 538.62 1940.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 1936)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1936 534.6165,-1936 534.6165,4 -4,4"/>
|
||||
<!-- A0 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="114.3497,-1912 -.1167,-1912 -.1167,-1868 120.3497,-1868 120.3497,-1906 114.3497,-1912"/>
|
||||
<polyline fill="none" stroke="#000000" points="114.3497,-1912 114.3497,-1906 "/>
|
||||
<polyline fill="none" stroke="#000000" points="120.3497,-1906 114.3497,-1906 "/>
|
||||
<text text-anchor="middle" x="60.1165" y="-1899" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
|
||||
<text text-anchor="middle" x="60.1165" y="-1887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
|
||||
<text text-anchor="middle" x="60.1165" y="-1875" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3PLUS inverter!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1900 138.1165,-1932 254.1165,-1932 254.1165,-1900 138.1165,-1900"/>
|
||||
<text text-anchor="start" x="147.7655" y="-1913" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1880 138.1165,-1900 254.1165,-1900 254.1165,-1880 138.1165,-1880"/>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1848 138.1165,-1880 254.1165,-1880 254.1165,-1848 138.1165,-1848"/>
|
||||
<text text-anchor="start" x="174.7265" y="-1861" 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="151.1165,-1688 151.1165,-1720 241.1165,-1720 241.1165,-1688 151.1165,-1688"/>
|
||||
<text text-anchor="start" x="160.823" y="-1701" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="151.1165,-1656 151.1165,-1688 241.1165,-1688 241.1165,-1656 151.1165,-1656"/>
|
||||
<text text-anchor="start" x="176.95" y="-1669" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="151.1165,-1624 151.1165,-1656 241.1165,-1656 241.1165,-1624 151.1165,-1624"/>
|
||||
<text text-anchor="start" x="181.119" y="-1637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A1->A14 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A1->A14</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M196.1165,-1837.756C196.1165,-1802.0883 196.1165,-1755.1755 196.1165,-1720.3644"/>
|
||||
<polygon fill="none" stroke="#000000" points="192.6166,-1837.9674 196.1165,-1847.9674 199.6166,-1837.9674 192.6166,-1837.9674"/>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-632 202.1165,-664 300.1165,-664 300.1165,-632 202.1165,-632"/>
|
||||
<text text-anchor="start" x="224.1665" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-576 202.1165,-632 300.1165,-632 300.1165,-576 202.1165,-576"/>
|
||||
<text text-anchor="start" x="241.1135" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="211.6695" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="216.9485" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-520 202.1165,-576 300.1165,-576 300.1165,-520 202.1165,-520"/>
|
||||
<text text-anchor="start" x="215.5585" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="236.119" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="419.4531,-320 322.7799,-320 322.7799,-284 419.4531,-284 419.4531,-320"/>
|
||||
<text text-anchor="middle" x="371.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A2->A3 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A2->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M285.5402,-508.8093C310.5478,-448.3743 342.848,-370.3156 359.7149,-329.5539"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="285.5219,-508.8538 286.9238,-515.9273 280.9336,-519.942 279.5317,-512.8685 285.5219,-508.8538"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="363.5595,-320.2627 363.894,-331.2235 361.6477,-324.8828 359.7359,-329.5029 359.7359,-329.5029 359.7359,-329.5029 361.6477,-324.8828 355.5779,-327.7823 363.5595,-320.2627 363.5595,-320.2627"/>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="304.5106,-320 197.7224,-320 197.7224,-284 304.5106,-284 304.5106,-320"/>
|
||||
<text text-anchor="middle" x="251.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A2->A4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A2->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M251.1165,-507.5905C251.1165,-447.68 251.1165,-370.9429 251.1165,-330.266"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="251.1166,-507.942 255.1165,-513.942 251.1165,-519.942 247.1165,-513.942 251.1166,-507.942"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="251.1165,-320.2627 255.6166,-330.2626 251.1165,-325.2627 251.1166,-330.2627 251.1166,-330.2627 251.1166,-330.2627 251.1165,-325.2627 246.6166,-330.2627 251.1165,-320.2627 251.1165,-320.2627"/>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,-100 265.1165,-132 443.1165,-132 443.1165,-100 265.1165,-100"/>
|
||||
<text text-anchor="start" x="309.668" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,-68 265.1165,-100 443.1165,-100 443.1165,-68 265.1165,-68"/>
|
||||
<text text-anchor="start" x="321.8875" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,0 265.1165,-68 443.1165,-68 443.1165,0 265.1165,0"/>
|
||||
<text text-anchor="start" x="305.774" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="296.605" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="274.9255" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="339.119" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M368.9314,-271.6651C366.5869,-239.1181 362.7725,-186.1658 359.6014,-142.1431"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="368.9485,-271.9044 373.3693,-277.6014 369.8108,-283.8733 365.39,-278.1763 368.9485,-271.9044"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="358.8731,-132.0321 364.08,-141.6829 359.2323,-137.0192 359.5916,-142.0063 359.5916,-142.0063 359.5916,-142.0063 359.2323,-137.0192 355.1032,-142.3296 358.8731,-132.0321 358.8731,-132.0321"/>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-82 93.1165,-114 231.1165,-114 231.1165,-82 93.1165,-82"/>
|
||||
<text text-anchor="start" x="119.6135" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-62 93.1165,-82 231.1165,-82 231.1165,-62 93.1165,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-18 93.1165,-62 231.1165,-62 231.1165,-18 93.1165,-18"/>
|
||||
<text text-anchor="start" x="115.9945" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="102.9405" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A4->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A4->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M244.2806,-283.8733C231.5204,-250.0372 203.5834,-175.9573 183.8383,-123.5994"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="180.2523,-114.0904 187.9915,-121.8593 182.0166,-118.7688 183.781,-123.4472 183.781,-123.4472 183.781,-123.4472 182.0166,-118.7688 179.5704,-125.0351 180.2523,-114.0904 180.2523,-114.0904"/>
|
||||
<text text-anchor="middle" x="229.9759" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-1054 145.1165,-1086 262.1165,-1086 262.1165,-1054 145.1165,-1054"/>
|
||||
<text text-anchor="start" x="173.0455" y="-1067" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-1034 145.1165,-1054 262.1165,-1054 262.1165,-1034 145.1165,-1034"/>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-762 145.1165,-1034 262.1165,-1034 262.1165,-762 145.1165,-762"/>
|
||||
<text text-anchor="start" x="173.0525" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="171.3825" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="185.28" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="183.0605" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="186.67" y="-955" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="182.78" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="186.95" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="182.7855" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="186.95" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="181.391" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="183.061" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="186.395" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="182.505" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="186.675" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="182.5105" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="186.675" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="178.6155" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="154.996" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="87.1165,-622 87.1165,-654 180.1165,-654 180.1165,-622 87.1165,-622"/>
|
||||
<text text-anchor="start" x="105.2805" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="87.1165,-530 87.1165,-622 180.1165,-622 180.1165,-530 87.1165,-530"/>
|
||||
<text text-anchor="start" x="96.6645" y="-603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="100.5535" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="100.2785" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="99.7125" y="-567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="115.83" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="109.166" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M166.8518,-752.0017C159.4629,-716.9571 152.1492,-682.2694 146.2303,-654.1971"/>
|
||||
<polygon fill="none" stroke="#000000" points="163.4489,-752.8275 168.9367,-761.8903 170.2983,-751.3833 163.4489,-752.8275"/>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-390 78.1165,-422 180.1165,-422 180.1165,-390 78.1165,-390"/>
|
||||
<text text-anchor="start" x="99.3905" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-310 78.1165,-390 180.1165,-390 180.1165,-310 78.1165,-310"/>
|
||||
<text text-anchor="start" x="114.6695" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="116.8995" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="119.1135" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="114.6695" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="115.2245" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-182 78.1165,-310 180.1165,-310 180.1165,-182 78.1165,-182"/>
|
||||
<text text-anchor="start" x="100.7705" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="116.8985" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="114.119" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="109.6705" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="94.387" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="93.8375" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="87.7235" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M132.1177,-519.5861C131.7106,-490.0737 131.229,-455.1552 130.7721,-422.0295"/>
|
||||
<polygon fill="none" stroke="#000000" points="128.6207,-519.837 132.2584,-529.7877 135.6201,-519.7404 128.6207,-519.837"/>
|
||||
</g>
|
||||
<!-- A7->A8 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A7->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M186.5777,-185.0204C187.4154,-184.0001 188.2616,-182.9929 189.1165,-182 210.4788,-157.1889 238.2469,-135.0276 264.8921,-116.8901"/>
|
||||
<polygon fill="none" stroke="#000000" points="183.6875,-183.0361 180.3256,-193.0834 189.2193,-187.3255 183.6875,-183.0361"/>
|
||||
</g>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M147.3214,-171.8077C150.1952,-151.2556 152.9992,-131.2022 155.3799,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="143.8252,-171.5375 145.9065,-181.9259 150.7577,-172.5069 143.8252,-171.5375"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-668 319.1165,-700 410.1165,-700 410.1165,-668 319.1165,-668"/>
|
||||
<text text-anchor="start" x="337.1115" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-552 319.1165,-668 410.1165,-668 410.1165,-552 319.1165,-552"/>
|
||||
<text text-anchor="start" x="345.4395" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="354.6135" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="349.6145" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="352.674" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="357.6725" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="336.8265" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="350.7285" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-484 319.1165,-552 410.1165,-552 410.1165,-484 319.1165,-484"/>
|
||||
<text text-anchor="start" x="329.057" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="345.1705" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="349.619" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M366.9763,-473.5237C368.222,-421.9136 369.5798,-365.6622 370.389,-332.138"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="366.733,-483.6023 362.4757,-473.4966 366.8537,-478.6038 366.9744,-473.6052 366.9744,-473.6052 366.9744,-473.6052 366.8537,-478.6038 371.4731,-473.7139 366.733,-483.6023 366.733,-483.6023"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="370.3911,-332.0495 366.5371,-325.9547 370.6807,-320.053 374.5347,-326.1478 370.3911,-332.0495"/>
|
||||
</g>
|
||||
<!-- A10->A4 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M318.2339,-474.2481C295.3796,-415.5956 270.1211,-350.7729 258.151,-320.053"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="321.8788,-483.6023 314.0551,-475.9185 320.0634,-478.9435 318.2481,-474.2847 318.2481,-474.2847 318.2481,-474.2847 320.0634,-478.9435 322.441,-472.6508 321.8788,-483.6023 321.8788,-483.6023"/>
|
||||
<text text-anchor="middle" x="272.6076" y="-330.8736" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-318 442.1165,-350 509.1165,-350 509.1165,-318 442.1165,-318"/>
|
||||
<text text-anchor="start" x="454.775" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-298 442.1165,-318 509.1165,-318 509.1165,-298 442.1165,-298"/>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-254 442.1165,-298 509.1165,-298 509.1165,-254 442.1165,-254"/>
|
||||
<text text-anchor="start" x="452.0005" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="459.7845" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A10->A12 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A10->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M405.6067,-483.6023C421.7045,-441.5449 439.4849,-395.0916 453.0329,-359.6958"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="456.737,-350.0185 457.3649,-360.9664 454.9496,-354.6881 453.1623,-359.3577 453.1623,-359.3577 453.1623,-359.3577 454.9496,-354.6881 448.9596,-357.7491 456.737,-350.0185 456.737,-350.0185"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-680 428.1165,-712 531.1165,-712 531.1165,-680 428.1165,-680"/>
|
||||
<text text-anchor="start" x="468.7785" y="-693" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-624 428.1165,-680 531.1165,-680 531.1165,-624 428.1165,-624"/>
|
||||
<text text-anchor="start" x="471.558" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="447.1025" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="460.72" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-472 428.1165,-624 531.1165,-624 531.1165,-472 428.1165,-472"/>
|
||||
<text text-anchor="start" x="455.452" y="-605" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="453.501" y="-593" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="450.447" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="448.777" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="446.8265" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="461.8295" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="454.6105" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="455.991" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="440.1535" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="449.602" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="437.939" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M477.322,-461.8987C476.7744,-422.1971 476.206,-380.9898 475.7834,-350.352"/>
|
||||
<polygon fill="none" stroke="#000000" points="473.823,-462.0018 477.4607,-471.9525 480.8223,-461.9052 473.823,-462.0018"/>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1464 172.1165,-1496 321.1165,-1496 321.1165,-1464 172.1165,-1464"/>
|
||||
<text text-anchor="start" x="226.334" y="-1477" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1240 172.1165,-1464 321.1165,-1464 321.1165,-1240 172.1165,-1240"/>
|
||||
<text text-anchor="start" x="209.943" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="220.5005" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="221.335" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="228.83" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="207.1595" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="221.6065" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
|
||||
<text text-anchor="start" x="227.4405" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
|
||||
<text text-anchor="start" x="224.941" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="218.8315" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
|
||||
<text text-anchor="start" x="215.7725" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
|
||||
<text text-anchor="start" x="222.7165" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
|
||||
<text text-anchor="start" x="196.321" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
|
||||
<text text-anchor="start" x="215.501" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
|
||||
<text text-anchor="start" x="211.6235" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
|
||||
<text text-anchor="start" x="220.5015" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
|
||||
<text text-anchor="start" x="209.669" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
|
||||
<text text-anchor="start" x="200.7705" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1136 172.1165,-1240 321.1165,-1240 321.1165,-1136 172.1165,-1136"/>
|
||||
<text text-anchor="start" x="195.501" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
|
||||
<text text-anchor="start" x="224.1165" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
|
||||
<text text-anchor="start" x="196.884" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="181.876" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async> end_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="231.619" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="217.447" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="215.777" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
</g>
|
||||
<!-- A13->A5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A13->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M226.347,-1135.7758C224.8967,-1122.5547 223.4359,-1109.2373 221.9911,-1096.0662"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="220.8898,-1086.0268 226.4535,-1095.4764 221.4351,-1090.997 221.9803,-1095.9672 221.9803,-1095.9672 221.9803,-1095.9672 221.4351,-1090.997 217.5072,-1096.4579 220.8898,-1086.0268 220.8898,-1086.0268"/>
|
||||
<text text-anchor="middle" x="215.9686" y="-1115.6794" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13->A10 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A13->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M277.1595,-1125.5329C299.2708,-989.8666 328.1962,-812.3923 346.4719,-700.2604"/>
|
||||
<polygon fill="none" stroke="#000000" points="273.6668,-1125.205 275.5125,-1135.6378 280.5757,-1126.3311 273.6668,-1125.205"/>
|
||||
</g>
|
||||
<!-- A14->A13 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A14->A13</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M204.2906,-1613.8004C208.8542,-1581.3079 214.8136,-1538.8764 220.7975,-1496.2713"/>
|
||||
<polygon fill="none" stroke="#000000" points="200.7847,-1613.5986 202.8597,-1623.9883 207.7166,-1614.5723 200.7847,-1613.5986"/>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1766 260.1165,-1798 335.1165,-1798 335.1165,-1766 260.1165,-1766"/>
|
||||
<text text-anchor="start" x="279.834" y="-1779" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1614 260.1165,-1766 335.1165,-1766 335.1165,-1614 260.1165,-1614"/>
|
||||
<text text-anchor="start" x="289.278" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="270.1065" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="271.2215" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="281.225" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="271.506" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="279.5585" y="-1675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="291.508" y="-1663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="278.17" y="-1651" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="276.4955" y="-1639" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="290.953" y="-1627" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1546 260.1165,-1614 335.1165,-1614 335.1165,-1546 260.1165,-1546"/>
|
||||
<text text-anchor="start" x="271.5065" y="-1595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="274.8405" y="-1583" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="272.3405" y="-1571" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="282.619" y="-1559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A15->A13 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A15->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M277.6392,-1536.041C275.7633,-1522.9463 273.8413,-1509.5297 271.9169,-1496.0971"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="279.064,-1545.9867 273.1913,-1536.726 278.3549,-1541.0373 277.6458,-1536.0878 277.6458,-1536.0878 277.6458,-1536.0878 278.3549,-1541.0373 282.1004,-1535.4496 279.064,-1545.9867 279.064,-1545.9867"/>
|
||||
<text text-anchor="middle" x="282.8544" y="-1509.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="268.1266" y="-1526.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
42
app/proxy_3.yuml
Normal file
42
app/proxy_3.yuml
Normal file
@@ -0,0 +1,42 @@
|
||||
// {type:class}
|
||||
// {direction:topDown}
|
||||
// {generate:true}
|
||||
|
||||
[note: Example of instantiation for a GEN3PLUS inverter!{bg:cornsilk}]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3P]++->[local:StreamPtr]
|
||||
[InverterG3P]++->[remote:StreamPtr]
|
||||
|
||||
[<<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]
|
||||
|
||||
[SolarmanV5|conn_no;addr;;control;serial;snr;db:InfosG3P;switch|msg_unknown();;healthy();close()]
|
||||
[SolarmanV5]<-++[local:StreamPtr]
|
||||
[local:StreamPtr]++->[AsyncStreamServer]
|
||||
[SolarmanV5]<-0..1[remote:StreamPtr]
|
||||
[remote:StreamPtr]0..1->[AsyncStreamClient]
|
||||
|
||||
[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]^[InfosG3P||ha_confs();parse()]
|
||||
|
||||
[SolarmanV5]->[InfosG3P]
|
||||
|
||||
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
|
||||
[Message]use->[<<AsyncIfc>>]
|
||||
|
||||
[<<ProtocolIfc>>|_registry|close()]
|
||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||
[<<ProtocolIfc>>]^-.-[Message]
|
||||
[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]<0..1-has[Message]
|
||||
@@ -2,5 +2,7 @@
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
python-dotenv
|
||||
mock
|
||||
coverage
|
||||
coverage
|
||||
jinja2-cli
|
||||
@@ -1,4 +1,4 @@
|
||||
aiomqtt==2.3.0
|
||||
schema==0.7.7
|
||||
aiocron==1.8
|
||||
aiohttp==3.10.5
|
||||
aiohttp==3.11.11
|
||||
@@ -6,16 +6,10 @@ 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
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
|
||||
|
||||
import gc
|
||||
@@ -221,7 +215,6 @@ class AsyncStream(AsyncIfcImpl):
|
||||
|
||||
async def disc(self) -> None:
|
||||
"""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}')
|
||||
@@ -306,6 +299,14 @@ class AsyncStream(AsyncIfcImpl):
|
||||
f"Fwd Exception for {self.r_addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
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 AsyncStreamServer(AsyncStream):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
@@ -355,14 +356,6 @@ class AsyncStreamServer(AsyncStream):
|
||||
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,
|
||||
@@ -370,6 +363,11 @@ class AsyncStreamClient(AsyncStream):
|
||||
AsyncStream.__init__(self, reader, writer, rstream)
|
||||
self.close_cb = close_cb
|
||||
|
||||
async def disc(self) -> None:
|
||||
logging.debug('AsyncStreamClient.disc()')
|
||||
self.remote = None
|
||||
await super().disc()
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug('AsyncStreamClient.close()')
|
||||
self.close_cb = None
|
||||
@@ -377,7 +375,11 @@ class AsyncStreamClient(AsyncStream):
|
||||
|
||||
async def client_loop(self, _: str) -> None:
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
Infos.inc_counter('Cloud_Conn_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
await self.loop()
|
||||
Infos.dec_counter('Cloud_Conn_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
'Client loop stopped for'
|
||||
f' l{self.l_addr}')
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
|
||||
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
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
|
||||
|
||||
class ByteFifo:
|
||||
""" a byte FIFO buffer with trigger callback """
|
||||
__slots__ = ('__buf', '__trigger_cb')
|
||||
|
||||
def __init__(self):
|
||||
self.__buf = bytearray()
|
||||
self.__trigger_cb = None
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
'''Config module handles the proxy configuration in the config.toml file'''
|
||||
'''Config module handles the proxy configuration'''
|
||||
|
||||
import shutil
|
||||
import tomllib
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from schema import Schema, And, Or, Use, Optional
|
||||
|
||||
|
||||
class ConfigIfc(ABC):
|
||||
'''Abstract basis class for config readers'''
|
||||
def __init__(self):
|
||||
Config.add(self)
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self) -> dict: # pragma: no cover
|
||||
'''get the unverified config from the reader'''
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def descr(self) -> str: # pragma: no cover
|
||||
'''return a descriction of the source, e.g. the file name'''
|
||||
pass
|
||||
|
||||
def _extend_key(self, conf, key, val):
|
||||
'''split a dotted dict key into a hierarchical dict tree '''
|
||||
lst = key.split('.')
|
||||
d = conf
|
||||
for i, idx in enumerate(lst, 1): # pragma: no branch
|
||||
if i == len(lst):
|
||||
d[idx] = val
|
||||
break
|
||||
if idx not in d:
|
||||
d[idx] = {}
|
||||
d = d[idx]
|
||||
|
||||
|
||||
class Config():
|
||||
'''Static class Config is reads and sanitize the config.
|
||||
'''Static class Config build and sanitize the internal config dictenary.
|
||||
|
||||
Read config.toml file and sanitize it with read().
|
||||
Get named parts of the config with get()'''
|
||||
Using config readers, a partial configuration is added to config.
|
||||
Config readers are a derivation of the abstract ConfigIfc reader.
|
||||
When a config reader is instantiated, theits `get_config` method is
|
||||
called automatically and afterwards the config will be merged.
|
||||
'''
|
||||
|
||||
act_config = {}
|
||||
def_config = {}
|
||||
conf_schema = Schema({
|
||||
'tsun': {
|
||||
'enabled': Use(bool),
|
||||
@@ -28,8 +57,10 @@ class Config():
|
||||
'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))
|
||||
'user': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None))),
|
||||
'passwd': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None)))
|
||||
},
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
@@ -57,7 +88,8 @@ class Config():
|
||||
Optional('client_mode'): {
|
||||
'host': Use(str),
|
||||
Optional('port', default=8899):
|
||||
And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
Optional('forward', default=False): Use(bool),
|
||||
},
|
||||
Optional('modbus_polling', default=True): Use(bool),
|
||||
Optional('suggested_area', default=""): Use(str),
|
||||
@@ -92,7 +124,13 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def class_init(cls) -> None | str: # pragma: no cover
|
||||
def init(cls, def_reader: ConfigIfc) -> None | str:
|
||||
'''Initialise the Proxy-Config
|
||||
|
||||
Copy the internal default config file into the config directory
|
||||
and initialise the Config with the default configuration '''
|
||||
cls.err = None
|
||||
cls.def_config = {}
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
@@ -102,66 +140,59 @@ class Config():
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
err_str = cls.read()
|
||||
del cls.conf_schema
|
||||
return err_str
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict: # pragma: no cover
|
||||
usr_config = {}
|
||||
|
||||
# read example config file as default configuration
|
||||
try:
|
||||
with open("config/config.toml", "rb") as f:
|
||||
usr_config = tomllib.load(f)
|
||||
def_config = def_reader.get_config()
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
logging.info(f'Read from {def_reader.descr()} => ok')
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
logging.info(
|
||||
'\n To create the missing config.toml file, '
|
||||
'you can rename the template config.example.toml\n'
|
||||
' and customize it for your scenario.\n')
|
||||
return usr_config
|
||||
cls.err = f'Config.read: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {def_reader.descr()} => error\n {error}")
|
||||
|
||||
cls.act_config = cls.def_config.copy()
|
||||
|
||||
@classmethod
|
||||
def read(cls, path='') -> None | str:
|
||||
'''Read config file, merge it with the default config
|
||||
def add(cls, reader: ConfigIfc):
|
||||
'''Merge the config from the Config Reader into the config
|
||||
|
||||
Checks if a default config exists. If no default configuration exists,
|
||||
the Config.init method has not yet been called.This is normal for the very
|
||||
first Config Reader which creates the default config and must be ignored
|
||||
here. The default config reader is handled in the Config.init method'''
|
||||
if hasattr(cls, 'def_config'):
|
||||
cls.__parse(reader)
|
||||
|
||||
@classmethod
|
||||
def get_error(cls) -> None | str:
|
||||
'''return the last error as a string or None if there is no error'''
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def __parse(cls, reader) -> None | str:
|
||||
'''Read config from the reader, merge it with the default config
|
||||
and sanitize the result'''
|
||||
err = None
|
||||
config = {}
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
res = 'ok'
|
||||
try:
|
||||
# read example config file as default configuration
|
||||
cls.def_config = {}
|
||||
with open(f"{path}default_config.toml", "rb") as f:
|
||||
def_config = tomllib.load(f)
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
|
||||
# overwrite the default values, with values from
|
||||
# the config.toml file
|
||||
usr_config = cls._read_config_file()
|
||||
|
||||
# merge the default and the user config
|
||||
config = def_config.copy()
|
||||
rd_config = reader.get_config()
|
||||
config = cls.act_config.copy()
|
||||
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
|
||||
'gen3plus']:
|
||||
if key in usr_config:
|
||||
config[key] |= usr_config[key]
|
||||
|
||||
try:
|
||||
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.act_config}" ')
|
||||
if key in rd_config:
|
||||
config[key] = config[key] | rd_config[key]
|
||||
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except FileNotFoundError:
|
||||
res = 'n/a'
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logger.error(err)
|
||||
cls.act_config = {}
|
||||
cls.err = f'error: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {reader.descr()} => error\n {error}")
|
||||
return cls.err
|
||||
|
||||
return err
|
||||
logging.info(f'Read from {reader.descr()} => {res}')
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def get(cls, member: str = None):
|
||||
25
app/src/cnf/config_read_env.py
Normal file
25
app/src/cnf/config_read_env.py
Normal file
@@ -0,0 +1,25 @@
|
||||
'''Config Reader module which handles config values from the environment'''
|
||||
|
||||
import os
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadEnv(ConfigIfc):
|
||||
'''Reader for environment values of the configuration'''
|
||||
|
||||
def get_config(self) -> dict:
|
||||
conf = {}
|
||||
data = [
|
||||
('mqtt.host', 'MQTT_HOST'),
|
||||
('mqtt.port', 'MQTT_PORT'),
|
||||
('mqtt.user', 'MQTT_USER'),
|
||||
('mqtt.passwd', 'MQTT_PASSWORD'),
|
||||
]
|
||||
for key, env_var in data:
|
||||
val = os.getenv(env_var)
|
||||
if val:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def descr(self):
|
||||
return "environment"
|
||||
46
app/src/cnf/config_read_json.py
Normal file
46
app/src/cnf/config_read_json.py
Normal file
@@ -0,0 +1,46 @@
|
||||
'''Config Reader module which handles *.json config files'''
|
||||
|
||||
import json
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadJson(ConfigIfc):
|
||||
'''Reader for json config files'''
|
||||
def __init__(self, cnf_file='/data/options.json'):
|
||||
'''Read a json file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def convert_inv(self, conf, inv):
|
||||
if 'serial' in inv:
|
||||
snr = inv['serial']
|
||||
del inv['serial']
|
||||
conf[snr] = {}
|
||||
|
||||
for key, val in inv.items():
|
||||
self._extend_key(conf[snr], key, val)
|
||||
|
||||
def convert_inv_arr(self, conf, key, val: list):
|
||||
if key not in conf:
|
||||
conf[key] = {}
|
||||
for elm in val:
|
||||
self.convert_inv(conf[key], elm)
|
||||
|
||||
def convert_to_obj(self, data):
|
||||
conf = {}
|
||||
for key, val in data.items():
|
||||
if key == 'inverters' and isinstance(val, list):
|
||||
self.convert_inv_arr(conf, key, val)
|
||||
else:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file) as f:
|
||||
data = json.load(f)
|
||||
return self.convert_to_obj(data)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
21
app/src/cnf/config_read_toml.py
Normal file
21
app/src/cnf/config_read_toml.py
Normal file
@@ -0,0 +1,21 @@
|
||||
'''Config Reader module which handles *.toml config files'''
|
||||
|
||||
import tomllib
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadToml(ConfigIfc):
|
||||
'''Reader for toml config files'''
|
||||
def __init__(self, cnf_file):
|
||||
'''Read a toml file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
@@ -3,13 +3,12 @@ import struct
|
||||
import logging
|
||||
from typing import Generator
|
||||
|
||||
if __name__ == "app.src.gen3.infos_g3":
|
||||
from app.src.infos import Infos, Register
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register
|
||||
from infos import Infos, Register
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
__slots__ = ()
|
||||
|
||||
map = {
|
||||
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
|
||||
0x000927c0: {'reg': Register.CHIP_TYPE},
|
||||
@@ -68,29 +67,27 @@ class RegisterMap:
|
||||
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},
|
||||
0x00000190: {'reg': Register.EVENT_ALARM},
|
||||
0x000001f4: {'reg': Register.EVENT_FAULT},
|
||||
0x00000258: {'reg': Register.EVENT_BF1},
|
||||
0x000002bc: {'reg': Register.EVENT_BF2},
|
||||
0x00000064: {'reg': Register.INVERTER_STATUS},
|
||||
|
||||
0x00000fa0: {'reg': Register.BOOT_STATUS},
|
||||
0x00001004: {'reg': Register.DSP_STATUS},
|
||||
0x000010cc: {'reg': Register.WORK_MODE},
|
||||
0x000011f8: {'reg': Register.OUTPUT_SHUTDOWN},
|
||||
0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
|
||||
0x000012c0: {'reg': Register.RATED_LEVEL},
|
||||
0x00001324: {'reg': Register.INPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
0x00001388: {'reg': Register.GRID_VOLT_CAL_COEF},
|
||||
0x00002710: {'reg': Register.PROD_COMPL_TYPE},
|
||||
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
}
|
||||
|
||||
|
||||
class InfosG3(Infos):
|
||||
__slots__ = ()
|
||||
|
||||
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
|
||||
sug_area: str = '') \
|
||||
@@ -180,11 +177,8 @@ class InfosG3(Infos):
|
||||
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)
|
||||
if row and 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
def __store_result(self, addr, result, info_id, node_id):
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
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
|
||||
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
|
||||
class InverterG3(InverterBase):
|
||||
|
||||
@@ -4,22 +4,12 @@ from zoneinfo import ZoneInfo
|
||||
from datetime import datetime
|
||||
from tzlocal import get_localzone
|
||||
|
||||
if __name__ == "app.src.gen3.talent":
|
||||
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 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
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from cnf.config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -42,19 +32,18 @@ class Control:
|
||||
|
||||
|
||||
class Talent(Message):
|
||||
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)
|
||||
super().__init__('G3', ifc, 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
|
||||
@@ -86,38 +75,17 @@ class Talent(Message):
|
||||
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()')
|
||||
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 = 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):
|
||||
@@ -135,6 +103,8 @@ class Talent(Message):
|
||||
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)
|
||||
if self.mb:
|
||||
self.mb.set_node_id(self.node_id)
|
||||
else:
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
@@ -203,16 +173,6 @@ class Talent(Message):
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -489,7 +449,7 @@ class Talent(Message):
|
||||
self.__build_header(0x99)
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.__process_data(False)
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
@@ -504,7 +464,7 @@ class Talent(Message):
|
||||
self.__build_header(0x99)
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.__process_data(True)
|
||||
self.state = State.up # allow MODBUS cmds
|
||||
if (self.modbus_polling):
|
||||
self.mb_timer.start(self.mb_first_timeout)
|
||||
@@ -519,8 +479,14 @@ class Talent(Message):
|
||||
|
||||
self.forward()
|
||||
|
||||
def __process_data(self):
|
||||
def __process_data(self, ignore_replay: bool):
|
||||
msg_hdr_len, ts = self.parse_msg_header()
|
||||
if ignore_replay:
|
||||
age = self._utc() - self._utcfromts(ts)
|
||||
age = age/(3600*24)
|
||||
logger.debug(f"Age: {age} days")
|
||||
if age > 1:
|
||||
return
|
||||
|
||||
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
|
||||
+ msg_hdr_len, self.node_id):
|
||||
@@ -590,8 +556,7 @@ class Talent(Message):
|
||||
return
|
||||
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||
hdr_len:],
|
||||
self.node_id):
|
||||
hdr_len:]):
|
||||
if update:
|
||||
self._set_mqtt_timestamp(key, self._utc())
|
||||
self.new_data[key] = True
|
||||
|
||||
@@ -1,39 +1,57 @@
|
||||
|
||||
import struct
|
||||
from typing import Generator
|
||||
|
||||
if __name__ == "app.src.gen3plus.infos_g3p":
|
||||
from app.src.infos import Infos, Register, ProxyMode
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register, ProxyMode
|
||||
from infos import Infos, Register, ProxyMode, Fmt
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
# make the class read/only by using __slots__
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
FMT_2_16BIT_VAL = '!HH'
|
||||
FMT_3_16BIT_VAL = '!HHH'
|
||||
FMT_4_16BIT_VAL = '!HHHH'
|
||||
|
||||
map = {
|
||||
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # 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
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'quotient': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||
0x4102001b: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 Max No Of Connected Devices
|
||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001d: {'reg': None, 'fmt': '<B', 'const': 1}, # 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
|
||||
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!6B', 'func': Fmt.mac}, # noqa: E501
|
||||
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x4102005c: {'reg': None, 'fmt': '<B', 'const': 15}, # noqa: E501
|
||||
0x4102005e: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 No Of Sensors (ListLen)
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x41020061: {'reg': None, 'fmt': '<HB', 'const': (15, 255)}, # noqa: E501
|
||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
|
||||
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
|
||||
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
|
||||
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # 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
|
||||
|
||||
# Start MODBUS Block: 0x3000 (R/O Measurements)
|
||||
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
|
||||
0x420100c2: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c4: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c6: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c8: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100ca: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100cc: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
# 0x420100ce
|
||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
|
||||
0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
|
||||
# 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
|
||||
# 0x420100da
|
||||
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
@@ -58,16 +76,45 @@ class RegisterMap:
|
||||
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
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
|
||||
0x42010116: {'reg': Register.INV_UNKNOWN_1, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
# Start MODBUS Block: 0x2000 (R/W Config Paramaneters)
|
||||
0x42010118: {'reg': Register.BOOT_STATUS, 'fmt': '!H'},
|
||||
0x4201011a: {'reg': Register.DSP_STATUS, 'fmt': '!H'},
|
||||
0x4201011c: {'reg': None, 'fmt': '!H', 'const': 1}, # noqa: E501
|
||||
0x4201011e: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||
0x42010124: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
|
||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H'},
|
||||
0x42010128: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
|
||||
0x4201012a: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x4201012c: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x4201012e: {'reg': None, 'fmt': '!H', 'const': 1024}, # noqa: E501
|
||||
0x42010130: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1024, 1, 0xffff, 1)}, # noqa: E501
|
||||
0x42010138: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
0x4201013a: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x68, 0x68, 0x500)}, # noqa: E501
|
||||
0x42010140: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9cd, 0x7b6, 0x139c, 0x1324)}, # noqa: E501
|
||||
0x42010148: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1, 0x7ae, 0x40f, 0x41)}, # noqa: E501
|
||||
0x42010150: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0xf, 0xa64, 0xa64, 0x6)}, # noqa: E501
|
||||
0x42010158: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x6, 0x9f6, 0x128c, 0x128c)}, # noqa: E501
|
||||
0x42010160: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x1452, 0x1452)}, # noqa: E501
|
||||
0x42010168: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x151, 0x5)}, # noqa: E501
|
||||
0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x42010172: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x1, 0x139c, 0xfa0)}, # noqa: E501
|
||||
0x42010178: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x4e, 0x66, 0x3e8, 0x400)}, # noqa: E501
|
||||
0x42010180: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9ce, 0x7a8, 0x139c, 0x1326)}, # noqa: E501
|
||||
0x42010188: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 0x0, 0)}, # noqa: E501
|
||||
0x42010190: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 1024, 1024)}, # noqa: E501
|
||||
0x42010198: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0, 0, 0xffff, 0)}, # noqa: E501
|
||||
0x420101a0: {'reg': None, 'fmt': FMT_2_16BIT_VAL, 'const': (0x0, 0x0)}, # noqa: E501
|
||||
|
||||
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
|
||||
class InfosG3P(Infos):
|
||||
__slots__ = ('client_mode', )
|
||||
|
||||
def __init__(self, client_mode: bool):
|
||||
super().__init__()
|
||||
self.client_mode = client_mode
|
||||
@@ -121,7 +168,7 @@ class InfosG3P(Infos):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
info_id = row['reg']
|
||||
result = self.__get_value(buf, addr, row)
|
||||
result = Fmt.get_value(buf, addr, row)
|
||||
|
||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||
|
||||
@@ -136,15 +183,22 @@ class InfosG3P(Infos):
|
||||
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
|
||||
def build(self, len, msg_type: int, rcv_ftype: int):
|
||||
buf = bytearray(len)
|
||||
for idx, row in RegisterMap.map.items():
|
||||
addr = idx & 0xffff
|
||||
ftype = (idx >> 16) & 0xff
|
||||
mtype = (idx >> 24) & 0xff
|
||||
if ftype != rcv_ftype or mtype != msg_type:
|
||||
continue
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if 'const' in row:
|
||||
val = row['const']
|
||||
else:
|
||||
info_id = row['reg']
|
||||
val = self.get_db_value(info_id)
|
||||
if not val:
|
||||
continue
|
||||
Fmt.set_value(buf, addr, row, val)
|
||||
return buf
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
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
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
|
||||
|
||||
class InverterG3P(InverterBase):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
client_mode: bool = False):
|
||||
remote_prot = None
|
||||
if client_mode:
|
||||
remote_prot = SolarmanEmu
|
||||
super().__init__(reader, writer, 'solarman',
|
||||
SolarmanV5, client_mode)
|
||||
SolarmanV5, client_mode, remote_prot)
|
||||
|
||||
138
app/src/gen3plus/solarman_emu.py
Normal file
138
app/src/gen3plus/solarman_emu.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from gen3plus.solarman_v5 import SolarmanBase
|
||||
from my_timer import Timer
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
|
||||
class SolarmanEmu(SolarmanBase):
|
||||
def __init__(self, addr, ifc: "AsyncIfc",
|
||||
server_side: bool, client_mode: bool):
|
||||
super().__init__(addr, ifc, server_side=False,
|
||||
_send_modbus_cb=None,
|
||||
mb_timeout=8)
|
||||
logging.debug('SolarmanEmu.init()')
|
||||
self.db = ifc.remote.stream.db
|
||||
self.snr = ifc.remote.stream.snr
|
||||
self.hb_timeout = 60
|
||||
'''actual heatbeat timeout from the last response message'''
|
||||
self.data_up_inv = self.db.get_db_value(Register.DATA_UP_INTERVAL)
|
||||
'''time interval for getting new MQTT data messages'''
|
||||
self.hb_timer = Timer(self.send_heartbeat_cb, self.node_id)
|
||||
self.data_timer = Timer(self.send_data_cb, self.node_id)
|
||||
self.last_sync = self._emu_timestamp()
|
||||
'''timestamp when we send the last sync message (4110)'''
|
||||
self.pkt_cnt = 0
|
||||
'''last sent packet number'''
|
||||
|
||||
self.switch = {
|
||||
|
||||
0x4210: 'msg_data_ind', # real time data
|
||||
0x1210: self.msg_response, # at least every 5 minutes
|
||||
|
||||
0x4710: 'msg_hbeat_ind', # heatbeat
|
||||
0x1710: self.msg_response, # every 2 minutes
|
||||
|
||||
0x4110: 'msg_dev_ind', # device data, sync start
|
||||
0x1110: self.msg_response, # every 3 hours
|
||||
|
||||
}
|
||||
|
||||
self.log_lvl = {
|
||||
|
||||
0x4110: logging.INFO, # device data, sync start
|
||||
0x1110: logging.INFO, # every 3 hours
|
||||
|
||||
0x4210: logging.INFO, # real time data
|
||||
0x1210: logging.INFO, # at least every 5 minutes
|
||||
|
||||
0x4710: logging.DEBUG, # heatbeat
|
||||
0x1710: logging.DEBUG, # every 2 minutes
|
||||
|
||||
}
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.info('SolarmanEmu.close()')
|
||||
# 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.hb_timer.close()
|
||||
self.data_timer.close()
|
||||
self.db = None
|
||||
super().close()
|
||||
|
||||
def _set_serial_no(self, snr: int):
|
||||
logging.debug(f'SolarmanEmu._set_serial_no, snr: {snr}')
|
||||
self.unique_id = str(snr)
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
logging.debug('SolarmanEmu.init_new()')
|
||||
self.data_timer.start(self.data_up_inv)
|
||||
return False
|
||||
|
||||
def next_pkt_cnt(self):
|
||||
'''get the next packet number'''
|
||||
self.pkt_cnt = (self.pkt_cnt + 1) & 0xffffffff
|
||||
return self.pkt_cnt
|
||||
|
||||
def seconds_since_last_sync(self):
|
||||
'''get seconds since last 0x4110 message was sent'''
|
||||
return self._emu_timestamp() - self.last_sync
|
||||
|
||||
def send_heartbeat_cb(self, exp_cnt):
|
||||
'''send a heartbeat to the TSUN cloud'''
|
||||
self._build_header(0x4710)
|
||||
self.ifc.tx_add(struct.pack('<B', 0))
|
||||
self._finish_send_msg()
|
||||
log_lvl = self.log_lvl.get(0x4710, logging.WARNING)
|
||||
self.ifc.tx_log(log_lvl, 'Send heartbeat:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def send_data_cb(self, exp_cnt):
|
||||
'''send a inverter data message to the TSUN cloud'''
|
||||
self.hb_timer.start(self.hb_timeout)
|
||||
self.data_timer.start(self.data_up_inv)
|
||||
_len = 420
|
||||
ftype = 1
|
||||
build_msg = self.db.build(_len, 0x42, ftype)
|
||||
|
||||
self._build_header(0x4210)
|
||||
self.ifc.tx_add(
|
||||
struct.pack(
|
||||
'<BHLLLHL', ftype, 0x02b0,
|
||||
self._emu_timestamp(),
|
||||
self.seconds_since_last_sync(),
|
||||
self.time_ofs,
|
||||
1, # offset 0x1a
|
||||
self.next_pkt_cnt()))
|
||||
self.ifc.tx_add(build_msg[0x20:])
|
||||
self._finish_send_msg()
|
||||
log_lvl = self.log_lvl.get(0x4210, logging.WARNING)
|
||||
self.ifc.tx_log(log_lvl, 'Send inv-data:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
'''
|
||||
Message handler methods
|
||||
'''
|
||||
def msg_response(self):
|
||||
'''handle a received response from the TSUN cloud'''
|
||||
logger.debug("EMU received rsp:")
|
||||
_, _, ts, hb = super().msg_response()
|
||||
logger.debug(f"EMU ts:{ts} hb:{hb}")
|
||||
self.hb_timeout = hb
|
||||
self.time_ofs = ts - self._emu_timestamp()
|
||||
self.hb_timer.start(self.hb_timeout)
|
||||
|
||||
def msg_unknown(self):
|
||||
'''counts a unknown or unexpected message from the TSUN cloud'''
|
||||
logger.warning(f"EMU Unknow Msg: ID:{int(self.control):#04x}")
|
||||
self.inc_counter('Unknown_Msg')
|
||||
@@ -4,22 +4,12 @@ import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||
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 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
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from cnf.config import Config
|
||||
from modbus import Modbus
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -50,13 +40,211 @@ class Sequence():
|
||||
return f'{self.rcv_idx:02x}:{self.snd_idx:02x}'
|
||||
|
||||
|
||||
class SolarmanV5(Message):
|
||||
class SolarmanBase(Message):
|
||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||
_send_modbus_cb, mb_timeout: int):
|
||||
super().__init__('G3P', ifc, server_side, _send_modbus_cb,
|
||||
mb_timeout)
|
||||
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.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.time_ofs = 0
|
||||
|
||||
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 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
|
||||
'''
|
||||
Our public methods
|
||||
'''
|
||||
def _flow_str(self, server_side: bool, type: str): # noqa: F821
|
||||
switch = {
|
||||
'rx': ' <',
|
||||
'tx': ' >',
|
||||
'forwrd': '<< ',
|
||||
'drop': ' xx',
|
||||
'rxS': '> ',
|
||||
'txS': '< ',
|
||||
'forwrdS': ' >>',
|
||||
'dropS': 'xx ',
|
||||
}
|
||||
if server_side:
|
||||
type += 'S'
|
||||
return switch.get(type, '???')
|
||||
|
||||
def get_fnc_handler(self, ctrl):
|
||||
fnc = self.switch.get(ctrl, self.msg_unknown)
|
||||
if callable(fnc):
|
||||
return fnc, repr(fnc.__name__)
|
||||
else:
|
||||
return self.msg_unknown, repr(fnc)
|
||||
|
||||
def _build_header(self, ctrl) -> None:
|
||||
'''build header for new transmit message'''
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
|
||||
self.ifc.tx_add(struct.pack(
|
||||
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr))
|
||||
_fnc, _str = self.get_fnc_handler(ctrl)
|
||||
logger.info(self._flow_str(self.server_side, 'tx') +
|
||||
f' Ctl: {int(ctrl):#04x} Msg: {_str}')
|
||||
|
||||
def _finish_send_msg(self) -> None:
|
||||
'''finish the transmit message, set lenght and checksum'''
|
||||
_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 _timestamp(self):
|
||||
# utc as epoche
|
||||
return int(time.time()) # pragma: no cover
|
||||
|
||||
def _emu_timestamp(self):
|
||||
'''timestamp for an emulated inverter (realtime - 1 day)'''
|
||||
one_day = 24*60*60
|
||||
return self._timestamp()-one_day
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __update_header(self, _forward_buffer):
|
||||
'''update header for message before forwarding,
|
||||
set sequence and checksum'''
|
||||
_len = len(_forward_buffer)
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('<BH', _forward_buffer, ofs)
|
||||
data_len = result[1] # len of variable id string
|
||||
|
||||
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 __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 __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||
|
||||
if (buf_len < self.header_len): # enough bytes for complete header?
|
||||
return
|
||||
|
||||
result = struct.unpack_from('<BHHHL', buf, 0)
|
||||
|
||||
# store parsed header values in the class
|
||||
start = result[0] # start byte
|
||||
self.data_len = result[1] # len of variable id string
|
||||
self.control = result[2]
|
||||
self.seq.set_recv(result[3])
|
||||
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.ifc.rx_clear()
|
||||
return
|
||||
self.header_valid = True
|
||||
|
||||
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 self.ifc.rx_len() > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
if next_start != 0xa5:
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
|
||||
return False
|
||||
|
||||
check = sum(buf[1:buf_len-2]) & 0xff
|
||||
if check != crc:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
|
||||
f' Stop:{int(stop):#02x}')
|
||||
# start & stop byte are valid, discard only this message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
self.ifc.rx_get(self.header_len + self.data_len+2)
|
||||
self.header_valid = False
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
_fnc, _str = self.get_fnc_handler(self.control)
|
||||
if self.unique_id:
|
||||
logger.info(self._flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {_str}')
|
||||
_fnc()
|
||||
else:
|
||||
logger.info(self._flow_str(self.server_side, 'drop') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {_str}')
|
||||
|
||||
'''
|
||||
Message handler methods
|
||||
'''
|
||||
def msg_response(self):
|
||||
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
|
||||
ts = result[2]
|
||||
set_hb = result[3] # always 60 or 120
|
||||
logger.debug(f'ftype:{ftype} accepted:{valid}'
|
||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
return ftype, valid, ts, set_hb
|
||||
|
||||
|
||||
class SolarmanV5(SolarmanBase):
|
||||
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'
|
||||
@@ -64,24 +252,15 @@ class SolarmanV5(Message):
|
||||
|
||||
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)
|
||||
super().__init__(addr, ifc, server_side, self.send_modbus_cb,
|
||||
mb_timeout=8)
|
||||
|
||||
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(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.establish_inv_emu = False
|
||||
'''create an Solarman EMU instance to send data to the TSUN cloud'''
|
||||
self.switch = {
|
||||
|
||||
0x4210: self.msg_data_ind, # real time data
|
||||
@@ -136,58 +315,44 @@ class SolarmanV5(Message):
|
||||
0x4510: logging.INFO, # from server
|
||||
0x1510: self.get_cmd_rsp_log_lvl,
|
||||
}
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
g3p_cnf = Config.get('gen3plus')
|
||||
|
||||
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
|
||||
self.sensor_list = 0
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.debug('Solarman.close()')
|
||||
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 = 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,
|
||||
forward: bool,
|
||||
start_timeout=MB_CLIENT_DATA_UP):
|
||||
self.no_forwarding = True
|
||||
self.establish_inv_emu = forward
|
||||
self.snr = snr
|
||||
self.__set_serial_no(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.DATA_UP_INTERVAL,
|
||||
300)
|
||||
self.db.set_db_def_value(Register.COLLECT_INTERVAL,
|
||||
1)
|
||||
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
||||
120)
|
||||
self.db.set_db_def_value(Register.SENSOR_LIST,
|
||||
Fmt.hex4((self.sensor_list, )))
|
||||
self.new_data['controller'] = True
|
||||
|
||||
self.state = State.up
|
||||
@@ -202,14 +367,25 @@ class SolarmanV5(Message):
|
||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||
self.mb_timeout)
|
||||
|
||||
def establish_emu(self):
|
||||
_len = 223
|
||||
build_msg = self.db.build(_len, 0x41, 2)
|
||||
struct.pack_into(
|
||||
'<BHHHLBL', build_msg, 0, 0xA5, _len-11, 0x4110,
|
||||
0, self.snr, 2, self._emu_timestamp())
|
||||
self.ifc.fwd_add(build_msg)
|
||||
self.ifc.fwd_add(struct.pack('<BB', 0, 0x15)) # crc & stop
|
||||
|
||||
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']
|
||||
if self.mb:
|
||||
self.mb.set_node_id(self.node_id)
|
||||
|
||||
def __set_serial_no(self, snr: int):
|
||||
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:
|
||||
@@ -226,7 +402,8 @@ class SolarmanV5(Message):
|
||||
self.db.set_pv_module_details(inv)
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, snr)
|
||||
self.db.set_db_def_value(Register.SERIAL_NUMBER, key)
|
||||
break
|
||||
else:
|
||||
self.node_id = ''
|
||||
@@ -240,35 +417,6 @@ class SolarmanV5(Message):
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
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 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
|
||||
|
||||
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:
|
||||
@@ -278,171 +426,37 @@ class SolarmanV5(Message):
|
||||
self.ifc.fwd_add(buffer[:buflen])
|
||||
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
|
||||
|
||||
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||
_, _str = self.get_fnc_handler(self.control)
|
||||
logger.info(self._flow_str(self.server_side, 'forwrd') +
|
||||
f' Ctl: {int(self.control):#04x}'
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
f' Msg: {_str}')
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
return False
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __flow_str(self, server_side: bool, type: str): # noqa: F821
|
||||
switch = {
|
||||
'rx': ' <',
|
||||
'tx': ' >',
|
||||
'forwrd': '<< ',
|
||||
'drop': ' xx',
|
||||
'rxS': '> ',
|
||||
'txS': '< ',
|
||||
'forwrdS': ' >>',
|
||||
'dropS': 'xx ',
|
||||
}
|
||||
if server_side:
|
||||
type += 'S'
|
||||
return switch.get(type, '???')
|
||||
|
||||
def _timestamp(self):
|
||||
# utc as epoche
|
||||
return int(time.time()) # pragma: no cover
|
||||
|
||||
def _heartbeat(self) -> int:
|
||||
return 60 # pragma: no cover
|
||||
|
||||
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||
|
||||
if (buf_len < self.header_len): # enough bytes for complete header?
|
||||
return
|
||||
|
||||
result = struct.unpack_from('<BHHHL', buf, 0)
|
||||
|
||||
# store parsed header values in the class
|
||||
start = result[0] # start byte
|
||||
self.data_len = result[1] # len of variable id string
|
||||
self.control = result[2]
|
||||
self.seq.set_recv(result[3])
|
||||
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.ifc.rx_clear()
|
||||
return
|
||||
self.header_valid = True
|
||||
|
||||
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 self.ifc.rx_len() > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
if next_start != 0xa5:
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
|
||||
return False
|
||||
|
||||
check = sum(buf[1:buf_len-2]) & 0xff
|
||||
if check != crc:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
|
||||
f' Stop:{int(stop):#02x}')
|
||||
# start & stop byte are valid, discard only this message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __build_header(self, ctrl) -> None:
|
||||
'''build header for new transmit message'''
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
|
||||
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 = 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)
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('<BH', _forward_buffer, ofs)
|
||||
data_len = result[1] # len of variable id string
|
||||
|
||||
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)
|
||||
if self.unique_id:
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
fnc()
|
||||
else:
|
||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
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._build_header(msgtype)
|
||||
self.ifc.tx_add(struct.pack('<BBLL', ftype, ack,
|
||||
self._timestamp(),
|
||||
self._heartbeat()))
|
||||
self.__finish_send_msg()
|
||||
self._finish_send_msg()
|
||||
|
||||
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||
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._build_header(0x4510)
|
||||
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()
|
||||
self._finish_send_msg()
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
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)
|
||||
|
||||
@@ -472,11 +486,11 @@ class SolarmanV5(Message):
|
||||
return
|
||||
|
||||
self.forward_at_cmd_resp = False
|
||||
self.__build_header(0x4510)
|
||||
self._build_header(0x4510)
|
||||
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._finish_send_msg()
|
||||
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
|
||||
try:
|
||||
self.ifc.tx_flush()
|
||||
@@ -643,6 +657,18 @@ class SolarmanV5(Message):
|
||||
return
|
||||
self.__forward_msg()
|
||||
|
||||
def __parse_modbus_rsp(self, data):
|
||||
inv_update = False
|
||||
self.modbus_elms = 0
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, self._timestamp())
|
||||
self.new_data[key] = True
|
||||
return inv_update
|
||||
|
||||
def __modbus_command_rsp(self, data):
|
||||
'''precess MODBUS RTU response'''
|
||||
valid = data[1]
|
||||
@@ -650,19 +676,13 @@ class SolarmanV5(Message):
|
||||
# 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
|
||||
inv_update = self.__parse_modbus_rsp(data)
|
||||
if inv_update:
|
||||
self.__build_model_name()
|
||||
|
||||
if self.establish_inv_emu and not self.ifc.remote.stream:
|
||||
self.establish_emu()
|
||||
|
||||
def msg_hbeat_ind(self):
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<B', data, 0)
|
||||
@@ -684,16 +704,3 @@ class SolarmanV5(Message):
|
||||
|
||||
self.__forward_msg()
|
||||
self.__send_ack_rsp(0x1810, ftype)
|
||||
|
||||
def msg_response(self):
|
||||
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
|
||||
ts = result[2]
|
||||
set_hb = result[3] # always 60 or 120
|
||||
logger.debug(f'ftype:{ftype} accepted:{valid}'
|
||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
|
||||
265
app/src/infos.py
265
app/src/infos.py
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Generator
|
||||
@@ -25,7 +26,11 @@ class Register(Enum):
|
||||
EQUIPMENT_MODEL = 24
|
||||
NO_INPUTS = 25
|
||||
MAX_DESIGNED_POWER = 26
|
||||
OUTPUT_COEFFICIENT = 27
|
||||
RATED_LEVEL = 27
|
||||
INPUT_COEFFICIENT = 28
|
||||
GRID_VOLT_CAL_COEF = 29
|
||||
OUTPUT_COEFFICIENT = 30
|
||||
PROD_COMPL_TYPE = 31
|
||||
INVERTER_CNT = 50
|
||||
UNKNOWN_SNR = 51
|
||||
UNKNOWN_MSG = 52
|
||||
@@ -38,10 +43,13 @@ class Register(Enum):
|
||||
AT_COMMAND = 59
|
||||
MODBUS_COMMAND = 60
|
||||
AT_COMMAND_BLOCKED = 61
|
||||
CLOUD_CONN_CNT = 62
|
||||
OUTPUT_POWER = 83
|
||||
RATED_POWER = 84
|
||||
INVERTER_TEMP = 85
|
||||
INVERTER_STATUS = 86
|
||||
DETECT_STATUS_1 = 87
|
||||
DETECT_STATUS_2 = 88
|
||||
PV1_VOLTAGE = 100
|
||||
PV1_CURRENT = 101
|
||||
PV1_POWER = 102
|
||||
@@ -84,6 +92,12 @@ class Register(Enum):
|
||||
PV5_TOTAL_GENERATION = 241
|
||||
PV6_DAILY_GENERATION = 250
|
||||
PV6_TOTAL_GENERATION = 251
|
||||
INV_UNKNOWN_1 = 252
|
||||
BOOT_STATUS = 253
|
||||
DSP_STATUS = 254
|
||||
WORK_MODE = 255
|
||||
OUTPUT_SHUTDOWN = 256
|
||||
|
||||
GRID_VOLTAGE = 300
|
||||
GRID_CURRENT = 301
|
||||
GRID_FREQUENCY = 302
|
||||
@@ -99,22 +113,11 @@ class Register(Enum):
|
||||
IP_ADDRESS = 407
|
||||
POLLING_INTERVAL = 408
|
||||
SENSOR_LIST = 409
|
||||
EVENT_401 = 500
|
||||
EVENT_402 = 501
|
||||
EVENT_403 = 502
|
||||
EVENT_404 = 503
|
||||
EVENT_405 = 504
|
||||
EVENT_406 = 505
|
||||
EVENT_407 = 506
|
||||
EVENT_408 = 507
|
||||
EVENT_409 = 508
|
||||
EVENT_410 = 509
|
||||
EVENT_411 = 510
|
||||
EVENT_412 = 511
|
||||
EVENT_413 = 512
|
||||
EVENT_414 = 513
|
||||
EVENT_415 = 514
|
||||
EVENT_416 = 515
|
||||
SSID = 410
|
||||
EVENT_ALARM = 500
|
||||
EVENT_FAULT = 501
|
||||
EVENT_BF1 = 502
|
||||
EVENT_BF2 = 503
|
||||
TS_INPUT = 600
|
||||
TS_GRID = 601
|
||||
TS_TOTAL = 602
|
||||
@@ -123,6 +126,76 @@ class Register(Enum):
|
||||
TEST_REG2 = 10001
|
||||
|
||||
|
||||
class Fmt:
|
||||
@staticmethod
|
||||
def get_value(buf: bytes, idx: int, row: dict):
|
||||
'''Get a value from buf and interpret as in row defined'''
|
||||
fmt = row['fmt']
|
||||
res = struct.unpack_from(fmt, buf, idx)
|
||||
result = res[0]
|
||||
if isinstance(result, (bytearray, bytes)):
|
||||
result = result.decode().split('\x00')[0]
|
||||
if 'func' in row:
|
||||
result = row['func'](res)
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
if 'quotient' in row:
|
||||
result = round(result/row['quotient'])
|
||||
if 'offset' in row:
|
||||
result = result + row['offset']
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def hex4(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
return f'{val[0]:04x}'
|
||||
else:
|
||||
return int(val, 16)
|
||||
|
||||
@staticmethod
|
||||
def mac(val: tuple | str, reverse=False) -> str | tuple:
|
||||
if not reverse:
|
||||
return "%02x:%02x:%02x:%02x:%02x:%02x" % val
|
||||
else:
|
||||
return (
|
||||
int(val[0:2], 16), int(val[3:5], 16),
|
||||
int(val[6:8], 16), int(val[9:11], 16),
|
||||
int(val[12:14], 16), int(val[15:], 16))
|
||||
|
||||
@staticmethod
|
||||
def version(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
x = val[0]
|
||||
return f'V{(x >> 12)}.{(x >> 8) & 0xf}' \
|
||||
f'.{(x >> 4) & 0xf}{x & 0xf:1X}'
|
||||
else:
|
||||
arr = val[1:].split('.')
|
||||
return int(arr[0], 10) << 12 | \
|
||||
int(arr[1], 10) << 8 | \
|
||||
int(arr[2][:-1], 10) << 4 | \
|
||||
int(arr[2][-1:], 16)
|
||||
|
||||
@staticmethod
|
||||
def set_value(buf: bytearray, idx: int, row: dict, val):
|
||||
'''Get a value from buf and interpret as in row defined'''
|
||||
fmt = row['fmt']
|
||||
if 'offset' in row:
|
||||
val = val - row['offset']
|
||||
if 'quotient' in row:
|
||||
val = round(val * row['quotient'])
|
||||
if 'ratio' in row:
|
||||
val = round(val / row['ratio'])
|
||||
if 'func' in row:
|
||||
val = row['func'](val, reverse=True)
|
||||
if isinstance(val, str):
|
||||
val = bytes(val, 'UTF8')
|
||||
|
||||
if isinstance(val, tuple):
|
||||
struct.pack_into(fmt, buf, idx, *val)
|
||||
else:
|
||||
struct.pack_into(fmt, buf, idx, val)
|
||||
|
||||
|
||||
class ClrAtMidnight:
|
||||
__clr_at_midnight = [Register.PV1_DAILY_GENERATION, Register.PV2_DAILY_GENERATION, Register.PV3_DAILY_GENERATION, Register.PV4_DAILY_GENERATION, Register.PV5_DAILY_GENERATION, Register.PV6_DAILY_GENERATION, Register.DAILY_GENERATION] # noqa: E501
|
||||
db = {}
|
||||
@@ -152,6 +225,8 @@ class ClrAtMidnight:
|
||||
|
||||
|
||||
class Infos:
|
||||
__slots__ = ('db', 'tracer', )
|
||||
|
||||
LIGHTNING = 'mdi:lightning-bolt'
|
||||
COUNTER = 'mdi:counter'
|
||||
GAUGE = 'mdi:gauge'
|
||||
@@ -201,6 +276,7 @@ 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
|
||||
__work_mode_val_tpl = "{%set mode = ['Normal-Mode', 'Aging-Mode', 'ATE-Mode', 'Shielding GFDI', 'DTU-Mode'] %}{{mode[value_json['Work_Mode']|int(0)]|default(value_json['Work_Mode'])}}" # noqa: E501
|
||||
__status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
|
||||
__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 = '''
|
||||
@@ -215,6 +291,121 @@ class Infos:
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_alarm_val_tpl = '''
|
||||
{% if 'Inverter_Alarm' in value_json and
|
||||
value_json['Inverter_Alarm'] != None %}
|
||||
{% set val_int = value_json['Inverter_Alarm'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noAlarm'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}
|
||||
{% set result = result + 'HBridgeFault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}
|
||||
{% set result = result + 'DriVoltageFault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}
|
||||
{% set result = result + 'GFDI-Fault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}
|
||||
{% set result = result + 'OverTemp, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}
|
||||
{% set result = result + 'CommLose, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}
|
||||
{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}
|
||||
{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}
|
||||
{% set result = result + 'EEPROM-Fault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}
|
||||
{% set result = result + 'NoUtility, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}
|
||||
{% set result = result + 'VG_Offset, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}
|
||||
{% set result = result + 'Relais_Open, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}
|
||||
{% set result = result + 'Relais_Short, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}
|
||||
{% set result = result + 'GridVoltOverRating, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}
|
||||
{% set result = result + 'GridVoltUnderRating, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}
|
||||
{% set result = result + 'GridFreqOverRating, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}
|
||||
{% set result = result + 'GridFreqUnderRating, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_fault_val_tpl = '''
|
||||
{% if 'Inverter_Fault' in value_json and
|
||||
value_json['Inverter_Fault'] != None %}
|
||||
{% set val_int = value_json['Inverter_Fault'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noFault'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}
|
||||
{% set result = result + 'PVOV-Fault (PV OverVolt), '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}
|
||||
{% set result = result + 'PVLV-Fault (PV LowVolt), '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}
|
||||
{% set result = result + 'PV OI-Fault (PV OverCurrent), '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}
|
||||
{% set result = result + 'PV OFV-Fault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}
|
||||
{% set result = result + 'DC ShortCircuitFault, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'Bit9, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
__input_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Input_Coefficient'] != None %}{{value_json['Input_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
__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 = {
|
||||
@@ -237,6 +428,8 @@ class Infos:
|
||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': 'mdi:power', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INPUT_COEFFICIENT: {'name': ['inverter', 'Input_Coefficient'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'input_coef_', 'val_tpl': __input_coef_val_tpl, 'name': 'Input Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
@@ -250,9 +443,11 @@ class Infos:
|
||||
Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MANUFACTURER: {'name': ['inverter', 'PV6_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.BOOT_STATUS: {'name': ['inverter', 'BOOT_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DSP_STATUS: {'name': ['inverter', 'DSP_STATUS'], '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': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
|
||||
Register.CLOUD_CONN_CNT: {'name': ['proxy', 'Cloud_Conn_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'cloud_conn_count_', 'fmt': FMT_INT, 'name': 'Active Cloud 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
|
||||
@@ -267,22 +462,12 @@ class Infos:
|
||||
# 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
|
||||
Register.EVENT_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_411: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_413: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_414: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_ALARM: {'name': ['events', 'Inverter_Alarm'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_alarm_', 'name': 'Inverter Alarm', 'val_tpl': __inv_alarm_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_FAULT: {'name': ['events', 'Inverter_Fault'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_fault_', 'name': 'Inverter Fault', 'val_tpl': __inv_fault_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_BF1: {'name': ['events', 'Inverter_Bitfield_1'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_BF2: {'name': ['events', 'Inverter_bitfield_2'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# grid measures:
|
||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -292,6 +477,8 @@ class Infos:
|
||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
||||
Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# input measures:
|
||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -341,6 +528,14 @@ class Infos:
|
||||
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
|
||||
Register.SSID: {'name': ['controller', 'WiFi_SSID'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.OUTPUT_SHUTDOWN: {'name': ['other', 'Output_Shutdown'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.RATED_LEVEL: {'name': ['other', 'Rated_Level'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.GRID_VOLT_CAL_COEF: {'name': ['other', 'Grid_Volt_Cal_Coef'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PROD_COMPL_TYPE: {'name': ['other', 'Prod_Compliance_Type'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -650,6 +845,8 @@ class Infos:
|
||||
|
||||
def get_db_value(self, id: Register, not_found_result: any = None):
|
||||
'''get database value'''
|
||||
if id not in self.info_defs:
|
||||
return not_found_result
|
||||
row = self.info_defs[id]
|
||||
if isinstance(row, dict):
|
||||
keys = row['name']
|
||||
|
||||
@@ -6,23 +6,15 @@ import json
|
||||
import gc
|
||||
from aiomqtt import MqttCodeError
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from ipaddress import ip_address
|
||||
|
||||
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
|
||||
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 cnf.config import Config
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
@@ -31,12 +23,16 @@ class InverterBase(InverterIfc, Proxy):
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
config_id: str, prot_class,
|
||||
client_mode: bool = False):
|
||||
client_mode: bool = False,
|
||||
remote_prot_class=None):
|
||||
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
|
||||
if remote_prot_class:
|
||||
self.prot_class = remote_prot_class
|
||||
else:
|
||||
self.prot_class = prot_class
|
||||
self.__ha_restarts = -1
|
||||
self.remote = StreamPtr(None)
|
||||
ifc = AsyncStreamServer(reader, writer,
|
||||
@@ -45,7 +41,7 @@ class InverterBase(InverterIfc, Proxy):
|
||||
self.remote)
|
||||
|
||||
self.local = StreamPtr(
|
||||
self.prot_class(self.addr, ifc, True, client_mode), ifc
|
||||
prot_class(self.addr, ifc, True, client_mode), ifc
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
@@ -106,6 +102,20 @@ class InverterBase(InverterIfc, Proxy):
|
||||
logging.info(f'[{stream.node_id}] Connect to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
r_addr = writer.get_extra_info('peername')
|
||||
if r_addr is not None:
|
||||
(ip, _) = r_addr
|
||||
if ip_address(ip).is_private:
|
||||
logging.error(
|
||||
f"""resolve {host} to {ip}, which is a private IP!
|
||||
\u001B[31m Check your DNS settings and use a public DNS resolver!
|
||||
|
||||
To prevent a possible loop, forwarding to local IP addresses is
|
||||
not supported and is deactivated for subsequent connections
|
||||
\u001B[0m
|
||||
""")
|
||||
Config.act_config[self.config_id]['enabled'] = False
|
||||
|
||||
ifc = AsyncStreamClient(
|
||||
reader, writer, self.local, self.__del_remote)
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ 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
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ formatter=console_formatter
|
||||
class=handlers.TimedRotatingFileHandler
|
||||
level=INFO
|
||||
formatter=file_formatter
|
||||
args=('log/proxy.log', when:='midnight')
|
||||
args=(handlers.log_path + 'proxy.log', when:='midnight', backupCount:=handlers.log_backups)
|
||||
|
||||
[handler_file_handler_name2]
|
||||
class=handlers.TimedRotatingFileHandler
|
||||
level=NOTSET
|
||||
formatter=file_formatter
|
||||
args=('log/trace.log', when:='midnight')
|
||||
args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups)
|
||||
|
||||
[formatter_console_formatter]
|
||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
||||
|
||||
@@ -3,15 +3,11 @@ import weakref
|
||||
from typing import Callable
|
||||
from enum import Enum
|
||||
|
||||
|
||||
if __name__ == "app.src.messages":
|
||||
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 protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from async_ifc import AsyncIfc
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -89,26 +85,38 @@ class Message(ProtocolIfc):
|
||||
'''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'''
|
||||
MB_START_TIMEOUT = 40
|
||||
'''start delay for Modbus polling in server mode'''
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
'''regular Modbus polling time in server mode'''
|
||||
|
||||
def __init__(self, server_side: bool, send_modbus_cb:
|
||||
Callable[[bytes, int, str], None], mb_timeout: int):
|
||||
def __init__(self, node_id, ifc: "AsyncIfc", server_side: bool,
|
||||
send_modbus_cb: Callable[[bytes, int, str], None],
|
||||
mb_timeout: int):
|
||||
self._registry.append(weakref.ref(self))
|
||||
|
||||
self.server_side = server_side
|
||||
self.ifc = ifc
|
||||
self.node_id = node_id
|
||||
if server_side:
|
||||
self.mb = Modbus(send_modbus_cb, mb_timeout)
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
else:
|
||||
self.mb = None
|
||||
|
||||
self.mb_timer = None
|
||||
self.header_valid = False
|
||||
self.header_len = 0
|
||||
self.data_len = 0
|
||||
self.unique_id = 0
|
||||
self._node_id = ''
|
||||
self.sug_area = ''
|
||||
self.new_data = {}
|
||||
self.state = State.init
|
||||
self.shutdown_started = False
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
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
|
||||
|
||||
@property
|
||||
def node_id(self):
|
||||
@@ -152,10 +160,35 @@ class Message(ProtocolIfc):
|
||||
to = self.MAX_DEF_IDLE_TIME
|
||||
return to
|
||||
|
||||
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)
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
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
|
||||
self.mb_timer.close()
|
||||
self.state = State.closed
|
||||
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
|
||||
|
||||
if self.mb:
|
||||
self.mb.close()
|
||||
self.mb = None
|
||||
|
||||
@@ -16,10 +16,7 @@ import logging
|
||||
import asyncio
|
||||
from typing import Generator, Callable
|
||||
|
||||
if __name__ == "app.src.modbus":
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from infos import Register
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
@@ -40,15 +37,30 @@ class Modbus():
|
||||
|
||||
__crc_tab = []
|
||||
mb_reg_mapping = {
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||
0x2006: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2008: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
|
||||
0x2009: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x200a: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x2010: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
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
|
||||
0x3001: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3002: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x3003: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x3004: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x3005: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3006: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # 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
|
||||
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
|
||||
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
|
||||
# 0x300d
|
||||
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
@@ -74,6 +86,7 @@ class Modbus():
|
||||
0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
# 0x302a
|
||||
}
|
||||
|
||||
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
||||
@@ -117,6 +130,9 @@ class Modbus():
|
||||
while not self.que.empty():
|
||||
self.que.get_nowait()
|
||||
|
||||
def set_node_id(self, node_id: str):
|
||||
self.node_id = node_id
|
||||
|
||||
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||
log_lvl=logging.DEBUG) -> None:
|
||||
"""Build MODBUS RTU request frame and add it to the tx queue
|
||||
@@ -160,14 +176,13 @@ class Modbus():
|
||||
|
||||
return True
|
||||
|
||||
def recv_resp(self, info_db, buf: bytes, node_id: str) -> \
|
||||
def recv_resp(self, info_db, buf: bytes) -> \
|
||||
Generator[tuple[str, bool, int | float | str], None, None]:
|
||||
"""Generator which check and parse a received MODBUS response.
|
||||
|
||||
Keyword arguments:
|
||||
info_db: database for info lockups
|
||||
buf: received Modbus RTU response frame
|
||||
node_id: string for logging which identifies the slave
|
||||
|
||||
Returns on error and set Self.err to:
|
||||
1: CRC error
|
||||
@@ -177,7 +192,6 @@ class Modbus():
|
||||
5: No MODBUS request pending
|
||||
"""
|
||||
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||
self.node_id = node_id
|
||||
|
||||
fcode = buf[1]
|
||||
data_available = self.last_addr == self.INV_ADDR and \
|
||||
@@ -228,17 +242,6 @@ class Modbus():
|
||||
|
||||
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):
|
||||
@@ -248,7 +251,7 @@ class Modbus():
|
||||
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)
|
||||
result = Fmt.get_value(buf, 3+2*i, row)
|
||||
name, update = info_db.update_db(keys, must_incr,
|
||||
result)
|
||||
yield keys[0], update, result
|
||||
|
||||
@@ -2,14 +2,9 @@ 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
|
||||
from cnf.config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
@@ -54,18 +49,20 @@ class ModbusTcp():
|
||||
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
|
||||
logger.info(f"'client_mode' for snr: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501
|
||||
loop.create_task(self.modbus_loop(client['host'],
|
||||
client['port'],
|
||||
inv['monitor_sn']))
|
||||
inv['monitor_sn'],
|
||||
client['forward']))
|
||||
|
||||
async def modbus_loop(self, host, port, snr: int) -> None:
|
||||
async def modbus_loop(self, host, port,
|
||||
snr: int, forward: bool) -> 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.send_start_cmd(snr, host, forward)
|
||||
await stream.ifc.loop()
|
||||
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||
f'Connection closed - Shutdown: '
|
||||
|
||||
@@ -2,16 +2,11 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
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
|
||||
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from cnf.config import Config
|
||||
from singleton import Singleton
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
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
|
||||
from async_ifc import AsyncIfc
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
|
||||
class ProtocolIfc(metaclass=AbstractIterMeta):
|
||||
|
||||
@@ -2,14 +2,9 @@ import asyncio
|
||||
import logging
|
||||
import json
|
||||
|
||||
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
|
||||
from cnf.config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import signal
|
||||
import os
|
||||
import argparse
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
@@ -10,7 +12,10 @@ 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 cnf.config import Config
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
@@ -77,7 +82,7 @@ async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
||||
await inv.local.ifc.server_loop()
|
||||
|
||||
|
||||
async def handle_shutdown(web_task):
|
||||
async def handle_shutdown(loop, web_task):
|
||||
'''Close all TCP connections and stop the event loop'''
|
||||
|
||||
logging.info('Shutdown due to SIGTERM')
|
||||
@@ -116,6 +121,8 @@ def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
corresponding logging.LOG_LEVEL'''
|
||||
log_level = os.getenv('LOG_LVL', 'INFO')
|
||||
logging.info(f"LOG_LVL : {log_level}")
|
||||
|
||||
if log_level == 'DEBUG':
|
||||
log_level = logging.DEBUG
|
||||
elif log_level == 'WARN':
|
||||
@@ -125,18 +132,46 @@ def get_log_level() -> int:
|
||||
return log_level
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main(): # pragma: no cover
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-c', '--config_path', type=str,
|
||||
default='./config/',
|
||||
help='set path for the configuration files')
|
||||
parser.add_argument('-j', '--json_config', type=str,
|
||||
help='read user config from json-file')
|
||||
parser.add_argument('-t', '--toml_config', type=str,
|
||||
help='read user config from toml-file')
|
||||
parser.add_argument('-l', '--log_path', type=str,
|
||||
default='./log/',
|
||||
help='set path for the logging files')
|
||||
parser.add_argument('-b', '--log_backups', type=int,
|
||||
default=0,
|
||||
help='set max number of daily log-files')
|
||||
args = parser.parse_args()
|
||||
#
|
||||
# Setup our daily, rotating logger
|
||||
#
|
||||
serv_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||
version = os.getenv('VERSION', 'unknown')
|
||||
|
||||
setattr(logging.handlers, "log_path", args.log_path)
|
||||
setattr(logging.handlers, "log_backups", args.log_backups)
|
||||
|
||||
logging.config.fileConfig('logging.ini')
|
||||
logging.info(f'Server "{serv_name} - {version}" will be started')
|
||||
logging.info(f'current dir: {os.getcwd()}')
|
||||
logging.info(f"config_path: {args.config_path}")
|
||||
logging.info(f"json_config: {args.json_config}")
|
||||
logging.info(f"toml_config: {args.toml_config}")
|
||||
logging.info(f"log_path: {args.log_path}")
|
||||
if args.log_backups == 0:
|
||||
logging.info("log_backups: unlimited")
|
||||
else:
|
||||
logging.info(f"log_backups: {args.log_backups} days")
|
||||
log_level = get_log_level()
|
||||
logging.info('******')
|
||||
|
||||
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
||||
log_level = get_log_level()
|
||||
logging.getLogger().setLevel(log_level)
|
||||
logging.getLogger('msg').setLevel(log_level)
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
@@ -149,9 +184,20 @@ if __name__ == "__main__":
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# read config file
|
||||
ConfigErr = Config.class_init()
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
ConfigReadEnv()
|
||||
ConfigReadJson(args.config_path + "config.json")
|
||||
ConfigReadToml(args.config_path + "config.toml")
|
||||
ConfigReadJson(args.json_config)
|
||||
ConfigReadToml(args.toml_config)
|
||||
config_err = Config.get_error()
|
||||
|
||||
if config_err is not None:
|
||||
logging.info(f'config_err: {config_err}')
|
||||
return
|
||||
|
||||
logging.info('******')
|
||||
|
||||
Proxy.class_init()
|
||||
Schedule.start()
|
||||
ModbusTcp(loop)
|
||||
@@ -162,6 +208,7 @@ if __name__ == "__main__":
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
for inv_class, port in [(InverterG3, 5005), (InverterG3P, 10000)]:
|
||||
logging.info(f'listen on port: {port} for inverters')
|
||||
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
|
||||
handle_client(r, w, i),
|
||||
'0.0.0.0', port))
|
||||
@@ -174,12 +221,12 @@ if __name__ == "__main__":
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
lambda loop=loop: asyncio.create_task(
|
||||
handle_shutdown(web_task)))
|
||||
handle_shutdown(loop, web_task)))
|
||||
|
||||
loop.set_debug(log_level == logging.DEBUG)
|
||||
try:
|
||||
if ConfigErr is None:
|
||||
proxy_is_up = True
|
||||
global proxy_is_up
|
||||
proxy_is_up = True
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -189,3 +236,7 @@ if __name__ == "__main__":
|
||||
logging.debug('Close event loop')
|
||||
loop.close()
|
||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
|
||||
@@ -4,12 +4,13 @@ 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
|
||||
from infos import Infos
|
||||
from inverter_base import InverterBase
|
||||
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from messages import Message
|
||||
|
||||
from test_modbus_tcp import FakeReader, FakeWriter
|
||||
from test_inverter_base import config_conn, patch_open_connection
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -17,10 +18,13 @@ pytest_plugins = ('pytest_asyncio',)
|
||||
Infos.static_init()
|
||||
|
||||
class FakeProto(Message):
|
||||
def __init__(self, server_side):
|
||||
super().__init__(server_side, None, 10)
|
||||
def __init__(self, ifc, server_side):
|
||||
super().__init__('G3F', ifc, server_side, None, 10)
|
||||
self.conn_no = 0
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
pass # empty callback
|
||||
|
||||
def fake_reader_fwd():
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
@@ -337,6 +341,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False):
|
||||
elif test_type == TestType.FWD_RUNTIME_ERROR_NO_STREAM:
|
||||
remote.stream = None
|
||||
raise RuntimeError("Peer closed")
|
||||
return True
|
||||
|
||||
def close():
|
||||
return
|
||||
@@ -349,7 +354,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False):
|
||||
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)
|
||||
remote.stream = FakeProto(remote.ifc, False)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward():
|
||||
@@ -530,3 +535,39 @@ async def test_forward_runtime_error3():
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_resp():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb)
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_resp2():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb)
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# test_with_pytest.py
|
||||
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
from byte_fifo import ByteFifo
|
||||
|
||||
def test_fifo():
|
||||
read = ByteFifo()
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
# test_with_pytest.py
|
||||
import tomllib
|
||||
import pytest
|
||||
import json
|
||||
from mock import patch
|
||||
from schema import SchemaMissingKeyError
|
||||
from app.src.config import Config
|
||||
from cnf.config import Config, ConfigIfc
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
class TstConfig(Config):
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
return self.buf.rd.copy()
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="rb"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
class TstConfig(ConfigIfc):
|
||||
|
||||
@classmethod
|
||||
def set(cls, cnf):
|
||||
def __init__(cls, cnf):
|
||||
cls.act_config = cnf
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict:
|
||||
def add_config(cls) -> dict:
|
||||
return cls.act_config
|
||||
|
||||
|
||||
@@ -22,82 +62,9 @@ def test_empty_config():
|
||||
except SchemaMissingKeyError:
|
||||
pass
|
||||
|
||||
def test_default_config():
|
||||
with open("app/config/default_config.toml", "rb") as f:
|
||||
cnf = tomllib.load(f)
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': 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},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'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)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': 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},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {}}
|
||||
}
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}}}
|
||||
|
||||
def test_read_empty():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
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'},
|
||||
@pytest.fixture
|
||||
def ConfigDefault():
|
||||
return {'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': {
|
||||
@@ -128,27 +95,150 @@ def test_read_empty():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigComplete():
|
||||
return {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}
|
||||
}
|
||||
},
|
||||
'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': 'PV-Garage/',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'suggested_area': 'Garage',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'pv3': {'manufacturer': 'man3',
|
||||
'type': 'type3'},
|
||||
'pv4': {'manufacturer': 'man4',
|
||||
'type': 'type4'},
|
||||
'suggested_area': 'Garage2',
|
||||
'sensor_list': 688}
|
||||
}
|
||||
}
|
||||
|
||||
def test_default_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
validated = Config.def_config
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'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(ConfigComplete):
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': ['AT+SUPDATE']}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': False,
|
||||
'R170000000000001': {'modbus_polling': False, 'node_id': 'PV-Garage/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}}}}
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == ConfigComplete
|
||||
|
||||
def test_read_empty(ConfigDefault):
|
||||
test_buffer.rd = ""
|
||||
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_no_file():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('')
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
err = Config.get_error()
|
||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == None
|
||||
|
||||
def test_read_cnf1():
|
||||
cnf = {'solarman' : {'enabled': False}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
def test_no_file2():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
|
||||
def test_invalid_filename():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml(None)
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_read_cnf1():
|
||||
test_buffer.rd = "solarman.enabled = false"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -180,18 +270,22 @@ def test_read_cnf1():
|
||||
}
|
||||
}
|
||||
}
|
||||
cnf = TstConfig.get('solarman')
|
||||
cnf = Config.get('solarman')
|
||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf2():
|
||||
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.enabled = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -223,22 +317,30 @@ def test_read_cnf2():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf3():
|
||||
cnf = {'solarman' : {'port': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'solarman': {'port': 'FALSE'}}
|
||||
def test_read_cnf3(ConfigDefault):
|
||||
test_buffer.rd = "solarman.port = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_read_cnf4():
|
||||
cnf = {'solarman' : {'port': 5000}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 5000"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -270,16 +372,22 @@ def test_read_cnf4():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf5():
|
||||
cnf = {'solarman' : {'port': 1023}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 1023"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
def test_read_cnf6():
|
||||
cnf = {'solarman' : {'port': 65536}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 65536"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
53
app/tests/test_config_read_env.py
Normal file
53
app/tests/test_config_read_env.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import os
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
|
||||
def patch_getenv():
|
||||
def new_getenv(key: str, defval=None):
|
||||
"""Get an environment variable, return None if it doesn't exist.
|
||||
The optional second argument can specify an alternate default. key,
|
||||
default and the result are str."""
|
||||
if key == 'MQTT_PASSWORD':
|
||||
return 'passwd'
|
||||
elif key == 'MQTT_PORT':
|
||||
return 1234
|
||||
elif key == 'MQTT_HOST':
|
||||
return ""
|
||||
return defval
|
||||
|
||||
with patch.object(os, 'getenv', new_getenv) as conn:
|
||||
yield conn
|
||||
|
||||
def test_extend_key():
|
||||
cnf_rd = ConfigReadEnv()
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt.user", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': {
|
||||
'user': 'testuser',
|
||||
},
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': 'testuser',
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "", "testuser")
|
||||
assert conf == {'': 'testuser'}
|
||||
|
||||
def test_read_env_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}
|
||||
for _ in patch_getenv():
|
||||
|
||||
ConfigReadEnv()
|
||||
assert Config.get_error() == None
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1234, 'user': None, 'passwd': 'passwd'}
|
||||
411
app/tests/test_config_read_json.py
Normal file
411
app/tests/test_config_read_json.py
Normal file
@@ -0,0 +1,411 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
from test_config import ConfigDefault, ConfigComplete
|
||||
|
||||
|
||||
class CnfIfc(ConfigReadJson):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
wr = str()
|
||||
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
print(f"Fake.read: bmode:{self.bin_mode}")
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
print(f"Fake.read: str:{self.buf.rd}")
|
||||
return self.buf.rd
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="r"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigTomlEmpty():
|
||||
return {
|
||||
'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'},
|
||||
'solarman': {
|
||||
'enabled': True,
|
||||
'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000,
|
||||
},
|
||||
'tsun': {
|
||||
'enabled': True,
|
||||
'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005,
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': False
|
||||
},
|
||||
'gen3plus': {'at_acl': {'tsun': {'allow': [], 'block': []},
|
||||
'mqtt': {'allow': [], 'block': []}}},
|
||||
}
|
||||
|
||||
|
||||
def test_no_config(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Expecting value: line 1 column 1 (char 0)'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_no_file(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_invalid_filename(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson(None)
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_cnv1():
|
||||
"""test dotted key converting"""
|
||||
tst = {
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE",
|
||||
"AT+"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {
|
||||
'block': [
|
||||
'AT+SUPDATE',
|
||||
"AT+"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv2():
|
||||
"""test a valid list with serials in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv3():
|
||||
"""test the combination of a list and a scalar in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
"inverters.allow_all": False,
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {},
|
||||
'allow_all': False,
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv4():
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": False,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": True,
|
||||
"client_mode.host": "InverterIP",
|
||||
"client_mode.port": 1234,
|
||||
"client_mode.forward": True,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": True,
|
||||
"solarman.enabled": True,
|
||||
"inverters.allow_all": False,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}}},
|
||||
'inverters': {'R170000000000001': {'modbus_polling': False,
|
||||
'node_id': 'PV-Garage/',
|
||||
'pv1': {
|
||||
'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {
|
||||
'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage'},
|
||||
'Y170000000000001': {'client_mode': {
|
||||
'host': 'InverterIP',
|
||||
'port': 1234,
|
||||
'forward': True},
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1': {
|
||||
'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {
|
||||
'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'pv3': {
|
||||
'manufacturer': 'man3',
|
||||
'type': 'type3'},
|
||||
'pv4': {
|
||||
'manufacturer': 'man4',
|
||||
'type': 'type4'},
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage2'},
|
||||
'allow_all': False},
|
||||
'solarman': {'enabled': True},
|
||||
'tsun': {'enabled': True}
|
||||
}
|
||||
|
||||
def test_cnv5():
|
||||
"""test a invalid list with missing serials"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"node_id": "PV-Garage1/",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}
|
||||
],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv6():
|
||||
"""test overwritting a value in inverters"""
|
||||
tst = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}],
|
||||
}
|
||||
tst2 = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garden/",
|
||||
}],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
conf = {}
|
||||
for key, val in tst.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, val in tst2.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garden/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_empty_config(ConfigDefault):
|
||||
test_buffer.rd = "{}" # empty json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
test_buffer.rd = """
|
||||
{
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": false,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": true,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": true,
|
||||
"solarman.enabled": true,
|
||||
"inverters.allow_all": false,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
"""
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigComplete
|
||||
@@ -2,8 +2,8 @@
|
||||
import pytest
|
||||
import json, math
|
||||
import logging
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.infos import Infos
|
||||
from infos import Register, ClrAtMidnight
|
||||
from infos import Infos, Fmt
|
||||
|
||||
def test_statistic_counter():
|
||||
i = Infos()
|
||||
@@ -17,13 +17,13 @@ def test_statistic_counter():
|
||||
assert val == None or val == 0
|
||||
|
||||
i.static_init() # initialize counter
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
|
||||
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||
assert val == 0
|
||||
|
||||
i.inc_counter('Inverter_Cnt')
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
val = i.dev_value(Register.INVERTER_CNT)
|
||||
assert val == 1
|
||||
|
||||
@@ -256,3 +256,24 @@ def test_key_obj():
|
||||
assert level == logging.DEBUG
|
||||
assert unit == 'kWh'
|
||||
assert must_incr == True
|
||||
|
||||
def test_hex4_cnv():
|
||||
tst_val = (0x12ef, )
|
||||
string = Fmt.hex4(tst_val)
|
||||
assert string == '12ef'
|
||||
val = Fmt.hex4(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
def test_mac_cnv():
|
||||
tst_val = (0x12, 0x34, 0x67, 0x89, 0xcd, 0xef)
|
||||
string = Fmt.mac(tst_val)
|
||||
assert string == '12:34:67:89:cd:ef'
|
||||
val = Fmt.mac(string, reverse=True)
|
||||
assert val == tst_val
|
||||
|
||||
def test_version_cnv():
|
||||
tst_val = (0x123f, )
|
||||
string = Fmt.version(tst_val)
|
||||
assert string == 'V1.2.3F'
|
||||
val = Fmt.version(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, json, math
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
|
||||
from infos import Register
|
||||
from gen3.infos_g3 import InfosG3, RegisterMap
|
||||
|
||||
@pytest.fixture
|
||||
def contr_data_seq(): # Get Time Request message
|
||||
@@ -421,7 +421,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||
if key == 'total' or key == 'inverter' or key == 'env':
|
||||
assert update == True
|
||||
tests +=1
|
||||
assert tests==8
|
||||
assert tests==12
|
||||
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_Status": 1, "Inverter_Temp": 23})
|
||||
@@ -435,7 +435,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||
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_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})
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": -1, "Input_Coefficient": -0.1, "Output_Coefficient": 100.0, "No_Inputs": 2})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (inv_data_seq2_zero):
|
||||
@@ -501,10 +501,10 @@ def test_new_data_types(inv_data_new):
|
||||
else:
|
||||
assert False
|
||||
|
||||
assert tests==15
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
||||
assert tests==7
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0, "DSP_STATUS": 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})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"Inverter_Alarm": 0, "Inverter_Fault": 0})
|
||||
|
||||
def test_invalid_data_type(invalid_data_seq):
|
||||
i = InfosG3()
|
||||
@@ -520,15 +520,3 @@ def test_invalid_data_type(invalid_data_seq):
|
||||
|
||||
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||
assert val == 1
|
||||
|
||||
def test_result_eval(inv_data_seq2: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
|
||||
|
||||
i = InfosG3()
|
||||
|
||||
for _, _ in i.parse (inv_data_seq2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
del RegisterMap.map[0x00000514]['eval'] # remove eval
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
# test_with_pytest.py
|
||||
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
|
||||
from infos import Register
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from gen3plus.infos_g3p import RegisterMap
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def str_test_ip():
|
||||
@@ -57,6 +57,7 @@ def inverter_data(): # 0x4210 ftype: 0x01
|
||||
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||
|
||||
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
@@ -85,10 +86,21 @@ def test_parse_4110(str_test_ip, device_data: bytes):
|
||||
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": str_test_ip, "Sensor_List": "02b0"},
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0", "WiFi_SSID": "Allius-Home"},
|
||||
'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_build_4110(str_test_ip, device_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
for key, update in i.parse (device_data, 0x41, 2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(device_data), 0x41, 2)
|
||||
for i in range(11, 20):
|
||||
build_msg[i] = device_data[i]
|
||||
assert device_data == build_msg
|
||||
|
||||
def test_parse_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
@@ -98,16 +110,31 @@ def test_parse_4210(inverter_data: bytes):
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": 2000, "Input_Coefficient": 100.0, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Detect_Status_1": 2, "Detect_Status_2": 0, "Inverter_Temp": 14},
|
||||
"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0},
|
||||
"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},
|
||||
"pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91},
|
||||
"pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89},
|
||||
"pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}},
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36}
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36},
|
||||
"inv_unknown": {"Unknown_1": 512},
|
||||
"other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024, "Prod_Compliance_Type": 6}
|
||||
})
|
||||
|
||||
def test_build_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
for i in range(11, 31):
|
||||
build_msg[i] = inverter_data[i]
|
||||
assert inverter_data == build_msg
|
||||
|
||||
def test_build_ha_conf1():
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.static_init() # initialize counter
|
||||
@@ -256,10 +283,12 @@ def test_build_ha_conf4():
|
||||
|
||||
assert tests==1
|
||||
|
||||
def test_exception_and_eval(inverter_data: bytes):
|
||||
def test_exception_and_calc(inverter_data: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
||||
# patch table to convert temperature from °F to °C
|
||||
ofs = RegisterMap.map[0x420100d8]['offset']
|
||||
RegisterMap.map[0x420100d8]['quotient'] = 1.8
|
||||
RegisterMap.map[0x420100d8]['offset'] = -32/1.8
|
||||
# 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)
|
||||
@@ -267,16 +296,43 @@ def test_exception_and_eval(inverter_data: bytes):
|
||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
||||
|
||||
i = InfosG3P(client_mode=False)
|
||||
# i.db.clear()
|
||||
i.db.clear()
|
||||
|
||||
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
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xde] == inverter_data[32:0xde]
|
||||
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
|
||||
# remove a table entry and test parsing and building
|
||||
del RegisterMap.map[0x420100d8]['quotient']
|
||||
del RegisterMap.map[0x420100d8]['offset']
|
||||
|
||||
i.db.clear()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
|
||||
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
# test restore table
|
||||
RegisterMap.map[0x420100d8]['offset'] = ofs
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
|
||||
# test orginial table
|
||||
i.db.clear()
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:-1] == inverter_data[32:-1]
|
||||
|
||||
@@ -5,14 +5,14 @@ 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 infos import Infos
|
||||
from cnf.config import Config
|
||||
from gen3.talent import Talent
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from async_stream import AsyncStream, AsyncStreamClient
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -54,11 +54,12 @@ class FakeReader():
|
||||
|
||||
|
||||
class FakeWriter():
|
||||
peer = ('47.1.2.3', 10000)
|
||||
def write(self, buf: bytes):
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return 'remote.intern'
|
||||
return self.peer
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -69,13 +70,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -85,9 +86,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -241,6 +242,118 @@ async def test_remote_conn(config_conn, patch_open_connection):
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn_to_private(config_conn, patch_open_connection):
|
||||
'''check DNS resolving of the TSUN FQDN to a local address'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = ("192.168.0.1", 10000)
|
||||
|
||||
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 not Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
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_to_loopback(config_conn, patch_open_connection):
|
||||
'''check DNS resolving of the TSUN FQDN to the loopback address'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = ("127.0.0.1", 10000)
|
||||
|
||||
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 not Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
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_to_None(config_conn, patch_open_connection):
|
||||
'''check if get_extra_info() return None in case of an error'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = None
|
||||
|
||||
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 Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
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_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
|
||||
_ = config_conn
|
||||
|
||||
@@ -5,15 +5,15 @@ 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 infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from async_stream import AsyncStream
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -59,7 +59,7 @@ class FakeWriter():
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return 'remote.intern'
|
||||
return ('47.1.2.3', 10000)
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -70,13 +70,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -86,9 +86,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -144,14 +144,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
test = MockType.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
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
@@ -4,14 +4,14 @@ 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 infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -58,7 +58,7 @@ class FakeWriter():
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return 'remote.intern'
|
||||
return ('47.1.2.3', 10000)
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -69,13 +69,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -85,16 +85,17 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.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():
|
||||
def test_method_calls(config_conn):
|
||||
_ = config_conn
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
InverterBase._registry.clear()
|
||||
@@ -121,14 +122,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
test = MockType.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
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
@@ -144,7 +145,7 @@ async def test_mqtt_publish(config_conn, patch_open_connection):
|
||||
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._set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
@@ -171,7 +172,7 @@ async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err):
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream._set_serial_no(snr= 123344)
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
@@ -188,7 +189,7 @@ async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream._set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from infos import Infos, Register
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -77,9 +77,10 @@ def test_recv_resp_crc_err():
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
mb.set_node_id('test')
|
||||
# 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'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3'):
|
||||
call += 1
|
||||
assert mb.err == 1
|
||||
assert 0 == call
|
||||
@@ -97,10 +98,11 @@ def test_recv_resp_invalid_addr():
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
mb.set_node_id('test')
|
||||
|
||||
# check not matching response, with wrong server addr
|
||||
call = 0
|
||||
for key, update in mb.recv_resp(mb.db, b'\x02\x03\x04\x01\x2c\x00\x46\x88\xf4', 'test'):
|
||||
for key, update in mb.recv_resp(mb.db, b'\x02\x03\x04\x01\x2c\x00\x46\x88\xf4'):
|
||||
call += 1
|
||||
assert mb.err == 2
|
||||
assert 0 == call
|
||||
@@ -120,7 +122,8 @@ def test_recv_recv_fcode():
|
||||
|
||||
# check not matching response, with wrong function code
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 3
|
||||
@@ -142,7 +145,8 @@ def test_recv_resp_len():
|
||||
|
||||
# check not matching response, with wrong data length
|
||||
call = 0
|
||||
for key, update, _ in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, _ in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 4
|
||||
@@ -161,7 +165,8 @@ def test_recv_unexpect_resp():
|
||||
|
||||
# check unexpected response, which must be dropped
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 5
|
||||
@@ -177,8 +182,9 @@ def test_parse_resp():
|
||||
assert mb.req_pend
|
||||
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
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'):
|
||||
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'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -226,8 +232,9 @@ def test_queue2():
|
||||
assert mb.send_calls == 1
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
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'):
|
||||
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'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -245,14 +252,14 @@ def test_queue2():
|
||||
assert mb.send_calls == 2
|
||||
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'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b'):
|
||||
pass # call generator mb.recv_resp()
|
||||
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.send_calls == 3
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
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'):
|
||||
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'):
|
||||
call += 1
|
||||
assert 0 == mb.err
|
||||
assert 5 == call
|
||||
@@ -276,8 +283,9 @@ def test_queue3():
|
||||
assert mb.recv_responses == 0
|
||||
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
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'):
|
||||
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'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -296,7 +304,7 @@ def test_queue3():
|
||||
assert mb.send_calls == 2
|
||||
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'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b'):
|
||||
pass # no code in loop is OK; calling the generator is the purpose
|
||||
assert 0 == mb.err
|
||||
assert mb.recv_responses == 2
|
||||
@@ -305,7 +313,7 @@ def test_queue3():
|
||||
assert mb.send_calls == 3
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
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'):
|
||||
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'):
|
||||
call += 1
|
||||
assert 0 == mb.err
|
||||
assert mb.recv_responses == 2
|
||||
@@ -373,7 +381,8 @@ def test_recv_unknown_data():
|
||||
|
||||
# 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\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
assert mb.err == 0
|
||||
assert 0 == call
|
||||
|
||||
@@ -5,14 +5,14 @@ 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
|
||||
from singleton import Singleton
|
||||
from cnf.config import Config
|
||||
from infos import Infos
|
||||
from mqtt import Mqtt
|
||||
from inverter_base import InverterBase
|
||||
from messages import Message, State
|
||||
from proxy import Proxy
|
||||
from modbus_tcp import ModbusConn, ModbusTcp
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -52,6 +52,10 @@ def config_conn(test_hostname, test_port):
|
||||
'proxy_node_id': 'test_1',
|
||||
'proxy_unique_id': ''
|
||||
},
|
||||
'solarman':{
|
||||
'host': 'access1.solarmanpv.com',
|
||||
'port': 10000
|
||||
},
|
||||
'inverters':{
|
||||
'allow_all': True,
|
||||
"R170000000000001":{
|
||||
@@ -65,7 +69,8 @@ def config_conn(test_hostname, test_port):
|
||||
'sensor_list': 0x2b0,
|
||||
'client_mode':{
|
||||
'host': '192.168.0.1',
|
||||
'port': 8899
|
||||
'port': 8899,
|
||||
'forward': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ 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
|
||||
from async_stream import AsyncIfcImpl
|
||||
from singleton import Singleton
|
||||
from mqtt import Mqtt
|
||||
from modbus import Modbus
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
|
||||
NO_MOSQUITTO_TEST = False
|
||||
'''disable all tests with connections to test.mosquitto.org'''
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -69,23 +71,79 @@ def spy_modbus_cmd_client():
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server.
|
||||
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
|
||||
all test cases which depends on the test.mosquitto.org server
|
||||
"""
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import threading
|
||||
|
||||
c = mqtt.Client()
|
||||
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
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)
|
||||
if not on_connect.wait(3):
|
||||
NO_MOSQUITTO_TEST = True # skip all mosquitto tests
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
finally:
|
||||
c.loop_stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = 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_ha_reconnect(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = 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_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
@@ -110,29 +168,6 @@ async def test_mqtt_no_config(config_no_conn):
|
||||
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
|
||||
@@ -209,26 +244,6 @@ async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
||||
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'''
|
||||
|
||||
@@ -5,11 +5,11 @@ 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
|
||||
from singleton import Singleton
|
||||
from proxy import Proxy
|
||||
from mqtt import Mqtt
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
24
app/tests/test_server.py
Normal file
24
app/tests/test_server.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import get_log_level
|
||||
|
||||
def test_get_log_level():
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': ''}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.DEBUG
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.WARNING
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
@@ -1,16 +1,16 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from app.src.singleton import Singleton
|
||||
from singleton import Singleton
|
||||
|
||||
class Test(metaclass=Singleton):
|
||||
class Example(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass # is a dummy test class
|
||||
|
||||
def test_singleton_metaclass():
|
||||
Singleton._instances.clear()
|
||||
a = Test()
|
||||
a = Example()
|
||||
assert 1 == len(Singleton._instances)
|
||||
b = Test()
|
||||
b = Example()
|
||||
assert 1 == len(Singleton._instances)
|
||||
assert a is b
|
||||
del a
|
||||
|
||||
@@ -5,12 +5,12 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
from math import isclose
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State, Message
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State, Message
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -37,6 +37,9 @@ class FakeIfc(AsyncIfcImpl):
|
||||
super().__init__()
|
||||
self.remote = StreamPtr(None)
|
||||
|
||||
async def create_remote(self):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
class MemoryStream(SolarmanV5):
|
||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||
_ifc = FakeIfc()
|
||||
@@ -109,7 +112,7 @@ class MemoryStream(SolarmanV5):
|
||||
c.ifc.remote.stream = self
|
||||
return c
|
||||
|
||||
def _SolarmanV5__flush_recv_msg(self) -> None:
|
||||
def _SolarmanBase__flush_recv_msg(self) -> None:
|
||||
self.msg_recvd.append(
|
||||
{
|
||||
'control': self.control,
|
||||
@@ -117,7 +120,7 @@ class MemoryStream(SolarmanV5):
|
||||
'data_len': self.data_len
|
||||
}
|
||||
)
|
||||
super()._SolarmanV5__flush_recv_msg()
|
||||
super()._SolarmanBase__flush_recv_msg()
|
||||
self.msg_count += 1
|
||||
|
||||
|
||||
@@ -1102,7 +1105,7 @@ def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg
|
||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||
|
||||
m.seq.server_side = False # simulate forawding to TSUN cloud
|
||||
m._update_header(m.ifc.fwd_fifo.peek())
|
||||
m._SolarmanBase__update_header(m.ifc.fwd_fifo.peek())
|
||||
assert str(m.seq) == '0d:0e' # value after forwarding indication
|
||||
assert m.ifc.fwd_fifo.get()==sync_start_fwd_msg
|
||||
|
||||
@@ -1768,7 +1771,7 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
||||
assert m.no_forwarding == False
|
||||
assert m.mb_timer.tim == None
|
||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||
await m.send_start_cmd(get_sn_int(), str_test_ip, m.mb_first_timeout)
|
||||
await m.send_start_cmd(get_sn_int(), str_test_ip, False, m.mb_first_timeout)
|
||||
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15')
|
||||
assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
|
||||
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
|
||||
@@ -1803,3 +1806,30 @@ def test_timeout(config_tsun_inv1):
|
||||
assert SolarmanV5.MAX_DEF_IDLE_TIME == m._timeout()
|
||||
m.state = State.closed
|
||||
m.close()
|
||||
|
||||
def test_fnc_dispatch():
|
||||
def msg():
|
||||
return
|
||||
|
||||
_ = config_tsun_inv1
|
||||
m = MemoryStream(b'')
|
||||
m.switch[1] = msg
|
||||
m.switch[2] = "msg"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(1)
|
||||
assert _obj == msg
|
||||
assert _str == "'msg'"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(2)
|
||||
assert _obj == m.msg_unknown
|
||||
assert _str == "'msg'"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(3)
|
||||
assert _obj == m.msg_unknown
|
||||
assert _str == "'msg_unknown'"
|
||||
|
||||
def test_timestamp():
|
||||
m = MemoryStream(b'')
|
||||
ts = m._timestamp()
|
||||
ts_emu = m._emu_timestamp()
|
||||
assert ts == ts_emu + 24*60*60
|
||||
233
app/tests/test_solarman_emu.py
Normal file
233
app/tests/test_solarman_emu.py
Normal file
@@ -0,0 +1,233 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
from infos import Infos, Register
|
||||
|
||||
from test_solarman import FakeIfc, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
|
||||
from test_infos_g3p import str_test_ip, bytes_test_ip
|
||||
|
||||
timestamp = 0x3224c8bc
|
||||
|
||||
class InvStream(MemoryStream):
|
||||
def __init__(self, msg=b''):
|
||||
super().__init__(msg)
|
||||
|
||||
def _emu_timestamp(self):
|
||||
return timestamp
|
||||
|
||||
class CldStream(SolarmanEmu):
|
||||
def __init__(self, inv: InvStream):
|
||||
_ifc = FakeIfc()
|
||||
_ifc.remote.stream = inv
|
||||
super().__init__(('test.local', 1234), _ifc, server_side=False, client_mode=False)
|
||||
self.__msg = b''
|
||||
self.__msg_len = 0
|
||||
self.__offs = 0
|
||||
self.msg_count = 0
|
||||
self.msg_recvd = []
|
||||
|
||||
def _emu_timestamp(self):
|
||||
return timestamp
|
||||
|
||||
def append_msg(self, msg):
|
||||
self.__msg += msg
|
||||
self.__msg_len += len(msg)
|
||||
|
||||
def _read(self) -> int:
|
||||
copied_bytes = 0
|
||||
try:
|
||||
if (self.__offs < self.__msg_len):
|
||||
self.ifc.rx_fifo += self.__msg[self.__offs:]
|
||||
copied_bytes = self.__msg_len - self.__offs
|
||||
self.__offs = self.__msg_len
|
||||
except Exception:
|
||||
pass # ignore exceptions here
|
||||
return copied_bytes
|
||||
|
||||
def _SolarmanBase__flush_recv_msg(self) -> None:
|
||||
self.msg_recvd.append(
|
||||
{
|
||||
'control': self.control,
|
||||
'seq': str(self.seq),
|
||||
'data_len': self.data_len
|
||||
}
|
||||
)
|
||||
super()._SolarmanBase__flush_recv_msg()
|
||||
self.msg_count += 1
|
||||
|
||||
@pytest.fixture
|
||||
def device_ind_msg(bytes_test_ip): # 0x4110
|
||||
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xbc\xc8\x24\x32'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + bytes_test_ip
|
||||
msg += b'\x0f\x00\x01\xb0'
|
||||
msg += b'\x02\x0f\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_ind_msg(): # 0x4210
|
||||
msg = b'\xa5\x99\x01\x10\x42\x00\x01' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||
msg += b'\x24\x32\x3c\x00\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||
msg += b'\x59\x31\x37\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x31'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\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\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||
msg += b'\x00\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_rsp_msg(): # 0x1210
|
||||
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += b'\x3c\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def heartbeat_ind():
|
||||
msg = b'\xa5\x01\x00\x10G\x00\x01\x00\x00\x00\x00\x00Y\x15'
|
||||
return msg
|
||||
|
||||
def test_emu_init_close():
|
||||
# received a message with wrong start byte plus an valid message
|
||||
# the complete receive buffer must be cleared to
|
||||
# find the next valid message
|
||||
inv = InvStream()
|
||||
cld = CldStream(inv)
|
||||
cld.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emu_start(config_tsun_inv1, msg_modbus_rsp, str_test_ip, device_ind_msg):
|
||||
_ = config_tsun_inv1
|
||||
assert asyncio.get_running_loop()
|
||||
inv = InvStream(msg_modbus_rsp)
|
||||
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, True, inv.mb_first_timeout)
|
||||
inv.read() # read complete msg, and dispatch msg
|
||||
assert not inv.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert inv.msg_count == 1
|
||||
assert inv.control == 0x1510
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld.ifc.update_header_cb(inv.ifc.fwd_fifo.peek())
|
||||
assert inv.ifc.fwd_fifo.peek() == device_ind_msg
|
||||
cld.close()
|
||||
|
||||
def test_snd_hb(config_tsun_inv1, heartbeat_ind):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
cld = CldStream(inv)
|
||||
|
||||
# await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
cld.send_heartbeat_cb(0)
|
||||
assert cld.ifc.tx_fifo.peek() == heartbeat_ind
|
||||
cld.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snd_inv_data(config_tsun_inv1, inverter_ind_msg, inverter_rsp_msg):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
inv.db.set_db_def_value(Register.INVERTER_STATUS, 1)
|
||||
inv.db.set_db_def_value(Register.DETECT_STATUS_1, 2)
|
||||
inv.db.set_db_def_value(Register.VERSION, 'V4.0.10')
|
||||
inv.db.set_db_def_value(Register.GRID_VOLTAGE, 224.8)
|
||||
inv.db.set_db_def_value(Register.GRID_CURRENT, 0.73)
|
||||
inv.db.set_db_def_value(Register.GRID_FREQUENCY, 50.05)
|
||||
inv.db.set_db_def_value(Register.PROD_COMPL_TYPE, 6)
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
inv.db.set_db_def_value(Register.DATA_UP_INTERVAL, 17) # set test value
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld.time_ofs = 0x33e447a0
|
||||
cld.last_sync = cld._emu_timestamp() - 60
|
||||
cld.pkt_cnt = 0x802
|
||||
assert cld.data_up_inv == 17 # check test value
|
||||
cld.data_up_inv = 0.1 # speedup test first data msg
|
||||
cld._init_new_client_conn()
|
||||
cld.data_up_inv = 0.5 # timeout for second data msg
|
||||
await asyncio.sleep(0.2)
|
||||
assert cld.ifc.tx_fifo.get() == inverter_ind_msg
|
||||
|
||||
cld.append_msg(inverter_rsp_msg)
|
||||
cld.read() # read complete msg, and dispatch msg
|
||||
|
||||
assert not cld.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert cld.msg_count == 1
|
||||
assert cld.header_len==11
|
||||
assert cld.snr == 2070233889
|
||||
assert cld.unique_id == '2070233889'
|
||||
assert cld.msg_recvd[0]['control']==0x1210
|
||||
assert cld.msg_recvd[0]['seq']=='02:02'
|
||||
assert cld.msg_recvd[0]['data_len']==0x0a
|
||||
assert '02b0' == cld.db.get_db_value(Register.SENSOR_LIST, None)
|
||||
assert cld.db.stat['proxy']['Unknown_Msg'] == 0
|
||||
|
||||
cld.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rcv_invalid(config_tsun_inv1, inverter_ind_msg, inverter_rsp_msg):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
inv.db.set_db_def_value(Register.DATA_UP_INTERVAL, 17) # set test value
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld._init_new_client_conn()
|
||||
|
||||
cld.append_msg(inverter_ind_msg)
|
||||
cld.read() # read complete msg, and dispatch msg
|
||||
|
||||
assert not cld.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert cld.msg_count == 1
|
||||
assert cld.header_len==11
|
||||
assert cld.snr == 2070233889
|
||||
assert cld.unique_id == '2070233889'
|
||||
assert cld.msg_recvd[0]['control']==0x4210
|
||||
assert cld.msg_recvd[0]['seq']=='00:01'
|
||||
assert cld.msg_recvd[0]['data_len']==0x199
|
||||
assert '02b0' == cld.db.get_db_value(Register.SENSOR_LIST, None)
|
||||
assert cld.db.stat['proxy']['Unknown_Msg'] == 1
|
||||
|
||||
|
||||
cld.close()
|
||||
@@ -1,12 +1,12 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, logging, asyncio
|
||||
from math import isclose
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3.talent import Talent, Control
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3.talent import Talent, Control
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -328,6 +328,90 @@ def msg_inverter_ind_new(): # Data indication from DSP V5.0.17
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_inverter_ind_new2(): # Data indication from DSP V5.0.17
|
||||
msg = b'\x00\x00\x04\xf4\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||
msg += b'\x01\x00\x00\x01'
|
||||
msg += b'\x86\x98\x55\xe7\x48\x00\x00\x00\xa3\x00\x00\x01\x93\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98'
|
||||
msg += b'\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\x9d\x53\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01'
|
||||
msg += b'\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\xfb\x53\x00\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd'
|
||||
msg += b'\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02'
|
||||
msg += b'\x59\x53\x00\x00\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\xbe\x53\x00\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03'
|
||||
msg += b'\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x65\xcc\xcd\x00\x00\x04'
|
||||
msg += b'\x4c\x46\x40\x0c\xcc\xcd\x00\x00\x04\xb0\x46\x42\x47\xd7\x0a\x00'
|
||||
msg += b'\x00\x05\x14\x53\x00\x35\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05'
|
||||
msg += b'\xdc\x53\x03\x20\x00\x00\x06\x40\x46\x43\xfd\x4c\xcd\x00\x00\x06'
|
||||
msg += b'\xa4\x46\x42\x18\x00\x00\x00\x00\x07\x08\x46\x40\xde\x14\x7b\x00'
|
||||
msg += b'\x00\x07\x6c\x46\x43\x84\x33\x33\x00\x00\x07\xd0\x46\x42\x1a\x00'
|
||||
msg += b'\x00\x00\x00\x08\x34\x46\x40\xda\x8f\x5c\x00\x00\x08\x98\x46\x43'
|
||||
msg += b'\x83\xb3\x33\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x09\xc4\x46\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x40\x9c'
|
||||
msg += b'\xcc\xcd\x00\x00\x0b\xb8\x46\x43\xea\xb5\xc3\x00\x00\x0c\x1c\x46'
|
||||
msg += b'\x40\x1e\xb8\x52\x00\x00\x0c\x80\x46\x43\x6d\x2b\x85\x00\x00\x0c'
|
||||
msg += b'\xe4\x46\x40\x1a\xe1\x48\x00\x00\x0d\x48\x46\x43\x68\x40\x00\x00'
|
||||
msg += b'\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00'
|
||||
msg += b'\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x01\x00'
|
||||
msg += b'\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00\x00\x11'
|
||||
msg += b'\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53'
|
||||
msg += b'\xff\xff\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x53\xff\xff'
|
||||
msg += b'\x00\x00\x13\x88\x53\xff\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00'
|
||||
msg += b'\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18'
|
||||
msg += b'\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53\x00'
|
||||
msg += b'\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00'
|
||||
msg += b'\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00'
|
||||
msg += b'\x00\x29\x04\x46\x43\x48\x00\x00\x00\x00\x29\x68\x46\x42\x48\x33'
|
||||
msg += b'\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00'
|
||||
msg += b'\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42'
|
||||
msg += b'\xce\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00'
|
||||
msg += b'\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43'
|
||||
msg += b'\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06\x00\x00\x2d\x50\x53\x00'
|
||||
msg += b'\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42'
|
||||
msg += b'\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0'
|
||||
msg += b'\x53\x00\x03\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42'
|
||||
msg += b'\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70'
|
||||
msg += b'\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00\x31\x38\x46\x42'
|
||||
msg += b'\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04'
|
||||
msg += b'\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00'
|
||||
msg += b'\x00\x33\x2c\x53\x0f\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33'
|
||||
msg += b'\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53'
|
||||
msg += b'\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53\x00\x00'
|
||||
msg += b'\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01'
|
||||
msg += b'\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82'
|
||||
msg += b'\x53\x00\x01\x00\x01\x38\x83\x53\x00\x00\x00\x00\x00\x0a\x08\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x14\x04\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x1e\x07\x00\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def msg_inverter_ind_0w(): # Data indication with 0.5W grid output
|
||||
msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||
@@ -2151,3 +2235,34 @@ def test_timeout(config_tsun_inv1):
|
||||
m.modbus_polling = False
|
||||
assert Talent.MAX_DEF_IDLE_TIME == m._timeout()
|
||||
m.close()
|
||||
|
||||
def test_msg_inv_replay(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ind_new2):
|
||||
'''replay must be ignores, since HA only supports realtime values'''
|
||||
_ = config_tsun_inv1
|
||||
|
||||
m = MemoryStream(msg_inverter_ind_0w, (0,)) # realtime msg with 0.5W Output Power
|
||||
m.append_msg(msg_inverter_ind_new2) # replay msg with 506.6W Output Power
|
||||
m.db.db['grid'] = {'Output_Power': 100}
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
m.db.stat['proxy']['Invalid_Data_Type'] = 0
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert m.msg_count == 2
|
||||
assert m.msg_recvd[0]['ctrl']==145
|
||||
assert m.msg_recvd[0]['msg_id']==4
|
||||
assert m.msg_recvd[0]['header_len']==23
|
||||
assert m.msg_recvd[0]['data_len']==1263
|
||||
assert m.msg_recvd[1]['ctrl']==145
|
||||
assert m.msg_recvd[1]['msg_id']==4
|
||||
assert m.msg_recvd[1]['header_len']==23
|
||||
assert m.msg_recvd[1]['data_len']==1249
|
||||
assert m.id_str == b"R170000000000001"
|
||||
assert m.unique_id == 'R170000000000001'
|
||||
|
||||
assert m.db.get_db_value(Register.INVERTER_STATUS) == 1
|
||||
assert isclose(m.db.db['grid']['Output_Power'], 0.5) # must be 0.5W not 100W nor 506.6W
|
||||
|
||||
m.close()
|
||||
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
||||
|
||||
2
ha_addons/.gitignore
vendored
Normal file
2
ha_addons/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.data.json
|
||||
config.yaml
|
||||
138
ha_addons/Makefile
Normal file
138
ha_addons/Makefile
Normal file
@@ -0,0 +1,138 @@
|
||||
#!make
|
||||
include ../.env
|
||||
|
||||
.PHONY: debug dev build clean rootfs repro rc rel
|
||||
|
||||
SHELL = /bin/sh
|
||||
JINJA = jinja2
|
||||
IMAGE = tsun-gen3-addon
|
||||
|
||||
|
||||
# Folders
|
||||
SRC=../app
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
ADDON_PATH = ha_addon
|
||||
DST=$(ADDON_PATH)/rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
INST_BASE=../../ha-addons/ha-addons
|
||||
|
||||
TEMPL=templates
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
export BUILD_DATE := ${shell date -Iminutes}
|
||||
VERSION := $(shell cat $(SRC)/.version)
|
||||
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||
|
||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||
|
||||
|
||||
dev debug: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
rc rel: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
|
||||
build: rootfs $(ADDON_PATH)/config.yaml repro
|
||||
|
||||
clean:
|
||||
rm -r -f $(DST_PROXY)
|
||||
rm -f $(DST)/requirements.txt
|
||||
rm -f $(ADDON_PATH)/config.yaml
|
||||
rm -f $(TEMPL)/.data.json
|
||||
|
||||
#
|
||||
# Build rootfs and config.yaml as local add-on
|
||||
# The rootfs is needed to build the add-on Dockercontainers
|
||||
#
|
||||
|
||||
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
|
||||
|
||||
STAGE=dev
|
||||
debug : STAGE=debug
|
||||
rc : STAGE=rc
|
||||
rel : STAGE=rel
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
|
||||
$(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION) --format=json $^ -o $@
|
||||
|
||||
$(TEMPL)/.data.json: FORCE
|
||||
rsync --checksum $(TEMPL)/$(STAGE)_data.json $@
|
||||
|
||||
FORCE : ;
|
||||
|
||||
|
||||
#
|
||||
# Build repository for Home Assistant Add-On
|
||||
#
|
||||
|
||||
INST=$(INST_BASE)/ha_addon_dev
|
||||
repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml rootfs/run.sh
|
||||
repro_root = CHANGELOG.md
|
||||
repro_templates = config.yaml
|
||||
repro_subdirs = translations rootfs
|
||||
repro_vers = debug dev rc rel
|
||||
|
||||
repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
|
||||
repro: $(repro_all_subdirs) $(repro_all_templates) $(repro_all_files) $(repro_root_files)
|
||||
|
||||
$(repro_all_subdirs) :
|
||||
mkdir -p $@
|
||||
|
||||
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION)-$* $< $(filter %.json,$^) -o $@
|
||||
|
||||
$(repro_root_files) : %/CHANGELOG.md : ../CHANGELOG.md
|
||||
cp $< $@
|
||||
|
||||
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_debug/% : ha_addon/%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_dev/% : ha_addon/%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_rc/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rc/% : ha_addon/%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rel/% : ha_addon/%
|
||||
cp $< $@
|
||||
|
||||
|
||||
99
ha_addons/docker-bake.hcl
Normal file
99
ha_addons/docker-bake.hcl
Normal file
@@ -0,0 +1,99 @@
|
||||
variable "IMAGE" {
|
||||
default = "tsun-gen3-addon"
|
||||
}
|
||||
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 = "ha_addon"
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
environment = "production"
|
||||
}
|
||||
attest = [
|
||||
"type =provenance,mode=max",
|
||||
"type =sbom,generator=docker/scout-sbom-indexer:latest"
|
||||
]
|
||||
annotations = [
|
||||
"index:io.hass.version=${VERSION}",
|
||||
"index:io.hass.type=addon",
|
||||
"index:io.hass.arch=armhf|aarch64|i386|amd64",
|
||||
"index:org.opencontainers.image.title=TSUN-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/ha_addons/ha_addon"
|
||||
]
|
||||
labels = {
|
||||
"io.hass.version" = "${VERSION}"
|
||||
"io.hass.type" = "addon"
|
||||
"io.hass.arch" = "armhf|aarch64|i386|amd64"
|
||||
"org.opencontainers.image.title" = "TSUN-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/ha_addonsha_addon"
|
||||
}
|
||||
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
|
||||
}
|
||||
162
ha_addons/ha_addon/DOCS.md
Normal file
162
ha_addons/ha_addon/DOCS.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Home Assistant Add-on: TSUN Proxy
|
||||
|
||||
[TSUN Proxy][tsunproxy] enables a reliable connection between TSUN third generation
|
||||
inverters and an MQTT broker. With the proxy, you can easily retrieve real-time values
|
||||
such as power, current and daily energy and integrate the inverter into Home Assistant.
|
||||
This works even without an internet connection.
|
||||
The optional connection to the TSUN Cloud can be disabled!
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
1. This Add-on requires an MQTT broker to work.
|
||||
For a typical installation, we recommend the [Mosquitto add-on][Mosquitto] running on your Home Assistant.
|
||||
|
||||
2. You need to loop the proxy into the connection between the inverter and the TSUN Cloud,
|
||||
you must adapt the DNS record within the network that your inverter uses. You need a mapping
|
||||
from logger.talent-monitoring.com and/or iot.talent-monitoring.com to the IP address of your
|
||||
Home Assistant.
|
||||
This can be done, for example, by adding a local DNS record to [AdGuard Home Add-on][AdGuard]
|
||||
(navigate to `filters` on the AdGuard panel and add an entry under `custom filtering rules`).
|
||||
|
||||
## Installation
|
||||
|
||||
The installation of this add-on is pretty straightforward and not different in
|
||||
comparison to installing any other Home Assistant add-on.
|
||||
|
||||
1. Add the repository URL to the Home Assistant add-on store
|
||||
[![Add repository on my Home Assistant][repository-badge]][repository-url]
|
||||
2. Reload the add-on store page
|
||||
3. Click the "Install" button to install the add-on.
|
||||
4. Add your inverter configuration to the add-on configuration
|
||||
5. Start the "TSUN-Proxy" add-on
|
||||
6. Check the logs of the "TSUN-Proxy" add-on to see if everything went well.
|
||||
|
||||
_Please note, the add-on is pre-configured to connect with
|
||||
Home Assistants default MQTT Broker. There is no need to configure any MQTT parameters
|
||||
if you're running an MOSQUITTO add-on. Home Assistant communication and TSUN Cloud URL
|
||||
and Ports are also pre-configured._
|
||||
|
||||
This automatic handling of the TSUN Cloud and MQTT Broker conflicts with the
|
||||
[TSUN Proxy official documentation][tsunproxy]. The official documentation
|
||||
will state `mqtt.host`, `mqtt.port`, `mqtt.user`, `mqtt.passwd` `solarman.host`,
|
||||
`solarman.port` `tsun.host`, `tsun.port` and Home Assistant options are required.
|
||||
For the add-on, however, this isn't needed.
|
||||
|
||||
## Configuration
|
||||
|
||||
**Note**: _Remember to restart the add-on when the configuration is changed._
|
||||
|
||||
Example add-on configuration after installation:
|
||||
|
||||
```yaml
|
||||
inverters:
|
||||
- serial: R17E760702080400
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: false
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
```
|
||||
|
||||
**Note**: _This is just an example, you need to replace the values with your own!_
|
||||
|
||||
Example add-on configuration for GEN3PLUS inverters:
|
||||
|
||||
```yaml
|
||||
inverters:
|
||||
- serial: Y17000000000000
|
||||
monitor_sn: 2000000000
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: true
|
||||
client_mode.host: 192.168.x.x
|
||||
client_mode.port: 8899
|
||||
client_mode.forward: true
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
pv3.manufacturer: Shinefar
|
||||
pv3.type: SF-M18/144550
|
||||
pv4.manufacturer: Shinefar
|
||||
pv4.type: SF-M18/144550
|
||||
```
|
||||
|
||||
**Note**: _This is just an example, you need to replace the values with your own!_
|
||||
|
||||
more information about the configuration can be found in the [configuration details page][configdetails].
|
||||
|
||||
## MQTT settings
|
||||
|
||||
By default, this add-on requires no `mqtt` config from the user. **This is not an error!**
|
||||
|
||||
However, you are free to set them if you want to override, however, in
|
||||
general usage, that should not be needed and is not recommended for this add-on.
|
||||
|
||||
## Changelog & Releases
|
||||
|
||||
This repository keeps a change log using [GitHub's releases][releases]
|
||||
functionality.
|
||||
|
||||
Releases are based on [Semantic Versioning][semver], and use the format
|
||||
of `MAJOR.MINOR.PATCH`. In a nutshell, the version will be incremented
|
||||
based on the following:
|
||||
|
||||
- `MAJOR`: Incompatible or major changes.
|
||||
- `MINOR`: Backwards-compatible new features and enhancements.
|
||||
- `PATCH`: Backwards-compatible bugfixes and package updates.
|
||||
|
||||
## Support
|
||||
|
||||
Got questions?
|
||||
|
||||
You have several options to get them answered:
|
||||
|
||||
- The Discussions section on [GitHub][discussions].
|
||||
- The [Home Assistant Discord chat server][discord-ha] for general Home
|
||||
Assistant discussions and questions.
|
||||
|
||||
You could also [open an issue here][issue] GitHub.
|
||||
|
||||
## Authors & contributors
|
||||
|
||||
The original setup of this repository is by [Stefan Allius][author].
|
||||
|
||||
We're very happy to receive contributions to this project! You can get started by reading [CONTRIBUTING.md][contribute].
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [BSD 3-clause License][bsd].
|
||||
|
||||
Note the aiomqtt library used is based on the paho-mqtt library, which has a dual license.
|
||||
One of the licenses is the so-called [Eclipse Distribution License v1.0.][eclipse]
|
||||
It is almost word-for-word identical to the BSD 3-clause License. The only differences are:
|
||||
|
||||
- One use of "COPYRIGHT OWNER" (EDL) instead of "COPYRIGHT HOLDER" (BSD)
|
||||
- One use of "Eclipse Foundation, Inc." (EDL) instead of "copyright holder" (BSD)
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
[tsunproxy]: https://github.com/s-allius/tsun-gen3-proxy
|
||||
[discussions]: https://github.com/s-allius/tsun-gen3-proxy/discussions
|
||||
[author]: https://github.com/s-allius
|
||||
[discord-ha]: https://discord.gg/c5DvZ4e
|
||||
[issue]: https://github.com/s-allius/tsun-gen3-proxy/issues
|
||||
[releases]: https://github.com/s-allius/tsun-gen3-proxy/releases
|
||||
[contribute]: https://github.com/s-allius/tsun-gen3-proxy/blob/main/CONTRIBUTING.md
|
||||
[semver]: http://semver.org/spec/v2.0.0.htm
|
||||
[bsd]: https://opensource.org/licenses/BSD-3-Clause
|
||||
[eclipse]: https://www.eclipse.org/org/documents/edl-v10.php
|
||||
[Mosquitto]: https://github.com/home-assistant/addons/blob/master/mosquitto/DOCS.md
|
||||
[AdGuard]: https://github.com/hassio-addons/addon-adguard-home
|
||||
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
|
||||
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fs-allius%2Fha-addons
|
||||
[configdetails]: https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml
|
||||
90
ha_addons/ha_addon/Dockerfile
Executable file
90
ha_addons/ha_addon/Dockerfile
Executable file
@@ -0,0 +1,90 @@
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# TSUN Proxy
|
||||
# Homeassistant Add-on
|
||||
#
|
||||
# based on https://github.com/s-allius/tsun-gen3-proxy/tree/main
|
||||
#
|
||||
############################################################################
|
||||
|
||||
|
||||
######################
|
||||
# 1 Build Base Image #
|
||||
######################
|
||||
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.1.0"
|
||||
# hadolint ignore=DL3006
|
||||
FROM $BUILD_FROM AS base
|
||||
|
||||
# Installiere Python, pip und virtuelle Umgebungstools
|
||||
RUN apk add --no-cache python3=3.12.8-r1 py3-pip=24.3.1-r0 && \
|
||||
python -m venv /opt/venv && \
|
||||
. /opt/venv/bin/activate
|
||||
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 2 Build wheel #
|
||||
#######################
|
||||
FROM base AS builder
|
||||
|
||||
COPY rootfs/requirements.txt /root/
|
||||
|
||||
RUN apk add --no-cache build-base=0.5-r3 && \
|
||||
python -m pip install --no-cache-dir wheel==0.45.1 && \
|
||||
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
|
||||
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 3 Build runtime #
|
||||
#######################
|
||||
FROM base AS runtime
|
||||
|
||||
ARG SERVICE_NAME
|
||||
ARG VERSION
|
||||
ENV SERVICE_NAME=${SERVICE_NAME}
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 4 Install libraries #
|
||||
#######################
|
||||
|
||||
# install the requirements from the wheels packages from the builder stage
|
||||
# and unistall python packages and alpine package manger to reduce attack surface
|
||||
|
||||
COPY --from=builder /root/wheels /root/wheels
|
||||
RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && \
|
||||
rm -rf /root/wheels && \
|
||||
python -m pip uninstall --yes wheel pip && \
|
||||
apk --purge del apk-tools
|
||||
|
||||
|
||||
#######################
|
||||
# 5 copy data #
|
||||
#######################
|
||||
|
||||
COPY rootfs/ /
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 6 run app #
|
||||
#######################
|
||||
|
||||
# make run.sh executable
|
||||
RUN chmod a+x /run.sh && \
|
||||
echo ${VERSION} > /proxy-version.txt
|
||||
|
||||
# command to run on container start
|
||||
CMD [ "/run.sh" ]
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
|
||||
BIN
ha_addons/ha_addon/icon.png
Normal file
BIN
ha_addons/ha_addon/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
ha_addons/ha_addon/logo.png
Normal file
BIN
ha_addons/ha_addon/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
33
ha_addons/ha_addon/rootfs/run.sh
Executable file
33
ha_addons/ha_addon/rootfs/run.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
|
||||
echo "Add-on environment started"
|
||||
|
||||
echo "check for Home Assistant MQTT"
|
||||
MQTT_HOST=$(bashio::services mqtt "host")
|
||||
MQTT_PORT=$(bashio::services mqtt "port")
|
||||
MQTT_USER=$(bashio::services mqtt "username")
|
||||
MQTT_PASSWORD=$(bashio::services mqtt "password")
|
||||
|
||||
# if a MQTT was/not found, drop a note
|
||||
if [ -z "$MQTT_HOST" ]; then
|
||||
echo "MQTT not found"
|
||||
else
|
||||
echo "MQTT found"
|
||||
export MQTT_HOST
|
||||
export MQTT_PORT
|
||||
export MQTT_USER
|
||||
export MQTT_PASSWORD
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
# Create folder for log und config files
|
||||
mkdir -p /homeassistant/tsun-proxy/logs
|
||||
|
||||
cd /home/proxy || exit
|
||||
|
||||
export VERSION=$(cat /proxy-version.txt)
|
||||
|
||||
echo "Start Proxyserver..."
|
||||
python3 server.py --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2
|
||||
95
ha_addons/ha_addon/translations/de.yaml
Executable file
95
ha_addons/ha_addon/translations/de.yaml
Executable file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
configuration:
|
||||
inverters:
|
||||
name: Wechselrichter
|
||||
description: >+
|
||||
Für jeden Wechselrichter muss die Seriennummer des Wechselrichters einer MQTT
|
||||
Definition zugeordnet werden. Dazu wird der entsprechende Konfigurationsblock mit der
|
||||
16-stellige Seriennummer gestartet, so dass alle nachfolgenden Parameter diesem
|
||||
Wechselrichter zugeordnet sind.
|
||||
Weitere wechselrichterspezifische Parameter (z.B. Polling Mode) können im
|
||||
Konfigurationsblock gesetzt werden.
|
||||
|
||||
Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` und die der GEN3PLUS
|
||||
Wechselrichter mir `Y17`oder `47`!
|
||||
|
||||
Siehe Beispielkonfiguration im Dokumentations-Tab
|
||||
|
||||
tsun.enabled:
|
||||
name: Verbindung zur TSUN Cloud - nur für GEN3-Wechselrichter
|
||||
description: >+
|
||||
Schaltet die Verbindung zur TSUN Cloud ein/aus.
|
||||
Diese Verbindung ist erforderlich, wenn Sie Daten an die TSUN Cloud senden möchten,
|
||||
z.B. um die TSUN-Apps zu nutzen oder Firmware-Updates zu erhalten.
|
||||
|
||||
ein => normaler Proxy-Betrieb.
|
||||
aus => Der Wechselrichter wird vom Internet isoliert.
|
||||
solarman.enabled:
|
||||
name: Verbindung zur Solarman Cloud - nur für GEN3PLUS Wechselrichter
|
||||
description: >+
|
||||
Schaltet die Verbindung zur Solarman Cloud ein/aus.
|
||||
Diese Verbindung ist erforderlich, wenn Sie Daten an die Solarman Cloud senden möchten,
|
||||
z.B. um die Solarman Apps zu nutzen oder Firmware-Updates zu erhalten.
|
||||
|
||||
ein => normaler Proxy-Betrieb.
|
||||
aus => Der Wechselrichter wird vom Internet isoliert.
|
||||
inverters.allow_all:
|
||||
name: Erlaube Verbindungen von sämtlichen Wechselrichtern
|
||||
description: >-
|
||||
Der Proxy akzeptiert normalerweise nur Verbindungen von konfigurierten Wechselrichtern.
|
||||
Schalten Sie dies für Testzwecke und unbekannte Seriennummern ein.
|
||||
mqtt.host:
|
||||
name: MQTT Broker Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse des MQTT-Brokers. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.port:
|
||||
name: MQTT Broker Port
|
||||
description: >-
|
||||
Port des MQTT-Brokers. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.user:
|
||||
name: MQTT Broker Benutzer
|
||||
description: >-
|
||||
Benutzer für den MQTT-Broker. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.passwd:
|
||||
name: MQTT Broker Passwort
|
||||
description: >-
|
||||
Passwort für den MQTT-Broker. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
ha.auto_conf_prefix:
|
||||
name: MQTT-Präfix für das Abonnieren von Home Assistant-Statusaktualisierungen
|
||||
ha.discovery_prefix:
|
||||
name: MQTT-Präfix für das discovery topic
|
||||
ha.entity_prefix:
|
||||
name: MQTT-Themenpräfix für die Veröffentlichung von Wechselrichterwerten
|
||||
ha.proxy_node_id:
|
||||
name: MQTT-Knoten-ID für die proxy_node_id
|
||||
ha.proxy_unique_id:
|
||||
name: MQTT-eindeutige ID zur Identifizierung einer Proxy-Instanz
|
||||
tsun.host:
|
||||
name: TSUN Cloud Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse der TSUN-Cloud. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zur Cloud logger.talent-monitoring.com herzustellen.
|
||||
solarman.host:
|
||||
name: Solarman Cloud Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse der Solarman-Cloud. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zur Cloud iot.talent-monitoring.com herzustellen.
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
name: TSUN GEN3PLUS ACL allow
|
||||
description: >-
|
||||
Liste erlaubter AT-Befehle für TSUN GEN3PLUS
|
||||
gen3plus.at_acl.tsun.block:
|
||||
name: TSUN GEN3 ACL block
|
||||
description: >-
|
||||
Liste blockierter AT-Befehle für TSUN GEN3PLUS
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
name: MQTT GEN3PLUS ACL allow
|
||||
description: >-
|
||||
Liste erlaubter MQTT-Befehle für GEN3PLUS
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
name: MQTT GEN3PLUS ACL block
|
||||
description: >-
|
||||
Liste blockierter MQTT-Befehle für GEN3PLUS
|
||||
|
||||
|
||||
network:
|
||||
5005/tcp: listening Port für TSUN GEN3 Wechselrichter
|
||||
10000/tcp: listening Port für TSUN GEN3PLUS Wechselrichter
|
||||
95
ha_addons/ha_addon/translations/en.yaml
Executable file
95
ha_addons/ha_addon/translations/en.yaml
Executable file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
configuration:
|
||||
inverters:
|
||||
name: Inverters
|
||||
description: >+
|
||||
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
|
||||
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` and that of the GEN3PLUS
|
||||
inverters with ‘Y17’ or ‘47’!
|
||||
|
||||
For reference see example configuration in Documentation Tab
|
||||
|
||||
tsun.enabled:
|
||||
name: Connection to TSUN Cloud - for GEN3 inverter only
|
||||
description: >+
|
||||
switch on/off connection to the TSUN cloud.
|
||||
This connection is only required if you want send data to the TSUN cloud
|
||||
eg. to use the TSUN APPs or receive firmware updates.
|
||||
|
||||
on => normal proxy operation.
|
||||
off => The Inverter become isolated from Internet.
|
||||
solarman.enabled:
|
||||
name: Connection to Solarman Cloud - for GEN3PLUS inverter only
|
||||
description: >+
|
||||
switch on/off connection to the Solarman cloud.
|
||||
This connection is only required if you want send data to the Solarman cloud
|
||||
eg. to use the Solarman APPs or receive firmware updates.
|
||||
|
||||
on => normal proxy operation.
|
||||
off => The Inverter become isolated from Internet
|
||||
inverters.allow_all:
|
||||
name: Allow all connections from all inverters
|
||||
description: >-
|
||||
The proxy only usually accepts connections from configured inverters.
|
||||
Switch on for test purposes and unknown serial numbers.
|
||||
mqtt.host:
|
||||
name: MQTT Broker Host
|
||||
description: >-
|
||||
Hostname or IP address of the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.port:
|
||||
name: MQTT Broker Port
|
||||
description: >-
|
||||
Port of the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.user:
|
||||
name: MQTT Broker User
|
||||
description: >-
|
||||
User for the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.passwd:
|
||||
name: MQTT Broker Password
|
||||
description: >-
|
||||
Password for the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
ha.auto_conf_prefix:
|
||||
name: MQTT prefix for subscribing for homeassistant status updates
|
||||
ha.discovery_prefix:
|
||||
name: MQTT prefix for discovery topic
|
||||
ha.entity_prefix:
|
||||
name: MQTT topic prefix for publishing inverter values
|
||||
ha.proxy_node_id:
|
||||
name: MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id:
|
||||
name: MQTT unique id, to identify a proxy instance
|
||||
tsun.host:
|
||||
name: TSUN Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the TSUN cloud. if not set, the addon will try to connect to the cloud
|
||||
on logger.talent-monitoring.com
|
||||
solarman.host:
|
||||
name: Solarman Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the Solarman cloud. if not set, the addon will try to connect to the cloud
|
||||
on iot.talent-monitoring.com
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
name: TSUN GEN3PLUS ACL allow
|
||||
description: >-
|
||||
List of allowed TSUN GEN3PLUS AT commands
|
||||
gen3plus.at_acl.tsun.block:
|
||||
name: TSUN GEN3 ACL block
|
||||
description: >-
|
||||
List of blocked TSUN GEN3PLUS AT commands
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
name: MQTT GEN3PLUS ACL allow
|
||||
description: >-
|
||||
List of allowed MQTT GEN3PLUS commands
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
name: MQTT GEN3PLUS ACL block
|
||||
description: >-
|
||||
List of blocked MQTT GEN3PLUS commands
|
||||
|
||||
network:
|
||||
5005/tcp: listening Port for TSUN GEN3 Devices
|
||||
10000/tcp: listening Port for TSUN GEN3PLUS Devices
|
||||
3
ha_addons/repository.yaml
Normal file
3
ha_addons/repository.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: TSUN-Proxy
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy/ha_addons
|
||||
maintainer: Stefan Allius
|
||||
112
ha_addons/templates/config.jinja
Executable file
112
ha_addons/templates/config.jinja
Executable file
@@ -0,0 +1,112 @@
|
||||
name: {{name}}
|
||||
description: {{description}}
|
||||
version: {% if version is defined and version|length %} {{version}} {% else %} {{AppVersion}} {% endif %}
|
||||
image: {{image}}
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy
|
||||
slug: {{slug}}
|
||||
advanced: {{advanced}}
|
||||
stage: {{stage}}
|
||||
init: false
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
- armhf
|
||||
- armv7
|
||||
startup: services
|
||||
homeassistant_api: true
|
||||
map:
|
||||
- type: addon_config
|
||||
path: /homeassistant/tsun-proxy
|
||||
read_only: False
|
||||
services:
|
||||
- mqtt:want
|
||||
ports:
|
||||
5005/tcp: 5005
|
||||
10000/tcp: 10000
|
||||
|
||||
# FIXME: we disabled the watchdog due to exceptions in the ha supervisor. See: https://github.com/s-allius/tsun-gen3-proxy/issues/249
|
||||
# watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
|
||||
|
||||
# Definition of parameters in the configuration tab of the addon
|
||||
# parameters are available within the container as /data/options.json
|
||||
# and should become picked up by the proxy - current workaround as a transfer script
|
||||
# TODO: check again for multi hierarchie parameters
|
||||
|
||||
schema:
|
||||
inverters:
|
||||
- serial: match(^(R17|Y17|Y47).{13}$)
|
||||
monitor_sn: int?
|
||||
node_id: str
|
||||
suggested_area: str
|
||||
modbus_polling: bool
|
||||
client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)?
|
||||
client_mode.port: port?
|
||||
client_mode.forward: bool?
|
||||
#strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt.
|
||||
# - string: str
|
||||
# type: str
|
||||
# manufacturer: str
|
||||
# daher diese variante
|
||||
pv1.manufacturer: str?
|
||||
pv1.type: str?
|
||||
pv2.manufacturer: str?
|
||||
pv2.type: str?
|
||||
pv3.manufacturer: str?
|
||||
pv3.type: str?
|
||||
pv4.manufacturer: str?
|
||||
pv4.type: str?
|
||||
pv5.manufacturer: str?
|
||||
pv5.type: str?
|
||||
pv6.manufacturer: str?
|
||||
pv6.type: str?
|
||||
tsun.enabled: bool
|
||||
solarman.enabled: bool
|
||||
inverters.allow_all: bool
|
||||
|
||||
# optionale parameter
|
||||
|
||||
mqtt.host: str?
|
||||
mqtt.port: port?
|
||||
mqtt.user: str?
|
||||
mqtt.passwd: password?
|
||||
ha.auto_conf_prefix: str? # suggeriert optionale konfigurationsoption -> es darf jedoch kein default unter "options" angegeben werden
|
||||
ha.discovery_prefix: str? # dito
|
||||
ha.entity_prefix: str? #dito
|
||||
ha.proxy_node_id: str? #dito
|
||||
ha.proxy_unique_id: str? #dito
|
||||
tsun.host: str?
|
||||
solarman.host: str?
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
- str
|
||||
gen3plus.at_acl.tsun.block:
|
||||
- str?
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
- str
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
- str?
|
||||
|
||||
# set default options for mandatory parameters
|
||||
# for optional parameters do not define any default value in the options dictionary.
|
||||
# If any default value is given, the option becomes a required value.
|
||||
options:
|
||||
inverters:
|
||||
- serial: R17E760702080400
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: false
|
||||
# strings:
|
||||
# - string: PV1
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
# - string: PV2
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
tsun.enabled: true # set default
|
||||
solarman.enabled: true # set default
|
||||
inverters.allow_all: false # set default
|
||||
gen3plus.at_acl.tsun.allow: ["AT+Z", "AT+UPURL", "AT+SUPDATE"]
|
||||
gen3plus.at_acl.mqtt.allow: ["AT+"]
|
||||
10
ha_addons/templates/debug_data.json
Normal file
10
ha_addons/templates/debug_data.json
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy (Debug)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters with Debug Logging",
|
||||
"version": "debug",
|
||||
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-debug",
|
||||
"advanced": true,
|
||||
"stage": "experimental"
|
||||
}
|
||||
10
ha_addons/templates/dev_data.json
Normal file
10
ha_addons/templates/dev_data.json
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy (Dev)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"version": "dev",
|
||||
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-dev",
|
||||
"advanced": false,
|
||||
"stage": "experimental"
|
||||
}
|
||||
10
ha_addons/templates/rc_data.json
Normal file
10
ha_addons/templates/rc_data.json
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy (Release Candidate)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"version": "rc",
|
||||
"image": "ghcr.io/s-allius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-rc",
|
||||
"advanced": true,
|
||||
"stage": "experimental"
|
||||
}
|
||||
9
ha_addons/templates/rel_data.json
Normal file
9
ha_addons/templates/rel_data.json
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"image": "ghcr.io/s-allius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy",
|
||||
"advanced": false,
|
||||
"stage": "stable"
|
||||
}
|
||||
20
proxy.c4
Normal file
20
proxy.c4
Normal file
@@ -0,0 +1,20 @@
|
||||
model {
|
||||
extend home.logger.proxy {
|
||||
component webserver 'http server'
|
||||
component inverter 'inverter'
|
||||
component local 'local connection'
|
||||
component remote 'remote connection'
|
||||
component r-ifc 'async-ifc'
|
||||
component l-ifc 'async-ifc'
|
||||
component prot 'Protocol' 'SolarmanV5 or Talent'
|
||||
component config 'config' 'reads the file confg.toml'
|
||||
component mqtt
|
||||
inverter -> local
|
||||
inverter -> remote
|
||||
remote -> r-ifc
|
||||
remote -> prot
|
||||
local -> l-ifc
|
||||
local -> prot
|
||||
prot -> mqtt
|
||||
}
|
||||
}
|
||||
8
pytest.ini
Normal file
8
pytest.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
# pytest.ini or .pytest.ini
|
||||
[pytest]
|
||||
minversion = 8.0
|
||||
addopts = -ra -q --durations=5
|
||||
pythonpath = app/src app/tests ha_addons/ha_addon/rootfs
|
||||
testpaths = app/tests ha_addons/ha_addon/tests
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
asyncio_mode = strict
|
||||
@@ -5,6 +5,10 @@
|
||||
},
|
||||
{
|
||||
"path": "../wiki"
|
||||
},
|
||||
{
|
||||
"name": "ha-addons",
|
||||
"path": "../ha-addons/ha-addons"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
||||
Reference in New Issue
Block a user