Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2dcb7212 | ||
|
|
32d7711ab7 | ||
|
|
dff8934b82 | ||
|
|
3eb6a24dcb | ||
|
|
f9be171865 | ||
|
|
45abc69ffb | ||
|
|
795a52e172 | ||
|
|
5d1ee60baf | ||
|
|
7cf9e98c7f | ||
|
|
e0777dca8e | ||
|
|
955657fd87 | ||
|
|
ecd21e46fb | ||
|
|
3489e8997d | ||
|
|
88cb01f613 | ||
|
|
be60f9ea1e | ||
|
|
10b4a84701 | ||
|
|
06ceb02f0d | ||
|
|
8a2ca3ab9a | ||
|
|
3f3ed1b14f | ||
|
|
036dd6d1dc | ||
|
|
1f0ac97368 | ||
|
|
5faf242d6c | ||
|
|
ec3af69e62 | ||
|
|
113a41ebfe | ||
|
|
13e6adc5c0 | ||
|
|
f9256099c7 | ||
|
|
204bc76153 | ||
|
|
58c7f51266 | ||
|
|
1eaabb97a2 | ||
|
|
7a6e6f73a5 | ||
|
|
39495d3e9e | ||
|
|
a257f09d4c | ||
|
|
5f0a35d55b | ||
|
|
4df36e2672 | ||
|
|
48a9696df2 | ||
|
|
24567eaf5f | ||
|
|
42fe33bacf | ||
|
|
cfdd65606d | ||
|
|
2e3ed8f162 | ||
|
|
66a875c291 | ||
|
|
46043e7754 | ||
|
|
01ad8eff6b | ||
|
|
53c76e72a2 | ||
|
|
24b092b69e | ||
|
|
cf1563dd55 | ||
|
|
962f6ee5fb | ||
|
|
9e60ad4bcd | ||
|
|
f5d760e2f0 | ||
|
|
3234e87b55 | ||
|
|
412013f626 | ||
|
|
1781dba065 | ||
|
|
1b3833989e | ||
|
|
26ca006853 | ||
|
|
1e160f3b0f | ||
|
|
338b86964d | ||
|
|
35952654db | ||
|
|
55c403a754 | ||
|
|
3bf245300d | ||
|
|
badc065b7a | ||
|
|
aea6cc9763 | ||
|
|
92d1e648ae | ||
|
|
879b6608b3 | ||
|
|
b69e7e2242 | ||
|
|
0913fde126 | ||
|
|
bedbe08eeb | ||
|
|
3c81d446dd | ||
|
|
b335881500 | ||
|
|
ac7b02bde9 | ||
|
|
47a89c269f | ||
|
|
be3b4d6df0 | ||
|
|
a5b2b4b7c2 | ||
|
|
668c631018 | ||
|
|
07c989a305 | ||
|
|
28cf875533 | ||
|
|
9bae905c08 | ||
|
|
45b57109a8 | ||
|
|
2c69044bf8 | ||
|
|
3bada76516 | ||
|
|
84231c034c | ||
|
|
d4fd396dcf | ||
|
|
211a958080 | ||
|
|
5ced5ff06a | ||
|
|
78a35b5513 | ||
|
|
9b22fe354c | ||
|
|
a6ad3d4f0d | ||
|
|
4993676614 | ||
|
|
10a18237c7 | ||
|
|
8d67f1745d | ||
|
|
9eb7c7fbe0 | ||
|
|
6c6109d421 | ||
|
|
7d0ea41728 | ||
|
|
ce5bd6eb0a | ||
|
|
6122f40718 | ||
|
|
c5f184a730 | ||
|
|
db06d8c8e6 | ||
|
|
3863454a84 | ||
|
|
5775cb1ce3 | ||
|
|
5d61a261b1 | ||
|
|
bbda66e455 | ||
|
|
0c7bf7956d |
3
.cover_ghaction_rc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[run]
|
||||||
|
branch = True
|
||||||
|
relative_files = True
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
[run]
|
[run]
|
||||||
branch = True
|
branch = True
|
||||||
relative_files = True
|
|
||||||
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=
|
||||||
13
.github/workflows/python-app.yml
vendored
@@ -26,20 +26,21 @@ permissions:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TZ: "Europe/Berlin"
|
TZ: "Europe/Berlin"
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@@ -53,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/
|
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
|
- name: Test with pytest
|
||||||
run: |
|
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
|
coverage report
|
||||||
- name: Analyze with SonarCloud
|
- name: Analyze with SonarCloud
|
||||||
uses: SonarSource/sonarcloud-github-action@v3.1.0
|
if: ${{ env.SONAR_TOKEN != 0 }}
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
projectBaseDir: .
|
projectBaseDir: .
|
||||||
args:
|
args:
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -1,11 +1,15 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
|
.venv/**
|
||||||
bin/**
|
bin/**
|
||||||
mosquitto/**
|
mosquitto/**
|
||||||
homeassistant/**
|
homeassistant/**
|
||||||
|
ha_addons/ha_addon/rootfs/home/proxy/*
|
||||||
|
ha_addons/ha_addon/rootfs/requirements.txt
|
||||||
tsun_proxy/**
|
tsun_proxy/**
|
||||||
Doku/**
|
Doku/**
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.coverage
|
.coverage
|
||||||
.env
|
.env
|
||||||
|
.venv
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|||||||
2
.hadolint.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ignored:
|
||||||
|
- SC1091
|
||||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13.2
|
||||||
20
.vscode/settings.json
vendored
@@ -1,16 +1,20 @@
|
|||||||
{
|
{
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"app/src",
|
||||||
|
"app/tests",
|
||||||
|
".venv/lib",
|
||||||
|
],
|
||||||
"python.testing.pytestArgs": [
|
"python.testing.pytestArgs": [
|
||||||
"-vv",
|
"-vvv",
|
||||||
"app",
|
|
||||||
"--cov=app/src",
|
"--cov=app/src",
|
||||||
"--cov-report=xml",
|
"--cov-report=xml",
|
||||||
"--cov-report=html",
|
"app",
|
||||||
"system_tests"
|
"system_tests"
|
||||||
],
|
],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"flake8.args": [
|
"flake8.args": [
|
||||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
"--extend-exclude=app/tests/*.py,system_tests/*.py"
|
||||||
],
|
],
|
||||||
"sonarlint.connectedMode.project": {
|
"sonarlint.connectedMode.project": {
|
||||||
"connectionId": "s-allius",
|
"connectionId": "s-allius",
|
||||||
@@ -18,5 +22,11 @@
|
|||||||
},
|
},
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/*.pyi": true
|
"**/*.pyi": true
|
||||||
}
|
},
|
||||||
|
"python.analysis.typeEvaluation.deprecateTypingAliases": true,
|
||||||
|
"python.autoComplete.extraPaths": [
|
||||||
|
".venv/lib"
|
||||||
|
],
|
||||||
|
"coverage-gutters.coverageBaseDir": "tsun",
|
||||||
|
"makefile.configureOnOpen": false
|
||||||
}
|
}
|
||||||
53
CHANGELOG.md
@@ -7,8 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
- update dependency python to 3.13
|
||||||
|
- add initial support for TSUN MS-3000
|
||||||
|
- add initial apparmor support [#293](https://github.com/s-allius/tsun-gen3-proxy/issues/293)
|
||||||
|
- add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292)
|
||||||
|
- add Modbus scanning mode
|
||||||
|
- allow `R47`serial numbers for GEN3 inverters
|
||||||
|
- add watchdog for Add-ons
|
||||||
|
- add first costumer apparmor definition
|
||||||
|
- Respect logging.ini file, if LOG_ENV isn't set well [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288)
|
||||||
|
- Remove trailing apostrophe in the log output [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288)
|
||||||
|
- update AddOn base docker image to version 17.2.1
|
||||||
|
- addon: add date and time to dev container version
|
||||||
|
- Update AddOn python3 to 3.12.9-r0
|
||||||
|
- add initial DCU support
|
||||||
|
- update aiohttp to version 3.11.12
|
||||||
|
- fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180)
|
||||||
|
|
||||||
|
## [0.12.1] - 2025-01-13
|
||||||
|
|
||||||
|
- addon: bump base image version to v17.1.0
|
||||||
|
- 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
|
## [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.
|
- Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
|
||||||
|
|
||||||
## [0.11.0] - 2024-10-13
|
## [0.11.0] - 2024-10-13
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
56
README.md
@@ -6,16 +6,16 @@
|
|||||||
<p align="center">integration</p>
|
<p align="center">integration</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
|
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
|
||||||
<a href="https://www.python.org/downloads/release/python-3120/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.12-blue.svg"></a>
|
<a href="https://www.python.org/downloads/release/python-3130/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.13-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://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.0-lightblue.svg"></a>
|
||||||
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
|
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
|
||||||
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
||||||
<br>
|
<br>
|
||||||
<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=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 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=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 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=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>
|
<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>
|
</p>
|
||||||
|
|
||||||
# Overview
|
# 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.
|
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>
|
<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.
|
ℹ️ 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>
|
<br><br>
|
||||||
@@ -65,11 +68,20 @@ Here are some screenshots of how the inverter is displayed in the Home Assistant
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
### for Docker Installation
|
||||||
|
|
||||||
- A running Docker engine to host the container
|
- A running Docker engine to host the container
|
||||||
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
|
- 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
|
# Getting Started
|
||||||
|
|
||||||
|
## for Docker Installation
|
||||||
|
|
||||||
To run the proxy, you first need to create the image. You can do this quite simply as follows:
|
To run the proxy, you first need to create the image. You can do this quite simply as follows:
|
||||||
|
|
||||||
```sh
|
```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
|
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
|
# 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
|
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.
|
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.
|
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
|
## 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
|
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
|
||||||
###
|
###
|
||||||
### Here you will find a description of all configuration options:
|
### 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
|
### 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
|
### 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
|
## 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.
|
## 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
|
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
|
## 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.
|
## 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
|
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
|
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||||
## to use the TSUN APPs or receive firmware updates.
|
## 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
|
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
|
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||||
## to use the TSUN APPs or receive firmware updates.
|
## 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.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||||
solarman.host = 'iot.talent-monitoring.com'
|
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
|
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||||
# the next line and configure the fixed IP of your inverter
|
# the next line and configure the fixed IP of your inverter
|
||||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||||
|
|
||||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
@@ -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.
|
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.
|
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)
|
## Client Mode (GEN3PLUS only)
|
||||||
@@ -408,3 +433,6 @@ We're very happy to receive contributions to this project! You can get started b
|
|||||||
## Changelog
|
## 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/).
|
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
@@ -0,0 +1 @@
|
|||||||
|
0.13.0
|
||||||
@@ -4,14 +4,13 @@ ARG GID=1000
|
|||||||
|
|
||||||
#
|
#
|
||||||
# first stage for our base image
|
# first stage for our base image
|
||||||
FROM python:3.12-alpine AS base
|
FROM python:3.13-alpine AS base
|
||||||
USER root
|
|
||||||
|
|
||||||
COPY --chmod=0700 ./hardening_base.sh .
|
COPY --chmod=0700 ./hardening_base.sh /
|
||||||
RUN apk upgrade --no-cache && \
|
RUN apk upgrade --no-cache && \
|
||||||
apk add --no-cache su-exec && \
|
apk add --no-cache su-exec=0.2-r3 && \
|
||||||
./hardening_base.sh && \
|
/hardening_base.sh && \
|
||||||
rm ./hardening_base.sh
|
rm /hardening_base.sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# second stage for building wheels packages
|
# 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 the dependencies file to the root dir and install requirements
|
||||||
COPY ./requirements.txt /root/
|
COPY ./requirements.txt /root/
|
||||||
RUN apk add --no-cache build-base && \
|
RUN apk add --no-cache build-base=0.5-r3 && \
|
||||||
python -m pip install --no-cache-dir -U pip wheel && \
|
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
|
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ ARG SERVICE_NAME
|
|||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG UID
|
ARG UID
|
||||||
ARG GID
|
ARG GID
|
||||||
ARG LOG_LVL
|
ARG LOG_LVL=INFO
|
||||||
ARG environment
|
ARG environment
|
||||||
|
|
||||||
ENV SERVICE_NAME=$SERVICE_NAME
|
ENV SERVICE_NAME=$SERVICE_NAME
|
||||||
@@ -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
|
# and unistall python packages and alpine package manger to reduce attack surface
|
||||||
COPY --from=builder /root/wheels /root/wheels
|
COPY --from=builder /root/wheels /root/wheels
|
||||||
COPY --chmod=0700 ./hardening_final.sh .
|
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 && \
|
rm -rf /root/wheels && \
|
||||||
python -m pip uninstall --yes setuptools wheel pip && \
|
python -m pip uninstall --yes wheel pip && \
|
||||||
apk --purge del apk-tools && \
|
apk --purge del apk-tools && \
|
||||||
./hardening_final.sh && \
|
./hardening_final.sh && \
|
||||||
rm ./hardening_final.sh
|
rm ./hardening_final.sh
|
||||||
@@ -60,7 +59,6 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
|||||||
|
|
||||||
# copy the content of the local src and config directory to the working directory
|
# copy the content of the local src and config directory to the working directory
|
||||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||||
COPY config .
|
|
||||||
COPY src .
|
COPY src .
|
||||||
RUN echo ${VERSION} > /proxy-version.txt \
|
RUN echo ${VERSION} > /proxy-version.txt \
|
||||||
&& date > /build-date.txt
|
&& date > /build-date.txt
|
||||||
|
|||||||
34
app/Makefile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!make
|
||||||
|
include ../.env
|
||||||
|
|
||||||
|
SHELL = /bin/sh
|
||||||
|
IMAGE = tsun-gen3-proxy
|
||||||
|
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
SRC=.
|
||||||
|
|
||||||
|
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
|
||||||
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
|
|
||||||
@@ -18,7 +18,7 @@ variable "DESCRIPTION" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
target "_common" {
|
target "_common" {
|
||||||
context = "app"
|
context = "."
|
||||||
dockerfile = "Dockerfile"
|
dockerfile = "Dockerfile"
|
||||||
args = {
|
args = {
|
||||||
VERSION = "${VERSION}"
|
VERSION = "${VERSION}"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
371
app/docu/proxy_2.svg
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<?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="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,-1996 534.566,-1996 534.566,4 -4,4"/>
|
||||||
|
<!-- A0 -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>A0</title>
|
||||||
|
<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="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="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="edge14" class="edge">
|
||||||
|
<title>A1->A14</title>
|
||||||
|
<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="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="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>
|
||||||
|
<!-- 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="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>
|
||||||
|
<!-- A2->A4 -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<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>
|
||||||
|
<!-- 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>
|
||||||
|
<!-- 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="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>
|
||||||
|
<!-- 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>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>
|
||||||
|
<!-- 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="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="edge7" class="edge">
|
||||||
|
<title>A10->A3</title>
|
||||||
|
<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="edge9" class="edge">
|
||||||
|
<title>A10->A4</title>
|
||||||
|
<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>
|
||||||
|
<!-- 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->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="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="edge11" class="edge">
|
||||||
|
<title>A11->A12</title>
|
||||||
|
<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>
|
||||||
|
<!-- 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="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>
|
||||||
|
<!-- 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>
|
||||||
|
After Width: | Height: | Size: 33 KiB |
43
app/docu/proxy_2.yuml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// {type:class}
|
||||||
|
// {direction:topDown}
|
||||||
|
// {generate:true}
|
||||||
|
|
||||||
|
[note: Example of instantiation for a GEN3 inverter!{bg:cornsilk}]
|
||||||
|
[<<AbstractIterMeta>>||__iter__()]
|
||||||
|
|
||||||
|
[InverterG3|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]
|
||||||
|
[AsyncStream|reader;writer;addr;r_addr;l_addr|;<async>loop;disc();close();healthy();;__async_read();__async_write();__async_forward()]
|
||||||
|
[AsyncStreamServer|create_remote|<async>server_loop();<async>_async_forward();<async>publish_outstanding_mqtt();close()]
|
||||||
|
[AsyncStreamClient||<async>client_loop();<async>_async_forward())]
|
||||||
|
[<<AsyncIfc>>]^-.-[AsyncIfcImpl]
|
||||||
|
[AsyncIfcImpl]^[AsyncStream]
|
||||||
|
[AsyncStream]^[AsyncStreamServer]
|
||||||
|
[AsyncStream]^[AsyncStreamClient]
|
||||||
|
|
||||||
|
|
||||||
|
[Talent|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()]
|
||||||
|
|
||||||
|
[Talent]->[InfosG3]
|
||||||
|
|
||||||
|
[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]^[Talent]
|
||||||
|
|
||||||
|
[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]
|
||||||
364
app/docu/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 |
@@ -2,11 +2,12 @@
|
|||||||
// {direction:topDown}
|
// {direction:topDown}
|
||||||
// {generate:true}
|
// {generate:true}
|
||||||
|
|
||||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
[note: Example of instantiation for a GEN3PLUS inverter!{bg:cornsilk}]
|
||||||
[<<AbstractIterMeta>>||__iter__()]
|
[<<AbstractIterMeta>>||__iter__()]
|
||||||
|
|
||||||
[InverterG3|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
|
||||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
[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()]
|
[<<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]
|
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
|
||||||
@@ -18,34 +19,24 @@
|
|||||||
[AsyncStream]^[AsyncStreamServer]
|
[AsyncStream]^[AsyncStreamServer]
|
||||||
[AsyncStream]^[AsyncStreamClient]
|
[AsyncStream]^[AsyncStreamClient]
|
||||||
|
|
||||||
|
[SolarmanV5|conn_no;addr;;control;serial;snr;db:InfosG3P;switch|msg_unknown();;healthy();close()]
|
||||||
[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()]
|
[SolarmanV5]<-++[local:StreamPtr]
|
||||||
[Talent]<remote-[InverterG3]
|
[local:StreamPtr]++->[AsyncStreamServer]
|
||||||
[InverterG3]-remote>[AsyncStreamClient]
|
[SolarmanV5]<-0..1[remote:StreamPtr]
|
||||||
[Talent]<-local++[InverterG3]
|
[remote:StreamPtr]0..1->[AsyncStreamClient]
|
||||||
[InverterG3]++local->[AsyncStreamServer]
|
|
||||||
|
|
||||||
[SolarmanV5|ifc:AsyncIfc;conn_no;addr;;control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;healthy();close()]
|
|
||||||
[SolarmanV5]<remote-[InverterG3P]
|
|
||||||
[InverterG3P]-remote>[AsyncStreamClient]
|
|
||||||
[SolarmanV5]<-local++[InverterG3P]
|
|
||||||
[InverterG3P]++local->[AsyncStreamServer]
|
|
||||||
|
|
||||||
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
|
[Infos|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()]
|
[Infos]^[InfosG3P||ha_confs();parse()]
|
||||||
|
|
||||||
[Talent]use->[<<AsyncIfc>>]
|
|
||||||
[Talent]->[InfosG3]
|
|
||||||
[SolarmanV5]use->[<<AsyncIfc>>]
|
|
||||||
[SolarmanV5]->[InfosG3P]
|
[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()]
|
[<<ProtocolIfc>>|_registry|close()]
|
||||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||||
[<<ProtocolIfc>>]^-.-[Message|node_id|inc_counter();dec_counter()]
|
[<<ProtocolIfc>>]^-.-[Message]
|
||||||
[Message]^[Talent]
|
|
||||||
[Message]^[SolarmanV5]
|
[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|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]<0..1-has[Message]
|
||||||
[Modbus]<1-has[Talent]
|
|
||||||
432
app/proxy_2.svg
@@ -1,432 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
|
||||||
-->
|
|
||||||
<!-- Title: G Pages: 1 -->
|
|
||||||
<svg width="468pt" height="1942pt"
|
|
||||||
viewBox="0.00 0.00 468.35 1942.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1938)">
|
|
||||||
<title>G</title>
|
|
||||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1938 464.348,-1938 464.348,4 -4,4"/>
|
|
||||||
<!-- A0 -->
|
|
||||||
<g id="node1" class="node">
|
|
||||||
<title>A0</title>
|
|
||||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1910 .1516,-1910 .1516,-1874 114.5444,-1874 114.5444,-1904 108.5444,-1910"/>
|
|
||||||
<polyline fill="none" stroke="#000000" points="108.5444,-1910 108.5444,-1904 "/>
|
|
||||||
<polyline fill="none" stroke="#000000" points="114.5444,-1904 108.5444,-1904 "/>
|
|
||||||
<text text-anchor="middle" x="57.348" y="-1895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
|
||||||
<text text-anchor="middle" x="57.348" y="-1883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
|
||||||
</g>
|
|
||||||
<!-- A1 -->
|
|
||||||
<g id="node2" class="node">
|
|
||||||
<title>A1</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="132.348,-1902 132.348,-1934 248.348,-1934 248.348,-1902 132.348,-1902"/>
|
|
||||||
<text text-anchor="start" x="141.997" y="-1915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="132.348,-1882 132.348,-1902 248.348,-1902 248.348,-1882 132.348,-1882"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="132.348,-1850 132.348,-1882 248.348,-1882 248.348,-1850 132.348,-1850"/>
|
|
||||||
<text text-anchor="start" x="168.958" y="-1863" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A14 -->
|
|
||||||
<g id="node15" class="node">
|
|
||||||
<title>A14</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145.348,-1768 145.348,-1800 235.348,-1800 235.348,-1768 145.348,-1768"/>
|
|
||||||
<text text-anchor="start" x="155.0545" y="-1781" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145.348,-1736 145.348,-1768 235.348,-1768 235.348,-1736 145.348,-1736"/>
|
|
||||||
<text text-anchor="start" x="171.1815" y="-1749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145.348,-1704 145.348,-1736 235.348,-1736 235.348,-1704 145.348,-1704"/>
|
|
||||||
<text text-anchor="start" x="175.3505" y="-1717" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A1->A14 -->
|
|
||||||
<g id="edge19" class="edge">
|
|
||||||
<title>A1->A14</title>
|
|
||||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1839.847C190.348,-1826.914 190.348,-1813.1101 190.348,-1800.3669"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="186.8481,-1839.9953 190.348,-1849.9954 193.8481,-1839.9954 186.8481,-1839.9953"/>
|
|
||||||
</g>
|
|
||||||
<!-- A2 -->
|
|
||||||
<g id="node3" class="node">
|
|
||||||
<title>A2</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="362.348,-584 362.348,-616 460.348,-616 460.348,-584 362.348,-584"/>
|
|
||||||
<text text-anchor="start" x="387.7325" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="362.348,-528 362.348,-584 460.348,-584 460.348,-528 362.348,-528"/>
|
|
||||||
<text text-anchor="start" x="401.345" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
|
||||||
<text text-anchor="start" x="371.901" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
|
||||||
<text text-anchor="start" x="377.18" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="362.348,-472 362.348,-528 460.348,-528 460.348,-472 362.348,-472"/>
|
|
||||||
<text text-anchor="start" x="375.79" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
|
||||||
<text text-anchor="start" x="396.3505" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A7 -->
|
|
||||||
<g id="node8" class="node">
|
|
||||||
<title>A7</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="29.348,-100 29.348,-132 207.348,-132 207.348,-100 29.348,-100"/>
|
|
||||||
<text text-anchor="start" x="73.8995" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="29.348,-68 29.348,-100 207.348,-100 207.348,-68 29.348,-68"/>
|
|
||||||
<text text-anchor="start" x="86.119" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="29.348,0 29.348,-68 207.348,-68 207.348,0 29.348,0"/>
|
|
||||||
<text text-anchor="start" x="70.0055" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
|
||||||
<text text-anchor="start" x="60.8365" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
|
||||||
<text text-anchor="start" x="39.157" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
|
||||||
<text text-anchor="start" x="103.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A2->A7 -->
|
|
||||||
<g id="edge8" class="edge">
|
|
||||||
<title>A2->A7</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M377.1669,-460.7015C372.2935,-447.8271 367.5247,-434.6178 363.348,-422 328.6206,-317.0888 364.6855,-270.3837 298.348,-182 272.7246,-147.8611 252.2817,-155.039 216.348,-132 216.2563,-131.9412 216.1645,-131.8823 216.0727,-131.8234"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="377.1669,-460.7018 383.0503,-464.8714 381.4643,-471.9059 375.5809,-467.7363 377.1669,-460.7018"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="207.433,-126.2441 218.2748,-127.8888 211.6333,-128.9566 215.8336,-131.6691 215.8336,-131.6691 215.8336,-131.6691 211.6333,-128.9566 213.3923,-135.4493 207.433,-126.2441 207.433,-126.2441"/>
|
|
||||||
<text text-anchor="middle" x="367.0813" y="-455.0087" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
|
||||||
</g>
|
|
||||||
<!-- A8 -->
|
|
||||||
<g id="node9" class="node">
|
|
||||||
<title>A8</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="225.348,-82 225.348,-114 363.348,-114 363.348,-82 225.348,-82"/>
|
|
||||||
<text text-anchor="start" x="251.845" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="225.348,-62 225.348,-82 363.348,-82 363.348,-62 225.348,-62"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="225.348,-18 225.348,-62 363.348,-62 363.348,-18 225.348,-18"/>
|
|
||||||
<text text-anchor="start" x="248.226" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
|
||||||
<text text-anchor="start" x="235.172" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
|
||||||
</g>
|
|
||||||
<!-- A2->A8 -->
|
|
||||||
<g id="edge6" class="edge">
|
|
||||||
<title>A2->A8</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M410.2845,-471.9734C407.2471,-397.4776 396.9339,-278.7213 363.348,-182 356.28,-161.6455 345.3872,-140.9471 334.3293,-122.7781"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="328.9036,-114.0745 338.0125,-120.18 331.5487,-118.3175 334.1938,-122.5606 334.1938,-122.5606 334.1938,-122.5606 331.5487,-118.3175 330.375,-124.9412 328.9036,-114.0745 328.9036,-114.0745"/>
|
|
||||||
<text text-anchor="middle" x="345.6654" y="-121.9851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
|
||||||
</g>
|
|
||||||
<!-- A3 -->
|
|
||||||
<g id="node4" class="node">
|
|
||||||
<title>A3</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="40.348,-342 40.348,-374 138.348,-374 138.348,-342 40.348,-342"/>
|
|
||||||
<text text-anchor="start" x="62.398" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="40.348,-286 40.348,-342 138.348,-342 138.348,-286 40.348,-286"/>
|
|
||||||
<text text-anchor="start" x="79.345" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
|
||||||
<text text-anchor="start" x="49.901" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
|
||||||
<text text-anchor="start" x="55.18" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="40.348,-230 40.348,-286 138.348,-286 138.348,-230 40.348,-230"/>
|
|
||||||
<text text-anchor="start" x="53.79" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
|
||||||
<text text-anchor="start" x="74.3505" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A3->A7 -->
|
|
||||||
<g id="edge12" class="edge">
|
|
||||||
<title>A3->A7</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M99.7163,-217.6237C102.7418,-193.0021 106.0292,-166.2494 108.9885,-142.1671"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="99.6702,-217.9995 102.9085,-224.4425 98.2065,-229.9099 94.9682,-223.4668 99.6702,-217.9995"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="110.2349,-132.0235 113.4816,-142.4978 109.6251,-136.9862 109.0152,-141.9489 109.0152,-141.9489 109.0152,-141.9489 109.6251,-136.9862 104.5488,-141.4 110.2349,-132.0235 110.2349,-132.0235"/>
|
|
||||||
<text text-anchor="middle" x="92.028" y="-207.8882" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
|
||||||
</g>
|
|
||||||
<!-- A3->A8 -->
|
|
||||||
<g id="edge10" class="edge">
|
|
||||||
<title>A3->A8</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M138.4873,-232.1115C151.0204,-215.3231 164.7944,-197.7122 178.348,-182 196.1632,-161.3474 216.8826,-139.9135 235.8403,-121.173"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="243.01,-114.1296 239.0299,-124.3477 239.4432,-117.6336 235.8763,-121.1375 235.8763,-121.1375 235.8763,-121.1375 239.4432,-117.6336 232.7227,-117.9274 243.01,-114.1296 243.01,-114.1296"/>
|
|
||||||
<text text-anchor="middle" x="236.0028" y="-129.8619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
|
||||||
</g>
|
|
||||||
<!-- A4 -->
|
|
||||||
<g id="node5" class="node">
|
|
||||||
<title>A4</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="180.348,-958 180.348,-990 297.348,-990 297.348,-958 180.348,-958"/>
|
|
||||||
<text text-anchor="start" x="208.277" y="-971" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="180.348,-938 180.348,-958 297.348,-958 297.348,-938 180.348,-938"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="180.348,-666 180.348,-938 297.348,-938 297.348,-666 180.348,-666"/>
|
|
||||||
<text text-anchor="start" x="208.284" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
|
||||||
<text text-anchor="start" x="206.614" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
|
||||||
<text text-anchor="start" x="220.5115" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
|
||||||
<text text-anchor="start" x="218.292" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
|
||||||
<text text-anchor="start" x="221.9015" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
|
||||||
<text text-anchor="start" x="218.0115" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
|
||||||
<text text-anchor="start" x="222.1815" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
|
||||||
<text text-anchor="start" x="218.017" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
|
||||||
<text text-anchor="start" x="222.1815" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
|
||||||
<text text-anchor="start" x="216.6225" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
|
||||||
<text text-anchor="start" x="218.2925" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
|
||||||
<text text-anchor="start" x="221.6265" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
|
||||||
<text text-anchor="start" x="217.7365" y="-751" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
|
||||||
<text text-anchor="start" x="221.9065" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
|
||||||
<text text-anchor="start" x="217.742" y="-727" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
|
||||||
<text text-anchor="start" x="221.9065" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
|
||||||
<text text-anchor="start" x="213.847" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
|
||||||
<text text-anchor="start" x="190.2275" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A5 -->
|
|
||||||
<g id="node6" class="node">
|
|
||||||
<title>A5</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="192.348,-574 192.348,-606 285.348,-606 285.348,-574 192.348,-574"/>
|
|
||||||
<text text-anchor="start" x="210.512" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="192.348,-482 192.348,-574 285.348,-574 285.348,-482 192.348,-482"/>
|
|
||||||
<text text-anchor="start" x="201.896" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
|
||||||
<text text-anchor="start" x="205.785" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
|
||||||
<text text-anchor="start" x="205.51" y="-531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
|
||||||
<text text-anchor="start" x="204.944" y="-519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
|
||||||
<text text-anchor="start" x="221.0615" y="-507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
|
||||||
<text text-anchor="start" x="214.3975" y="-495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
|
||||||
</g>
|
|
||||||
<!-- A4->A5 -->
|
|
||||||
<g id="edge1" class="edge">
|
|
||||||
<title>A4->A5</title>
|
|
||||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M238.348,-655.339C238.348,-637.8929 238.348,-621.0952 238.348,-606.0686"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="234.8481,-655.6774 238.348,-665.6775 241.8481,-655.6775 234.8481,-655.6774"/>
|
|
||||||
</g>
|
|
||||||
<!-- A6 -->
|
|
||||||
<g id="node7" class="node">
|
|
||||||
<title>A6</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="187.348,-390 187.348,-422 289.348,-422 289.348,-390 187.348,-390"/>
|
|
||||||
<text text-anchor="start" x="208.622" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="187.348,-310 187.348,-390 289.348,-390 289.348,-310 187.348,-310"/>
|
|
||||||
<text text-anchor="start" x="223.901" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
|
||||||
<text text-anchor="start" x="226.131" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
|
||||||
<text text-anchor="start" x="228.345" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
|
||||||
<text text-anchor="start" x="223.901" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
|
||||||
<text text-anchor="start" x="224.456" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="187.348,-182 187.348,-310 289.348,-310 289.348,-182 187.348,-182"/>
|
|
||||||
<text text-anchor="start" x="210.002" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
|
||||||
<text text-anchor="start" x="226.13" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
|
||||||
<text text-anchor="start" x="223.3505" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
<text text-anchor="start" x="218.902" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
|
||||||
<text text-anchor="start" x="203.6185" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
|
||||||
<text text-anchor="start" x="203.069" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
|
||||||
<text text-anchor="start" x="196.955" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A5->A6 -->
|
|
||||||
<g id="edge2" class="edge">
|
|
||||||
<title>A5->A6</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M238.348,-471.886C238.348,-456.1951 238.348,-439.1858 238.348,-422.1976"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="234.8481,-471.9932 238.348,-481.9932 241.8481,-471.9933 234.8481,-471.9932"/>
|
|
||||||
</g>
|
|
||||||
<!-- A6->A7 -->
|
|
||||||
<g id="edge3" class="edge">
|
|
||||||
<title>A6->A7</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M182.6502,-192.461C172.1789,-171.8674 161.5319,-150.9284 151.9938,-132.1701"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="179.6308,-194.2451 187.2832,-201.5725 185.8705,-191.0723 179.6308,-194.2451"/>
|
|
||||||
</g>
|
|
||||||
<!-- A6->A8 -->
|
|
||||||
<g id="edge4" class="edge">
|
|
||||||
<title>A6->A8</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M269.1746,-172.088C274.0746,-151.438 278.8579,-131.2796 282.9161,-114.1772"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="265.7436,-171.3879 266.8402,-181.9259 272.5545,-173.0041 265.7436,-171.3879"/>
|
|
||||||
</g>
|
|
||||||
<!-- A9 -->
|
|
||||||
<g id="node10" class="node">
|
|
||||||
<title>A9</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="302.348,-1320 302.348,-1352 416.348,-1352 416.348,-1320 302.348,-1320"/>
|
|
||||||
<text text-anchor="start" x="345.456" y="-1333" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="302.348,-1168 302.348,-1320 416.348,-1320 416.348,-1168 302.348,-1168"/>
|
|
||||||
<text text-anchor="start" x="334.0665" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
|
||||||
<text text-anchor="start" x="340.171" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
|
||||||
<text text-anchor="start" x="349.345" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
|
||||||
<text text-anchor="start" x="312.111" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
|
||||||
<text text-anchor="start" x="347.1255" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
|
||||||
<text text-anchor="start" x="327.948" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
|
||||||
<text text-anchor="start" x="331.288" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
|
||||||
<text text-anchor="start" x="334.8925" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
|
||||||
<text text-anchor="start" x="333.232" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
|
||||||
<text text-anchor="start" x="345.46" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="302.348,-1040 302.348,-1168 416.348,-1168 416.348,-1040 302.348,-1040"/>
|
|
||||||
<text text-anchor="start" x="316.8405" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
|
||||||
<text text-anchor="start" x="318.7805" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
|
||||||
<text text-anchor="start" x="324.6245" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
|
||||||
<text text-anchor="start" x="312.6765" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
|
||||||
<text text-anchor="start" x="314.6215" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
|
||||||
<text text-anchor="start" x="323.7885" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
|
||||||
<text text-anchor="start" x="339.902" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
|
||||||
<text text-anchor="start" x="344.3505" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A9->A2 -->
|
|
||||||
<g id="edge5" class="edge">
|
|
||||||
<title>A9->A2</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M377.8061,-1029.6429C379.5037,-1016.2527 381.0593,-1002.9144 382.348,-990 395.4702,-858.5013 399.9848,-704.3277 404.1955,-616.0127"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="376.4823,-1039.8864 373.3012,-1029.3921 377.1232,-1034.9276 377.764,-1029.9689 377.764,-1029.9689 377.764,-1029.9689 377.1232,-1034.9276 382.2269,-1030.5457 376.4823,-1039.8864 376.4823,-1039.8864"/>
|
|
||||||
<text text-anchor="middle" x="370.4228" y="-1017.8264" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
|
||||||
</g>
|
|
||||||
<!-- A9->A2 -->
|
|
||||||
<g id="edge7" class="edge">
|
|
||||||
<title>A9->A2</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M395.7566,-1029.6429C397.5037,-1016.2527 399.0593,-1002.9144 400.348,-990 412.8936,-864.2801 417.5714,-717.8344 416.9909,-628.0298"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="394.3845,-1039.8864 391.2521,-1029.3774 395.0484,-1034.9307 395.7122,-1029.9749 395.7122,-1029.9749 395.7122,-1029.9749 395.0484,-1034.9307 400.1724,-1030.5724 394.3845,-1039.8864 394.3845,-1039.8864"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="416.9908,-628.0122 412.9345,-622.0501 416.8778,-616.0127 420.9341,-621.9747 416.9908,-628.0122"/>
|
|
||||||
<text text-anchor="middle" x="425.5004" y="-631.0585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
|
||||||
</g>
|
|
||||||
<!-- A9->A4 -->
|
|
||||||
<g id="edge15" class="edge">
|
|
||||||
<title>A9->A4</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M308.0331,-1039.9349C303.6793,-1026.6936 299.2605,-1013.2547 294.8698,-999.9011"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="291.679,-990.1968 299.0774,-998.2908 293.2408,-994.9466 294.8026,-999.6964 294.8026,-999.6964 294.8026,-999.6964 293.2408,-994.9466 290.5277,-1001.102 291.679,-990.1968 291.679,-990.1968"/>
|
|
||||||
<text text-anchor="middle" x="294.3419" y="-1022.3558" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
|
||||||
</g>
|
|
||||||
<!-- A12 -->
|
|
||||||
<g id="node13" class="node">
|
|
||||||
<title>A12</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="315.348,-844 315.348,-876 382.348,-876 382.348,-844 315.348,-844"/>
|
|
||||||
<text text-anchor="start" x="331.341" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="315.348,-824 315.348,-844 382.348,-844 382.348,-824 315.348,-824"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="315.348,-780 315.348,-824 382.348,-824 382.348,-780 315.348,-780"/>
|
|
||||||
<text text-anchor="start" x="325.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
|
||||||
<text text-anchor="start" x="333.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A9->A12 -->
|
|
||||||
<g id="edge16" class="edge">
|
|
||||||
<title>A9->A12</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M354.683,-1039.9349C353.0582,-985.577 351.3338,-927.8888 350.095,-886.4463"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="349.7909,-876.272 354.5878,-886.1331 349.9404,-881.2698 350.0898,-886.2676 350.0898,-886.2676 350.0898,-886.2676 349.9404,-881.2698 345.5918,-886.4021 349.7909,-876.272 349.7909,-876.272"/>
|
|
||||||
</g>
|
|
||||||
<!-- A10 -->
|
|
||||||
<g id="node11" class="node">
|
|
||||||
<title>A10</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="72.348,-1284 72.348,-1316 163.348,-1316 163.348,-1284 72.348,-1284"/>
|
|
||||||
<text text-anchor="start" x="90.343" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="72.348,-1144 72.348,-1284 163.348,-1284 163.348,-1144 72.348,-1144"/>
|
|
||||||
<text text-anchor="start" x="92.5665" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
|
||||||
<text text-anchor="start" x="98.671" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
|
||||||
<text text-anchor="start" x="107.845" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
|
||||||
<text text-anchor="start" x="102.846" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
|
||||||
<text text-anchor="start" x="105.9055" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
|
||||||
<text text-anchor="start" x="110.904" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
|
||||||
<text text-anchor="start" x="90.058" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
|
||||||
<text text-anchor="start" x="91.732" y="-1169" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
|
||||||
<text text-anchor="start" x="103.96" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="72.348,-1076 72.348,-1144 163.348,-1144 163.348,-1076 72.348,-1076"/>
|
|
||||||
<text text-anchor="start" x="82.2885" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
|
||||||
<text text-anchor="start" x="98.402" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
|
||||||
<text text-anchor="start" x="102.8505" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A10->A3 -->
|
|
||||||
<g id="edge9" class="edge">
|
|
||||||
<title>A10->A3</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M85.2254,-1066.0442C81.4271,-1040.8951 78.2542,-1014.6889 76.348,-990 59.0619,-766.107 69.322,-500.0139 79.5568,-374.449"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="86.7695,-1075.9973 80.7895,-1066.8055 86.0029,-1071.0565 85.2364,-1066.1156 85.2364,-1066.1156 85.2364,-1066.1156 86.0029,-1071.0565 89.6832,-1065.4256 86.7695,-1075.9973 86.7695,-1075.9973"/>
|
|
||||||
<text text-anchor="middle" x="75.6382" y="-1056.3813" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
|
||||||
</g>
|
|
||||||
<!-- A10->A3 -->
|
|
||||||
<g id="edge11" class="edge">
|
|
||||||
<title>A10->A3</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M102.8817,-1066.0442C99.4271,-1040.8951 96.2542,-1014.6889 94.348,-990 77.6021,-773.1036 86.7076,-516.6035 90.1168,-386.6269"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="104.2701,-1075.9973 98.4317,-1066.7149 103.5793,-1071.0453 102.8885,-1066.0932 102.8885,-1066.0932 102.8885,-1066.0932 103.5793,-1071.0453 107.3454,-1065.4715 104.2701,-1075.9973 104.2701,-1075.9973"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="90.1213,-386.4451 86.2759,-380.3449 90.4278,-374.449 94.2733,-380.5493 90.1213,-386.4451"/>
|
|
||||||
<text text-anchor="middle" x="98.4146" y="-389.7851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
|
||||||
</g>
|
|
||||||
<!-- A10->A4 -->
|
|
||||||
<g id="edge17" class="edge">
|
|
||||||
<title>A10->A4</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M156.8842,-1075.7577C164.8622,-1051.494 173.3952,-1025.5424 181.8248,-999.9053"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="185.0491,-990.0992 186.2003,-1001.0045 183.4873,-994.849 181.9255,-999.5989 181.9255,-999.5989 181.9255,-999.5989 183.4873,-994.849 177.6506,-998.1932 185.0491,-990.0992 185.0491,-990.0992"/>
|
|
||||||
<text text-anchor="middle" x="154.5165" y="-1052.8983" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
|
||||||
</g>
|
|
||||||
<!-- A13 -->
|
|
||||||
<g id="node14" class="node">
|
|
||||||
<title>A13</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="95.348,-844 95.348,-876 162.348,-876 162.348,-844 95.348,-844"/>
|
|
||||||
<text text-anchor="start" x="108.0065" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="95.348,-824 95.348,-844 162.348,-844 162.348,-824 95.348,-824"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="95.348,-780 95.348,-824 162.348,-824 162.348,-780 95.348,-780"/>
|
|
||||||
<text text-anchor="start" x="105.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
|
||||||
<text text-anchor="start" x="113.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A10->A13 -->
|
|
||||||
<g id="edge18" class="edge">
|
|
||||||
<title>A10->A13</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M120.9422,-1075.7577C122.842,-1012.1995 125.0881,-937.0598 126.6045,-886.3285"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="126.9072,-876.2026 131.1063,-886.3326 126.7577,-881.2004 126.6083,-886.1981 126.6083,-886.1981 126.6083,-886.1981 126.7577,-881.2004 122.1103,-886.0636 126.9072,-876.2026 126.9072,-876.2026"/>
|
|
||||||
</g>
|
|
||||||
<!-- A11 -->
|
|
||||||
<g id="node12" class="node">
|
|
||||||
<title>A11</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="181.348,-1284 181.348,-1316 284.348,-1316 284.348,-1284 181.348,-1284"/>
|
|
||||||
<text text-anchor="start" x="222.01" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="181.348,-1228 181.348,-1284 284.348,-1284 284.348,-1228 181.348,-1228"/>
|
|
||||||
<text text-anchor="start" x="224.7895" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
|
||||||
<text text-anchor="start" x="200.334" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
|
||||||
<text text-anchor="start" x="213.9515" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="181.348,-1076 181.348,-1228 284.348,-1228 284.348,-1076 181.348,-1076"/>
|
|
||||||
<text text-anchor="start" x="208.6835" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
|
||||||
<text text-anchor="start" x="206.7325" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
|
||||||
<text text-anchor="start" x="203.6785" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
|
||||||
<text text-anchor="start" x="202.0085" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
|
||||||
<text text-anchor="start" x="200.058" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
|
||||||
<text text-anchor="start" x="215.061" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
|
||||||
<text text-anchor="start" x="207.842" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
|
||||||
<text text-anchor="start" x="209.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
|
||||||
<text text-anchor="start" x="193.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
|
||||||
<text text-anchor="start" x="202.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
|
||||||
<text text-anchor="start" x="191.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
|
||||||
</g>
|
|
||||||
<!-- A11->A12 -->
|
|
||||||
<g id="edge13" class="edge">
|
|
||||||
<title>A11->A12</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M280.9382,-1066.3836C289.808,-1041.1849 298.6736,-1014.8736 306.348,-990 318.0407,-952.103 329.205,-908.562 337.0802,-876.1703"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="277.5783,-1065.3868 277.541,-1075.9815 284.1771,-1067.7225 277.5783,-1065.3868"/>
|
|
||||||
</g>
|
|
||||||
<!-- A11->A13 -->
|
|
||||||
<g id="edge14" class="edge">
|
|
||||||
<title>A11->A13</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M192.4636,-1066.2454C184.9411,-1041.0063 177.2684,-1014.7156 170.348,-990 159.6936,-951.9489 148.506,-908.6064 140.3587,-876.3336"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="189.1205,-1067.2825 195.3374,-1075.8616 195.8274,-1065.2781 189.1205,-1067.2825"/>
|
|
||||||
</g>
|
|
||||||
<!-- A15 -->
|
|
||||||
<g id="node16" class="node">
|
|
||||||
<title>A15</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="150.348,-1550 150.348,-1582 231.348,-1582 231.348,-1550 150.348,-1550"/>
|
|
||||||
<text text-anchor="start" x="170.5655" y="-1563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="150.348,-1518 150.348,-1550 231.348,-1550 231.348,-1518 150.348,-1518"/>
|
|
||||||
<text text-anchor="start" x="173.0615" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="150.348,-1474 150.348,-1518 231.348,-1518 231.348,-1474 150.348,-1474"/>
|
|
||||||
<text text-anchor="start" x="161.6785" y="-1499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
|
||||||
<text text-anchor="start" x="160.0085" y="-1487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A14->A15 -->
|
|
||||||
<g id="edge20" class="edge">
|
|
||||||
<title>A14->A15</title>
|
|
||||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1693.5712C190.348,-1659.1613 190.348,-1615.9264 190.348,-1582.2666"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="186.8481,-1693.9463 190.348,-1703.9464 193.8481,-1693.9464 186.8481,-1693.9463"/>
|
|
||||||
</g>
|
|
||||||
<!-- A15->A9 -->
|
|
||||||
<g id="edge21" class="edge">
|
|
||||||
<title>A15->A9</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M229.7336,-1465.2507C249.8317,-1432.1375 274.0435,-1390.492 293.348,-1352 296.3143,-1346.0855 299.269,-1340.0055 302.1912,-1333.8349"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="226.6683,-1463.555 224.4506,-1473.9151 232.6449,-1467.1992 226.6683,-1463.555"/>
|
|
||||||
</g>
|
|
||||||
<!-- A15->A10 -->
|
|
||||||
<g id="edge22" class="edge">
|
|
||||||
<title>A15->A10</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M176.3052,-1464.1339C167.0994,-1422.2665 154.7762,-1366.2214 143.7769,-1316.197"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="172.8909,-1464.9045 178.4568,-1473.9196 179.7276,-1463.4012 172.8909,-1464.9045"/>
|
|
||||||
</g>
|
|
||||||
<!-- A16 -->
|
|
||||||
<g id="node17" class="node">
|
|
||||||
<title>A16</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="285.348,-1622 285.348,-1654 360.348,-1654 360.348,-1622 285.348,-1622"/>
|
|
||||||
<text text-anchor="start" x="305.0655" y="-1635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="285.348,-1470 285.348,-1622 360.348,-1622 360.348,-1470 285.348,-1470"/>
|
|
||||||
<text text-anchor="start" x="314.5095" y="-1603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
|
||||||
<text text-anchor="start" x="295.338" y="-1579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
|
||||||
<text text-anchor="start" x="296.453" y="-1567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
|
||||||
<text text-anchor="start" x="306.4565" y="-1555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
|
||||||
<text text-anchor="start" x="296.7375" y="-1543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
|
||||||
<text text-anchor="start" x="304.79" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
|
||||||
<text text-anchor="start" x="316.7395" y="-1519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
|
||||||
<text text-anchor="start" x="303.4015" y="-1507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
|
||||||
<text text-anchor="start" x="301.727" y="-1495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
|
||||||
<text text-anchor="start" x="316.1845" y="-1483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="285.348,-1402 285.348,-1470 360.348,-1470 360.348,-1402 285.348,-1402"/>
|
|
||||||
<text text-anchor="start" x="296.738" y="-1451" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
|
||||||
<text text-anchor="start" x="300.072" y="-1439" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
|
||||||
<text text-anchor="start" x="297.572" y="-1427" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
|
||||||
<text text-anchor="start" x="307.8505" y="-1415" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A16->A9 -->
|
|
||||||
<g id="edge24" class="edge">
|
|
||||||
<title>A16->A9</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M337.5861,-1391.2689C339.0212,-1378.3918 340.4833,-1365.272 341.9365,-1352.2329"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="336.4417,-1401.5379 333.077,-1391.101 336.9955,-1396.5687 337.5493,-1391.5995 337.5493,-1391.5995 337.5493,-1391.5995 336.9955,-1396.5687 342.0217,-1392.0979 336.4417,-1401.5379 336.4417,-1401.5379"/>
|
|
||||||
<text text-anchor="middle" x="348.3292" y="-1368.1837" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
|
||||||
<text text-anchor="middle" x="330.049" y="-1379.5871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
|
||||||
</g>
|
|
||||||
<!-- A16->A10 -->
|
|
||||||
<g id="edge23" class="edge">
|
|
||||||
<title>A16->A10</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M279.1725,-1451.7757C267.6828,-1434.4941 254.4915,-1416.871 240.348,-1402 214.2483,-1374.5578 193.9188,-1382.411 171.348,-1352 163.2346,-1341.0684 156.273,-1328.8227 150.315,-1316.1269"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="284.6729,-1460.2184 275.4437,-1454.2962 281.9435,-1456.0291 279.2141,-1451.8397 279.2141,-1451.8397 279.2141,-1451.8397 281.9435,-1456.0291 282.9845,-1449.3833 284.6729,-1460.2184 284.6729,-1460.2184"/>
|
|
||||||
<text text-anchor="middle" x="165.7688" y="-1325.8225" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
|
||||||
<text text-anchor="middle" x="267.6964" y="-1446.645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 38 KiB |
@@ -2,5 +2,7 @@
|
|||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
python-dotenv
|
||||||
mock
|
mock
|
||||||
coverage
|
coverage
|
||||||
|
jinja2-cli
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
aiomqtt==2.3.0
|
aiomqtt==2.3.0
|
||||||
schema==0.7.7
|
schema==0.7.7
|
||||||
aiocron==1.8
|
aiocron==2.1
|
||||||
aiohttp==3.10.11
|
aiohttp==3.11.14
|
||||||
@@ -6,16 +6,10 @@ from asyncio import StreamReader, StreamWriter
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
if __name__ == "app.src.async_stream":
|
from proxy import Proxy
|
||||||
from app.src.proxy import Proxy
|
from byte_fifo import ByteFifo
|
||||||
from app.src.byte_fifo import ByteFifo
|
from async_ifc import AsyncIfc
|
||||||
from app.src.async_ifc import AsyncIfc
|
from infos import Infos
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from proxy import Proxy
|
|
||||||
from byte_fifo import ByteFifo
|
|
||||||
from async_ifc import AsyncIfc
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
@@ -221,7 +215,6 @@ class AsyncStream(AsyncIfcImpl):
|
|||||||
|
|
||||||
async def disc(self) -> None:
|
async def disc(self) -> None:
|
||||||
"""Async disc handler for graceful disconnect"""
|
"""Async disc handler for graceful disconnect"""
|
||||||
self.remote = None
|
|
||||||
if self._writer.is_closing():
|
if self._writer.is_closing():
|
||||||
return
|
return
|
||||||
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
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"Fwd Exception for {self.r_addr}:\n"
|
||||||
f"{traceback.format_exc()}")
|
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):
|
class AsyncStreamServer(AsyncStream):
|
||||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
@@ -355,14 +356,6 @@ class AsyncStreamServer(AsyncStream):
|
|||||||
self.remote.ifc._writer.write(self.fwd_fifo.get())
|
self.remote.ifc._writer.write(self.fwd_fifo.get())
|
||||||
await self.remote.ifc._writer.drain()
|
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):
|
class AsyncStreamClient(AsyncStream):
|
||||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
@@ -370,6 +363,11 @@ class AsyncStreamClient(AsyncStream):
|
|||||||
AsyncStream.__init__(self, reader, writer, rstream)
|
AsyncStream.__init__(self, reader, writer, rstream)
|
||||||
self.close_cb = close_cb
|
self.close_cb = close_cb
|
||||||
|
|
||||||
|
async def disc(self) -> None:
|
||||||
|
logging.debug('AsyncStreamClient.disc()')
|
||||||
|
self.remote = None
|
||||||
|
await super().disc()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug('AsyncStreamClient.close()')
|
logging.debug('AsyncStreamClient.close()')
|
||||||
self.close_cb = None
|
self.close_cb = None
|
||||||
@@ -377,7 +375,11 @@ class AsyncStreamClient(AsyncStream):
|
|||||||
|
|
||||||
async def client_loop(self, _: str) -> None:
|
async def client_loop(self, _: str) -> None:
|
||||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
|
Infos.inc_counter('Cloud_Conn_Cnt')
|
||||||
|
await self.publish_outstanding_mqtt()
|
||||||
await self.loop()
|
await self.loop()
|
||||||
|
Infos.dec_counter('Cloud_Conn_Cnt')
|
||||||
|
await self.publish_outstanding_mqtt()
|
||||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||||
'Client loop stopped for'
|
'Client loop stopped for'
|
||||||
f' l{self.l_addr}')
|
f' l{self.l_addr}')
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
|
from messages import hex_dump_str, hex_dump_memory
|
||||||
if __name__ == "app.src.byte_fifo":
|
|
||||||
from app.src.messages import hex_dump_str, hex_dump_memory
|
|
||||||
else: # pragma: no cover
|
|
||||||
from messages import hex_dump_str, hex_dump_memory
|
|
||||||
|
|
||||||
|
|
||||||
class ByteFifo:
|
class ByteFifo:
|
||||||
|
|||||||
249
app/src/cnf/config.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
'''Config module handles the proxy configuration'''
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
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 build and sanitize the internal config dictenary.
|
||||||
|
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
conf_schema = Schema({
|
||||||
|
'tsun': {
|
||||||
|
'enabled': Use(bool),
|
||||||
|
'host': Use(str),
|
||||||
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
|
},
|
||||||
|
'solarman': {
|
||||||
|
'enabled': Use(bool),
|
||||||
|
'host': Use(str),
|
||||||
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
|
},
|
||||||
|
'mqtt': {
|
||||||
|
'host': Use(str),
|
||||||
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||||
|
'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),
|
||||||
|
'discovery_prefix': Use(str),
|
||||||
|
'entity_prefix': Use(str),
|
||||||
|
'proxy_node_id': Use(str),
|
||||||
|
'proxy_unique_id': Use(str)
|
||||||
|
},
|
||||||
|
'gen3plus': {
|
||||||
|
'at_acl': {
|
||||||
|
Or('mqtt', 'tsun'): {
|
||||||
|
'allow': [str],
|
||||||
|
Optional('block', default=[]): [str]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'inverters': {
|
||||||
|
'allow_all': Use(bool),
|
||||||
|
And(Use(str), lambda s: len(s) == 16): {
|
||||||
|
Optional('monitor_sn', default=0): Use(int),
|
||||||
|
Optional('node_id', default=""): And(Use(str),
|
||||||
|
Use(lambda s: s + '/'
|
||||||
|
if len(s) > 0
|
||||||
|
and s[-1] != '/'
|
||||||
|
else s)),
|
||||||
|
Optional('client_mode'): {
|
||||||
|
'host': Use(str),
|
||||||
|
Optional('port', default=8899):
|
||||||
|
And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||||
|
Optional('forward', default=False): Use(bool),
|
||||||
|
},
|
||||||
|
Optional('modbus_polling', default=True): Use(bool),
|
||||||
|
Optional('modbus_scanning'): {
|
||||||
|
'start': Use(int),
|
||||||
|
Optional('step', default=0x400): Use(int),
|
||||||
|
Optional('bytes', default=0x10): Use(int),
|
||||||
|
},
|
||||||
|
Optional('suggested_area', default=""): Use(str),
|
||||||
|
Optional('sensor_list', default=0): Use(int),
|
||||||
|
Optional('pv1'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv2'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv3'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv4'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv5'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv6'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'batteries': {
|
||||||
|
And(Use(str), lambda s: len(s) == 16): {
|
||||||
|
Optional('monitor_sn', default=0): Use(int),
|
||||||
|
Optional('node_id', default=""): And(Use(str),
|
||||||
|
Use(lambda s: s + '/'
|
||||||
|
if len(s) > 0
|
||||||
|
and s[-1] != '/'
|
||||||
|
else s)),
|
||||||
|
Optional('client_mode'): {
|
||||||
|
'host': Use(str),
|
||||||
|
Optional('port', default=8899):
|
||||||
|
And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||||
|
Optional('forward', default=False): Use(bool),
|
||||||
|
},
|
||||||
|
Optional('modbus_polling', default=True): Use(bool),
|
||||||
|
Optional('modbus_scanning'): {
|
||||||
|
'start': Use(int),
|
||||||
|
Optional('step', default=0x400): Use(int),
|
||||||
|
Optional('bytes', default=0x10): Use(int),
|
||||||
|
},
|
||||||
|
Optional('suggested_area', default=""): Use(str),
|
||||||
|
Optional('sensor_list', default=0): Use(int),
|
||||||
|
Optional('pv1'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
},
|
||||||
|
Optional('pv2'): {
|
||||||
|
Optional('type'): Use(str),
|
||||||
|
Optional('manufacturer'): Use(str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ignore_extra_keys=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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
|
||||||
|
logging.debug('Copy Default Config to config.example.toml')
|
||||||
|
|
||||||
|
shutil.copy2("default_config.toml",
|
||||||
|
"config/config.example.toml")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# read example config file as default configuration
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
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 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'''
|
||||||
|
res = 'ok'
|
||||||
|
try:
|
||||||
|
rd_config = reader.get_config()
|
||||||
|
config = cls.act_config.copy()
|
||||||
|
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
|
||||||
|
'gen3plus', 'batteries']:
|
||||||
|
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:
|
||||||
|
cls.err = f'error: {error}'
|
||||||
|
logging.error(
|
||||||
|
f"Can't read from {reader.descr()} => error\n {error}")
|
||||||
|
return cls.err
|
||||||
|
|
||||||
|
logging.info(f'Read from {reader.descr()} => {res}')
|
||||||
|
return cls.err
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, member: str = None):
|
||||||
|
'''Get a named attribute from the proxy config. If member ==
|
||||||
|
None it returns the complete config dict'''
|
||||||
|
|
||||||
|
if member:
|
||||||
|
return cls.act_config.get(member, {})
|
||||||
|
else:
|
||||||
|
return cls.act_config
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_default(cls, member: str) -> bool:
|
||||||
|
'''Check if the member is the default value'''
|
||||||
|
|
||||||
|
return cls.act_config.get(member) == cls.def_config.get(member)
|
||||||
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"
|
||||||
47
app/src/cnf/config_read_json.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
'''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' or key == 'batteries') 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
@@ -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
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
|
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
|
||||||
###
|
###
|
||||||
### Here you will find a description of all configuration options:
|
### 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
|
### 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
|
### 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
|
## 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.
|
## 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
|
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
|
## 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.
|
## 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
|
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
|
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||||
## to use the TSUN APPs or receive firmware updates.
|
## 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
|
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
|
## inverters. This connection is only required if you want send data to the TSUN cloud
|
||||||
## to use the TSUN APPs or receive firmware updates.
|
## 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.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||||
@@ -113,7 +113,7 @@ inverters.allow_all = false # only allow known inverters
|
|||||||
##
|
##
|
||||||
## For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
|
## 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
|
## definition. To do this, the corresponding configuration block is started with
|
||||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
|
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
|
||||||
## in the configuration block
|
## in the configuration block
|
||||||
##
|
##
|
||||||
@@ -132,7 +132,7 @@ pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module de
|
|||||||
##
|
##
|
||||||
## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
|
## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
|
||||||
## definition. To do this, the corresponding configuration block is started with
|
## definition. To do this, the corresponding configuration block is started with
|
||||||
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||||
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
|
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
|
||||||
## can be set in the configuration block
|
## can be set in the configuration block
|
||||||
##
|
##
|
||||||
@@ -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
|
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||||
# the next line and configure the fixed IP of your inverter
|
# the next line and configure the fixed IP of your inverter
|
||||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||||
|
|
||||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
@@ -157,6 +157,33 @@ pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de
|
|||||||
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
|
|
||||||
|
##########################################################################################
|
||||||
|
##
|
||||||
|
## For each GEN3PLUS enrgy storage system, the serial number must be mapped to an MQTT
|
||||||
|
## definition. To do this, the corresponding configuration block is started with
|
||||||
|
## `[batteries.“<16-digit serial number>”]` so that all subsequent parameters are assigned
|
||||||
|
## to this energy storage system. Further device-specific parameters (e.g. polling mode,
|
||||||
|
## client mode) can be set in the configuration block
|
||||||
|
##
|
||||||
|
## The serial numbers of all GEN3PLUS energy storage systems/batteries start with `410`!
|
||||||
|
## Each GEN3PLUS device is supplied with a “Monitoring SN:”. This can be found on a
|
||||||
|
## sticker enclosed with the inverter.
|
||||||
|
##
|
||||||
|
|
||||||
|
[batteries."4100000000000001"]
|
||||||
|
monitor_sn = 3000000000 # The GEN3PLUS "Monitoring SN:"
|
||||||
|
node_id = '' # MQTT replacement for devices serial number
|
||||||
|
suggested_area = '' # suggested installation place for home-assistant
|
||||||
|
modbus_polling = true # Enable optional MODBUS polling
|
||||||
|
|
||||||
|
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||||
|
# the next line and configure the fixed IP of your inverter
|
||||||
|
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||||
|
|
||||||
|
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
|
|
||||||
##########################################################################################
|
##########################################################################################
|
||||||
###
|
###
|
||||||
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via
|
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
'''Config module handles the proxy configuration in the config.toml file'''
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import tomllib
|
|
||||||
import logging
|
|
||||||
from schema import Schema, And, Or, Use, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Config():
|
|
||||||
'''Static class Config is reads and sanitize the config.
|
|
||||||
|
|
||||||
Read config.toml file and sanitize it with read().
|
|
||||||
Get named parts of the config with get()'''
|
|
||||||
|
|
||||||
act_config = {}
|
|
||||||
def_config = {}
|
|
||||||
conf_schema = Schema({
|
|
||||||
'tsun': {
|
|
||||||
'enabled': Use(bool),
|
|
||||||
'host': Use(str),
|
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
|
||||||
},
|
|
||||||
'solarman': {
|
|
||||||
'enabled': Use(bool),
|
|
||||||
'host': Use(str),
|
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
|
||||||
},
|
|
||||||
'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))
|
|
||||||
},
|
|
||||||
'ha': {
|
|
||||||
'auto_conf_prefix': Use(str),
|
|
||||||
'discovery_prefix': Use(str),
|
|
||||||
'entity_prefix': Use(str),
|
|
||||||
'proxy_node_id': Use(str),
|
|
||||||
'proxy_unique_id': Use(str)
|
|
||||||
},
|
|
||||||
'gen3plus': {
|
|
||||||
'at_acl': {
|
|
||||||
Or('mqtt', 'tsun'): {
|
|
||||||
'allow': [str],
|
|
||||||
Optional('block', default=[]): [str]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'inverters': {
|
|
||||||
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
|
||||||
Optional('monitor_sn', default=0): Use(int),
|
|
||||||
Optional('node_id', default=""): And(Use(str),
|
|
||||||
Use(lambda s: s + '/'
|
|
||||||
if len(s) > 0
|
|
||||||
and s[-1] != '/'
|
|
||||||
else s)),
|
|
||||||
Optional('client_mode'): {
|
|
||||||
'host': Use(str),
|
|
||||||
Optional('port', default=8899):
|
|
||||||
And(Use(int), lambda n: 1024 <= n <= 65535)
|
|
||||||
},
|
|
||||||
Optional('modbus_polling', default=True): Use(bool),
|
|
||||||
Optional('suggested_area', default=""): Use(str),
|
|
||||||
Optional('sensor_list', default=0x2b0): Use(int),
|
|
||||||
Optional('pv1'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
},
|
|
||||||
Optional('pv2'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
},
|
|
||||||
Optional('pv3'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
},
|
|
||||||
Optional('pv4'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
},
|
|
||||||
Optional('pv5'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
},
|
|
||||||
Optional('pv6'): {
|
|
||||||
Optional('type'): Use(str),
|
|
||||||
Optional('manufacturer'): Use(str),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, ignore_extra_keys=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def class_init(cls) -> None | str: # pragma: no cover
|
|
||||||
try:
|
|
||||||
# make the default config transparaent by copying it
|
|
||||||
# in the config.example file
|
|
||||||
logging.debug('Copy Default Config to config.example.toml')
|
|
||||||
|
|
||||||
shutil.copy2("default_config.toml",
|
|
||||||
"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 = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open("config/config.toml", "rb") as f:
|
|
||||||
usr_config = tomllib.load(f)
|
|
||||||
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
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def read(cls, path='') -> None | str:
|
|
||||||
'''Read config file, merge it with the default config
|
|
||||||
and sanitize the result'''
|
|
||||||
err = None
|
|
||||||
config = {}
|
|
||||||
logger = logging.getLogger('data')
|
|
||||||
|
|
||||||
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()
|
|
||||||
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}" ')
|
|
||||||
|
|
||||||
except Exception as error:
|
|
||||||
err = f'Config.read: {error}'
|
|
||||||
logger.error(err)
|
|
||||||
cls.act_config = {}
|
|
||||||
|
|
||||||
return err
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, member: str = None):
|
|
||||||
'''Get a named attribute from the proxy config. If member ==
|
|
||||||
None it returns the complete config dict'''
|
|
||||||
|
|
||||||
if member:
|
|
||||||
return cls.act_config.get(member, {})
|
|
||||||
else:
|
|
||||||
return cls.act_config
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_default(cls, member: str) -> bool:
|
|
||||||
'''Check if the member is the default value'''
|
|
||||||
|
|
||||||
return cls.act_config.get(member) == cls.def_config.get(member)
|
|
||||||
@@ -2,29 +2,14 @@
|
|||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
if __name__ == "app.src.gen3.infos_g3":
|
from infos import Infos, Register
|
||||||
from app.src.infos import Infos, Register
|
|
||||||
else: # pragma: no cover
|
|
||||||
from infos import Infos, Register
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterMap:
|
class RegisterMap:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
map = {
|
map = {
|
||||||
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
|
|
||||||
0x000927c0: {'reg': Register.CHIP_TYPE},
|
|
||||||
0x00092f90: {'reg': Register.CHIP_MODEL},
|
|
||||||
0x00094ae8: {'reg': Register.MAC_ADDR},
|
|
||||||
0x00095a88: {'reg': Register.TRACE_URL},
|
|
||||||
0x00095aec: {'reg': Register.LOGGER_URL},
|
|
||||||
0x0000000a: {'reg': Register.PRODUCT_NAME},
|
|
||||||
0x00000014: {'reg': Register.MANUFACTURER},
|
|
||||||
0x0000001e: {'reg': Register.VERSION},
|
|
||||||
0x00000028: {'reg': Register.SERIAL_NUMBER},
|
|
||||||
0x00000032: {'reg': Register.EQUIPMENT_MODEL},
|
|
||||||
0x00013880: {'reg': Register.NO_INPUTS},
|
|
||||||
0xffffff00: {'reg': Register.INVERTER_CNT},
|
0xffffff00: {'reg': Register.INVERTER_CNT},
|
||||||
0xffffff01: {'reg': Register.UNKNOWN_SNR},
|
0xffffff01: {'reg': Register.UNKNOWN_SNR},
|
||||||
0xffffff02: {'reg': Register.UNKNOWN_MSG},
|
0xffffff02: {'reg': Register.UNKNOWN_MSG},
|
||||||
@@ -36,6 +21,100 @@ class RegisterMap:
|
|||||||
0xffffff08: {'reg': Register.POLLING_INTERVAL},
|
0xffffff08: {'reg': Register.POLLING_INTERVAL},
|
||||||
0xfffffffe: {'reg': Register.TEST_REG1},
|
0xfffffffe: {'reg': Register.TEST_REG1},
|
||||||
0xffffffff: {'reg': Register.TEST_REG2},
|
0xffffffff: {'reg': Register.TEST_REG2},
|
||||||
|
}
|
||||||
|
map_0e100000 = {
|
||||||
|
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
|
||||||
|
0x000927c0: {'reg': Register.CHIP_TYPE},
|
||||||
|
0x00092f90: {'reg': Register.CHIP_MODEL},
|
||||||
|
0x00094ae8: {'reg': Register.MAC_ADDR},
|
||||||
|
0x00095a88: {'reg': Register.TRACE_URL},
|
||||||
|
0x00095aec: {'reg': Register.LOGGER_URL},
|
||||||
|
0x000cfc38: {'reg': Register.CONNECT_COUNT},
|
||||||
|
0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
|
||||||
|
0x000c96a8: {'reg': Register.POWER_ON_TIME},
|
||||||
|
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
|
||||||
|
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
|
||||||
|
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
|
||||||
|
}
|
||||||
|
map_01900000 = {
|
||||||
|
0x0000000a: {'reg': Register.PRODUCT_NAME},
|
||||||
|
0x00000014: {'reg': Register.MANUFACTURER},
|
||||||
|
0x0000001e: {'reg': Register.VERSION},
|
||||||
|
0x00000046: {'reg': Register.SERIAL_NUMBER},
|
||||||
|
0x0000005A: {'reg': Register.EQUIPMENT_MODEL},
|
||||||
|
0x00000064: {'reg': Register.INVERTER_STATUS},
|
||||||
|
0x00000190: {'reg': Register.EVENT_ALARM},
|
||||||
|
0x000001f4: {'reg': Register.EVENT_FAULT},
|
||||||
|
0x00000258: {'reg': Register.EVENT_BF1},
|
||||||
|
0x000002bc: {'reg': Register.EVENT_BF2},
|
||||||
|
0x00000320: {'reg': Register.TEST_IVAL_1},
|
||||||
|
0x000003e8: {'reg': Register.TEST_VAL_0},
|
||||||
|
0x0000044c: {'reg': Register.TEST_VAL_1}, # DC 1 Inpput Voltage *10
|
||||||
|
0x000004b0: {'reg': Register.TEST_VAL_2},
|
||||||
|
0x00000514: {'reg': Register.GRID_VOLTAGE}, # Grid Voltage
|
||||||
|
0x00000578: {'reg': Register.GRID_CURRENT}, # Grid Current
|
||||||
|
0x000005dc: {'reg': Register.TEST_VAL_3},
|
||||||
|
0x00000640: {'reg': Register.GRID_FREQUENCY},
|
||||||
|
0x000006a4: {'reg': Register.TEST_IVAL_2},
|
||||||
|
0x00000708: {'reg': Register.TEST_IVAL_3},
|
||||||
|
0x0000076c: {'reg': Register.TEST_IVAL_4},
|
||||||
|
0x000007d0: {'reg': Register.TEST_VAL_4}, # DC 2 Input Voltage *10
|
||||||
|
0x00000834: {'reg': Register.MAX_DESIGNED_POWER},
|
||||||
|
0x00000898: {'reg': Register.OUTPUT_POWER}, # Grid Power
|
||||||
|
0x000008fc: {'reg': Register.DAILY_GENERATION}, # Daily Generation
|
||||||
|
0x00000960: {'reg': Register.TOTAL_GENERATION}, # Total Genration
|
||||||
|
0x000009c4: {'reg': Register.TEST_IVAL_5},
|
||||||
|
0x00000a28: {'reg': Register.TEST_VAL_10}, # Isolationsimpedanz Rx
|
||||||
|
0x00000a8c: {'reg': Register.TEST_VAL_11}, # Isolationsimpedanz Ry
|
||||||
|
0x00000af0: {'reg': Register.TEST_IVAL_6},
|
||||||
|
0x000001324: {'reg': Register.PV1_VOLTAGE}, # PV1 Voltage
|
||||||
|
0x000001388: {'reg': Register.PV1_CURRENT}, # PV1 Current
|
||||||
|
0x0000013ec: {'reg': Register.PV1_POWER}, # PV1 Power
|
||||||
|
0x000001450: {'reg': Register.TEST_VAL_5},
|
||||||
|
0x0000015e0: {'reg': Register.PV2_VOLTAGE}, # PV2 Voltage
|
||||||
|
0x000001644: {'reg': Register.PV2_CURRENT}, # PV2 Current
|
||||||
|
0x0000016a8: {'reg': Register.PV2_POWER}, # PV2 Power
|
||||||
|
0x00000170c: {'reg': Register.TEST_VAL_6},
|
||||||
|
0x00000189c: {'reg': Register.PV3_VOLTAGE},
|
||||||
|
0x000001900: {'reg': Register.PV3_CURRENT},
|
||||||
|
0x000001964: {'reg': Register.PV3_POWER},
|
||||||
|
0x0000019c8: {'reg': Register.TEST_VAL_7},
|
||||||
|
0x000001c20: {'reg': Register.TEST_VAL_14},
|
||||||
|
0x000001c84: {'reg': Register.TEST_VAL_15},
|
||||||
|
0x000001ce8: {'reg': Register.TEST_VAL_16}, # DC 1 Voltage
|
||||||
|
0x000001d4c: {'reg': Register.TEST_VAL_17},
|
||||||
|
0x000001db0: {'reg': Register.TEST_VAL_18},
|
||||||
|
0x000001e14: {'reg': Register.TEST_IVAL_8},
|
||||||
|
0x000001e78: {'reg': Register.PV4_VOLTAGE},
|
||||||
|
0x000001edc: {'reg': Register.PV4_CURRENT},
|
||||||
|
0x000001f40: {'reg': Register.PV4_POWER},
|
||||||
|
0x000001fa4: {'reg': Register.TEST_VAL_8},
|
||||||
|
0x0000020c9: {'reg': Register.TEST_IVAL_9},
|
||||||
|
0x0000020db: {'reg': Register.TEST_IVAL_10},
|
||||||
|
|
||||||
|
0x000002134: {'reg': Register.PV5_VOLTAGE},
|
||||||
|
0x000002198: {'reg': Register.PV5_CURRENT},
|
||||||
|
0x0000021fc: {'reg': Register.PV5_POWER},
|
||||||
|
# 0x000002260: {'reg': Register.TEST_VAL_13},
|
||||||
|
0x0000023f0: {'reg': Register.PV6_VOLTAGE},
|
||||||
|
0x000002454: {'reg': Register.PV6_CURRENT},
|
||||||
|
0x0000024b8: {'reg': Register.PV6_POWER},
|
||||||
|
# 0x00000251c: {'reg': Register.TEST_VAL_14},
|
||||||
|
0x000002774: {'reg': Register.TEST_VAL_24},
|
||||||
|
0x0000027d8: {'reg': Register.TEST_VAL_25},
|
||||||
|
0x00000283c: {'reg': Register.TEST_VAL_26}, # DC 2 Voltage
|
||||||
|
0x0000028a0: {'reg': Register.TEST_VAL_27},
|
||||||
|
0x000002904: {'reg': Register.TEST_VAL_28},
|
||||||
|
0x000002968: {'reg': Register.TEST_IVAL_11},
|
||||||
|
0x0000029cc: {'reg': Register.TEST_IVAL_12},
|
||||||
|
}
|
||||||
|
map_01900001 = {
|
||||||
|
0x0000000a: {'reg': Register.PRODUCT_NAME},
|
||||||
|
0x00000014: {'reg': Register.MANUFACTURER},
|
||||||
|
0x0000001e: {'reg': Register.VERSION},
|
||||||
|
0x00000028: {'reg': Register.SERIAL_NUMBER},
|
||||||
|
0x00000032: {'reg': Register.EQUIPMENT_MODEL},
|
||||||
|
0x00013880: {'reg': Register.NO_INPUTS},
|
||||||
0x00000640: {'reg': Register.OUTPUT_POWER},
|
0x00000640: {'reg': Register.OUTPUT_POWER},
|
||||||
0x000005dc: {'reg': Register.RATED_POWER},
|
0x000005dc: {'reg': Register.RATED_POWER},
|
||||||
0x00000514: {'reg': Register.INVERTER_TEMP},
|
0x00000514: {'reg': Register.INVERTER_TEMP},
|
||||||
@@ -64,34 +143,38 @@ class RegisterMap:
|
|||||||
0x000003e8: {'reg': Register.GRID_VOLTAGE},
|
0x000003e8: {'reg': Register.GRID_VOLTAGE},
|
||||||
0x0000044c: {'reg': Register.GRID_CURRENT},
|
0x0000044c: {'reg': Register.GRID_CURRENT},
|
||||||
0x000004b0: {'reg': Register.GRID_FREQUENCY},
|
0x000004b0: {'reg': Register.GRID_FREQUENCY},
|
||||||
0x000cfc38: {'reg': Register.CONNECT_COUNT},
|
|
||||||
0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
|
0x00000190: {'reg': Register.EVENT_ALARM},
|
||||||
0x000c96a8: {'reg': Register.POWER_ON_TIME},
|
0x000001f4: {'reg': Register.EVENT_FAULT},
|
||||||
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
|
0x00000258: {'reg': Register.EVENT_BF1},
|
||||||
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
|
0x000002bc: {'reg': Register.EVENT_BF2},
|
||||||
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
|
|
||||||
0x00000191: {'reg': Register.EVENT_401},
|
|
||||||
0x00000192: {'reg': Register.EVENT_402},
|
|
||||||
0x00000193: {'reg': Register.EVENT_403},
|
|
||||||
0x00000194: {'reg': Register.EVENT_404},
|
|
||||||
0x00000195: {'reg': Register.EVENT_405},
|
|
||||||
0x00000196: {'reg': Register.EVENT_406},
|
|
||||||
0x00000197: {'reg': Register.EVENT_407},
|
|
||||||
0x00000198: {'reg': Register.EVENT_408},
|
|
||||||
0x00000199: {'reg': Register.EVENT_409},
|
|
||||||
0x0000019a: {'reg': Register.EVENT_410},
|
|
||||||
0x0000019b: {'reg': Register.EVENT_411},
|
|
||||||
0x0000019c: {'reg': Register.EVENT_412},
|
|
||||||
0x0000019d: {'reg': Register.EVENT_413},
|
|
||||||
0x0000019e: {'reg': Register.EVENT_414},
|
|
||||||
0x0000019f: {'reg': Register.EVENT_415},
|
|
||||||
0x000001a0: {'reg': Register.EVENT_416},
|
|
||||||
0x00000064: {'reg': Register.INVERTER_STATUS},
|
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},
|
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},
|
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterSel:
|
||||||
|
__sensor_map = {
|
||||||
|
0x0e100000: RegisterMap.map_0e100000,
|
||||||
|
0x01900000: RegisterMap.map_01900000,
|
||||||
|
0x01900001: RegisterMap.map_01900001,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, sensor: int):
|
||||||
|
return cls.__sensor_map.get(sensor, RegisterMap.map)
|
||||||
|
|
||||||
|
|
||||||
class InfosG3(Infos):
|
class InfosG3(Infos):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
@@ -107,18 +190,27 @@ class InfosG3(Infos):
|
|||||||
entity strings
|
entity strings
|
||||||
sug_area:str ==> suggested area string from the config file'''
|
sug_area:str ==> suggested area string from the config file'''
|
||||||
# iterate over RegisterMap.map and get the register values
|
# iterate over RegisterMap.map and get the register values
|
||||||
for row in RegisterMap.map.values():
|
sensor = self.get_db_value(Register.SENSOR_LIST)
|
||||||
|
if "01900000" == sensor:
|
||||||
|
items = RegisterMap.map_01900000.items()
|
||||||
|
elif "01900001" == sensor:
|
||||||
|
items = RegisterMap.map_01900001.items()
|
||||||
|
else:
|
||||||
|
items = {}
|
||||||
|
|
||||||
|
for _, row in chain(RegisterMap.map_0e100000.items(), items):
|
||||||
reg = row['reg']
|
reg = row['reg']
|
||||||
res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||||
if res:
|
if res:
|
||||||
yield res
|
yield res
|
||||||
|
|
||||||
def parse(self, buf, ind=0, node_id: str = '') -> \
|
def parse(self, buf, ind=0, sensor: int = 0, node_id: str = '') -> \
|
||||||
Generator[tuple[str, bool], None, None]:
|
Generator[tuple[str, bool], None, None]:
|
||||||
'''parse a data sequence received from the inverter and
|
'''parse a data sequence received from the inverter and
|
||||||
stores the values in Infos.db
|
stores the values in Infos.db
|
||||||
|
|
||||||
buf: buffer of the sequence to parse'''
|
buf: buffer of the sequence to parse'''
|
||||||
|
reg_map = RegisterSel.get(sensor)
|
||||||
result = struct.unpack_from('!l', buf, ind)
|
result = struct.unpack_from('!l', buf, ind)
|
||||||
elms = result[0]
|
elms = result[0]
|
||||||
i = 0
|
i = 0
|
||||||
@@ -126,11 +218,11 @@ class InfosG3(Infos):
|
|||||||
while i < elms:
|
while i < elms:
|
||||||
result = struct.unpack_from('!lB', buf, ind)
|
result = struct.unpack_from('!lB', buf, ind)
|
||||||
addr = result[0]
|
addr = result[0]
|
||||||
if addr not in RegisterMap.map:
|
if addr not in reg_map:
|
||||||
row = None
|
row = None
|
||||||
info_id = -1
|
info_id = -1
|
||||||
else:
|
else:
|
||||||
row = RegisterMap.map[addr]
|
row = reg_map[addr]
|
||||||
info_id = row['reg']
|
info_id = row['reg']
|
||||||
data_type = result[1]
|
data_type = result[1]
|
||||||
ind += 5
|
ind += 5
|
||||||
@@ -183,11 +275,8 @@ class InfosG3(Infos):
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
def __modify_val(self, row, result):
|
def __modify_val(self, row, result):
|
||||||
if row:
|
if row and 'ratio' in row:
|
||||||
if 'eval' in row:
|
result = round(result * row['ratio'], 2)
|
||||||
result = eval(row['eval'])
|
|
||||||
if 'ratio' in row:
|
|
||||||
result = round(result * row['ratio'], 2)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __store_result(self, addr, result, info_id, node_id):
|
def __store_result(self, addr, result, info_id, node_id):
|
||||||
@@ -201,3 +290,6 @@ class InfosG3(Infos):
|
|||||||
if update:
|
if update:
|
||||||
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
||||||
f' {result}{unit}')
|
f' {result}{unit}')
|
||||||
|
|
||||||
|
logging.log(level, f'[{node_id}] GEN3: {name} :'
|
||||||
|
f' {result}{unit}')
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
if __name__ == "app.src.gen3.inverter_g3":
|
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.gen3.talent import Talent
|
from gen3.talent import Talent
|
||||||
else: # pragma: no cover
|
|
||||||
from inverter_base import InverterBase
|
|
||||||
from gen3.talent import Talent
|
|
||||||
|
|
||||||
|
|
||||||
class InverterG3(InverterBase):
|
class InverterG3(InverterBase):
|
||||||
|
|||||||
@@ -4,22 +4,12 @@ from zoneinfo import ZoneInfo
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
if __name__ == "app.src.gen3.talent":
|
from async_ifc import AsyncIfc
|
||||||
from app.src.async_ifc import AsyncIfc
|
from messages import Message, State
|
||||||
from app.src.messages import Message, State
|
from modbus import Modbus
|
||||||
from app.src.modbus import Modbus
|
from cnf.config import Config
|
||||||
from app.src.my_timer import Timer
|
from gen3.infos_g3 import InfosG3
|
||||||
from app.src.config import Config
|
from infos import Register
|
||||||
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
|
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -42,19 +32,18 @@ class Control:
|
|||||||
|
|
||||||
|
|
||||||
class Talent(Message):
|
class Talent(Message):
|
||||||
MB_START_TIMEOUT = 40
|
|
||||||
MB_REGULAR_TIMEOUT = 60
|
|
||||||
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
|
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
|
||||||
|
|
||||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||||
client_mode: bool = False, id_str=b''):
|
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.rx_set_cb(self.read)
|
||||||
ifc.prot_set_timeout_cb(self._timeout)
|
ifc.prot_set_timeout_cb(self._timeout)
|
||||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||||
ifc.prot_set_update_header_cb(self._update_header)
|
ifc.prot_set_update_header_cb(self._update_header)
|
||||||
|
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.ifc = ifc
|
|
||||||
self.conn_no = ifc.get_conn_no()
|
self.conn_no = ifc.get_conn_no()
|
||||||
self.await_conn_resp_cnt = 0
|
self.await_conn_resp_cnt = 0
|
||||||
self.id_str = id_str
|
self.id_str = id_str
|
||||||
@@ -86,38 +75,18 @@ class Talent(Message):
|
|||||||
0x87: self.get_modbus_log_lvl,
|
0x87: self.get_modbus_log_lvl,
|
||||||
0x04: logging.INFO,
|
0x04: logging.INFO,
|
||||||
}
|
}
|
||||||
self.modbus_elms = 0 # for unit tests
|
self.sensor_list = 0
|
||||||
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
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug('Talent.close()')
|
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
|
# we have references to methods of this class in self.switch
|
||||||
# so we have to erase self.switch, otherwise this instance can't be
|
# so we have to erase self.switch, otherwise this instance can't be
|
||||||
# deallocated by the garbage collector ==> we get a memory leak
|
# deallocated by the garbage collector ==> we get a memory leak
|
||||||
self.switch.clear()
|
self.switch.clear()
|
||||||
self.log_lvl.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()
|
super().close()
|
||||||
|
|
||||||
def __set_serial_no(self, serial_no: str):
|
def __set_serial_no(self, serial_no: str):
|
||||||
@@ -130,11 +99,9 @@ class Talent(Message):
|
|||||||
|
|
||||||
if serial_no in inverters:
|
if serial_no in inverters:
|
||||||
inv = inverters[serial_no]
|
inv = inverters[serial_no]
|
||||||
self.node_id = inv['node_id']
|
self._set_config_parms(inv)
|
||||||
self.sug_area = inv['suggested_area']
|
|
||||||
self.modbus_polling = inv['modbus_polling']
|
|
||||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
|
||||||
self.db.set_pv_module_details(inv)
|
self.db.set_pv_module_details(inv)
|
||||||
|
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||||
else:
|
else:
|
||||||
self.node_id = ''
|
self.node_id = ''
|
||||||
self.sug_area = ''
|
self.sug_area = ''
|
||||||
@@ -203,24 +170,19 @@ class Talent(Message):
|
|||||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||||
self.ifc.tx_flush()
|
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):
|
def mb_timout_cb(self, exp_cnt):
|
||||||
self.mb_timer.start(self.mb_timeout)
|
self.mb_timer.start(self.mb_timeout)
|
||||||
|
if self.mb_scan:
|
||||||
|
self._send_modbus_scan()
|
||||||
|
return
|
||||||
|
|
||||||
if 2 == (exp_cnt % 30):
|
if 2 == (exp_cnt % 30):
|
||||||
# logging.info("Regular Modbus Status request")
|
# logging.info("Regular Modbus Status request")
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x2000,
|
||||||
|
96, logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000,
|
||||||
|
48, logging.DEBUG)
|
||||||
|
|
||||||
def _init_new_client_conn(self) -> bool:
|
def _init_new_client_conn(self) -> bool:
|
||||||
contact_name = self.contact_name
|
contact_name = self.contact_name
|
||||||
@@ -482,14 +444,14 @@ class Talent(Message):
|
|||||||
logger.debug(f'time: {timestamp:08x}')
|
logger.debug(f'time: {timestamp:08x}')
|
||||||
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
|
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
|
||||||
# "%Y-%m-%d %H:%M:%S")}')
|
# "%Y-%m-%d %H:%M:%S")}')
|
||||||
return msg_hdr_len, timestamp
|
return msg_hdr_len, data_id, timestamp
|
||||||
|
|
||||||
def msg_collector_data(self):
|
def msg_collector_data(self):
|
||||||
if self.ctrl.is_ind():
|
if self.ctrl.is_ind():
|
||||||
self.__build_header(0x99)
|
self.__build_header(0x99)
|
||||||
self.ifc.tx_add(b'\x01')
|
self.ifc.tx_add(b'\x01')
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
self.__process_data()
|
self.__process_data(False)
|
||||||
|
|
||||||
elif self.ctrl.is_resp():
|
elif self.ctrl.is_resp():
|
||||||
return # ignore received response
|
return # ignore received response
|
||||||
@@ -504,7 +466,7 @@ class Talent(Message):
|
|||||||
self.__build_header(0x99)
|
self.__build_header(0x99)
|
||||||
self.ifc.tx_add(b'\x01')
|
self.ifc.tx_add(b'\x01')
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
self.__process_data()
|
self.__process_data(True)
|
||||||
self.state = State.up # allow MODBUS cmds
|
self.state = State.up # allow MODBUS cmds
|
||||||
if (self.modbus_polling):
|
if (self.modbus_polling):
|
||||||
self.mb_timer.start(self.mb_first_timeout)
|
self.mb_timer.start(self.mb_first_timeout)
|
||||||
@@ -519,15 +481,51 @@ class Talent(Message):
|
|||||||
|
|
||||||
self.forward()
|
self.forward()
|
||||||
|
|
||||||
def __process_data(self):
|
def __build_model_name(self):
|
||||||
msg_hdr_len, ts = self.parse_msg_header()
|
db = self.db
|
||||||
|
model = db.get_db_value(Register.EQUIPMENT_MODEL, None)
|
||||||
|
if model:
|
||||||
|
return
|
||||||
|
max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
if max_pow == 3000:
|
||||||
|
model = f'TSOL-MS{max_pow}'
|
||||||
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
||||||
|
self.db.set_db_def_value(Register.MANUFACTURER, 'TSUN')
|
||||||
|
self.db.set_db_def_value(Register.NO_INPUTS, 4)
|
||||||
|
|
||||||
|
def __process_data(self, inv_data: bool):
|
||||||
|
msg_hdr_len, data_id, ts = self.parse_msg_header()
|
||||||
|
if inv_data:
|
||||||
|
# handle register mapping
|
||||||
|
if 0 == self.sensor_list:
|
||||||
|
self.sensor_list = data_id
|
||||||
|
self.db.set_db_def_value(Register.SENSOR_LIST,
|
||||||
|
f"{self.sensor_list:08x}")
|
||||||
|
logging.debug(f"Use sensor-list: {self.sensor_list:#08x}"
|
||||||
|
f" for '{self.unique_id}'")
|
||||||
|
if data_id != self.sensor_list:
|
||||||
|
logging.warning(f'Unexpected Sensor-List:{data_id:08x}'
|
||||||
|
f' (!={self.sensor_list:08x})')
|
||||||
|
# ignore replays for inverter data
|
||||||
|
age = self._utc() - self._utcfromts(ts)
|
||||||
|
age = age/(3600*24)
|
||||||
|
logger.debug(f"Age: {age} days")
|
||||||
|
if age > 1: # is a replay?
|
||||||
|
return
|
||||||
|
|
||||||
|
inv_update = False
|
||||||
|
|
||||||
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
|
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
|
||||||
+ msg_hdr_len, self.node_id):
|
+ msg_hdr_len, data_id, self.node_id):
|
||||||
if update:
|
if update:
|
||||||
|
if key == 'inverter':
|
||||||
|
inv_update = True
|
||||||
self._set_mqtt_timestamp(key, self._utcfromts(ts))
|
self._set_mqtt_timestamp(key, self._utcfromts(ts))
|
||||||
self.new_data[key] = True
|
self.new_data[key] = True
|
||||||
|
|
||||||
|
if inv_update:
|
||||||
|
self.__build_model_name()
|
||||||
|
|
||||||
def msg_ota_update(self):
|
def msg_ota_update(self):
|
||||||
if self.ctrl.is_req():
|
if self.ctrl.is_req():
|
||||||
self.inc_counter('OTA_Start_Msg')
|
self.inc_counter('OTA_Start_Msg')
|
||||||
@@ -588,10 +586,12 @@ class Talent(Message):
|
|||||||
logger.warning('Unknown Message')
|
logger.warning('Unknown Message')
|
||||||
self.inc_counter('Unknown_Msg')
|
self.inc_counter('Unknown_Msg')
|
||||||
return
|
return
|
||||||
|
if (self.mb_scan):
|
||||||
|
modbus_msg_len = self.data_len - hdr_len
|
||||||
|
self._dump_modbus_scan(data, hdr_len, modbus_msg_len)
|
||||||
|
|
||||||
for key, update, _ in self.mb.recv_resp(self.db, data[
|
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||||
hdr_len:],
|
hdr_len:]):
|
||||||
self.node_id):
|
|
||||||
if update:
|
if update:
|
||||||
self._set_mqtt_timestamp(key, self._utc())
|
self._set_mqtt_timestamp(key, self._utc())
|
||||||
self.new_data[key] = True
|
self.new_data[key] = True
|
||||||
|
|||||||
@@ -1,39 +1,74 @@
|
|||||||
|
|
||||||
import struct
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
if __name__ == "app.src.gen3plus.infos_g3p":
|
from infos import Infos, Register, ProxyMode, Fmt
|
||||||
from app.src.infos import Infos, Register, ProxyMode
|
|
||||||
else: # pragma: no cover
|
|
||||||
from infos import Infos, Register, ProxyMode
|
class RegisterFunc:
|
||||||
|
@staticmethod
|
||||||
|
def prod_sum(info: Infos, arr: dict) -> None | int:
|
||||||
|
result = 0
|
||||||
|
for sum in arr:
|
||||||
|
prod = 1
|
||||||
|
for factor in sum:
|
||||||
|
val = info.get_db_value(factor)
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
prod = prod * val
|
||||||
|
result += prod
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class RegisterMap:
|
class RegisterMap:
|
||||||
# make the class read/only by using __slots__
|
# make the class read/only by using __slots__
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
FMT_2_16BIT_VAL = '!HH'
|
||||||
|
FMT_3_16BIT_VAL = '!HHH'
|
||||||
|
FMT_4_16BIT_VAL = '!HHHH'
|
||||||
|
|
||||||
map = {
|
map = {
|
||||||
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
||||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 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', 'eval': 'round(result/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
|
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
|
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
|
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
|
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
|
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||||
|
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # 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
|
||||||
|
}
|
||||||
|
map_02b0 = {
|
||||||
|
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
|
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
|
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
|
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
|
0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # 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
|
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', 'offset': -40}, # noqa: E501
|
||||||
# 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
|
# 0x420100da
|
||||||
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.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
|
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
@@ -58,13 +93,103 @@ class RegisterMap:
|
|||||||
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
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
|
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
|
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
|
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},
|
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||||
|
|
||||||
}
|
}
|
||||||
|
map_3026 = {
|
||||||
|
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
|
||||||
|
0x42010030: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage
|
||||||
|
0x42010032: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current
|
||||||
|
0x42010034: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage
|
||||||
|
0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current
|
||||||
|
0x42010038: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x4201003a: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 MPTT-1 Status?
|
||||||
|
0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 MPTT-2 Status?
|
||||||
|
0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010044: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent
|
||||||
|
0x42010046: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010048: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201004a: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201004c: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201004e: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010050: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010052: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010054: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010056: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010058: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201005a: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201005c: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x4201005e: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010060: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010062: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x42010064: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
|
||||||
|
0x42010066: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x42010068: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x4201006a: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010070: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501, state of output value 0 or 1
|
||||||
|
0x42010072: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501 controller temp
|
||||||
|
0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501, control input 0..2048
|
||||||
|
0x42010076: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x42010078: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501
|
||||||
|
'calc': {
|
||||||
|
1: {'reg': Register.BATT_PV_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
|
||||||
|
'params': [[Register.BATT_PV1_VOLT, Register.BATT_PV1_CUR],
|
||||||
|
[Register.BATT_PV2_VOLT, Register.BATT_PV2_CUR]]},
|
||||||
|
2: {'reg': Register.BATT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
|
||||||
|
'params': [[Register.BATT_VOLT, Register.BATT_CUR]]},
|
||||||
|
3: {'reg': Register.BATT_OUT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
|
||||||
|
'params': [[Register.BATT_OUT_VOLT, Register.BATT_OUT_CUR]]},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterSel:
|
||||||
|
__sensor_map = {
|
||||||
|
0x02b0: RegisterMap.map_02b0,
|
||||||
|
0x3026: RegisterMap.map_3026,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, sensor: int):
|
||||||
|
return cls.__sensor_map.get(sensor, RegisterMap.map)
|
||||||
|
|
||||||
|
|
||||||
class InfosG3P(Infos):
|
class InfosG3P(Infos):
|
||||||
@@ -99,7 +224,22 @@ class InfosG3P(Infos):
|
|||||||
entity strings
|
entity strings
|
||||||
sug_area:str ==> suggested area string from the config file'''
|
sug_area:str ==> suggested area string from the config file'''
|
||||||
# iterate over RegisterMap.map and get the register values
|
# iterate over RegisterMap.map and get the register values
|
||||||
for row in RegisterMap.map.values():
|
sensor = self.get_db_value(Register.SENSOR_LIST)
|
||||||
|
if "3026" == sensor:
|
||||||
|
reg_map = RegisterMap.map_3026
|
||||||
|
elif "02b0" == sensor:
|
||||||
|
reg_map = RegisterMap.map_02b0
|
||||||
|
else:
|
||||||
|
reg_map = {}
|
||||||
|
items = reg_map.items()
|
||||||
|
if 'calc' in reg_map:
|
||||||
|
virt = reg_map['calc'].items()
|
||||||
|
else:
|
||||||
|
virt = {}
|
||||||
|
|
||||||
|
for idx, row in chain(RegisterMap.map.items(), items, virt):
|
||||||
|
if 'calc' == idx:
|
||||||
|
continue
|
||||||
info_id = row['reg']
|
info_id = row['reg']
|
||||||
if self.__hide_topic(row):
|
if self.__hide_topic(row):
|
||||||
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
|
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
|
||||||
@@ -108,13 +248,17 @@ class InfosG3P(Infos):
|
|||||||
if res:
|
if res:
|
||||||
yield res
|
yield res
|
||||||
|
|
||||||
def parse(self, buf, msg_type: int, rcv_ftype: int, node_id: str = '') \
|
def parse(self, buf, msg_type: int, rcv_ftype: int,
|
||||||
|
sensor: int = 0, node_id: str = '') \
|
||||||
-> Generator[tuple[str, bool], None, None]:
|
-> Generator[tuple[str, bool], None, None]:
|
||||||
'''parse a data sequence received from the inverter and
|
'''parse a data sequence received from the inverter and
|
||||||
stores the values in Infos.db
|
stores the values in Infos.db
|
||||||
|
|
||||||
buf: buffer of the sequence to parse'''
|
buf: buffer of the sequence to parse'''
|
||||||
for idx, row in RegisterMap.map.items():
|
reg_map = RegisterSel.get(sensor)
|
||||||
|
for idx, row in reg_map.items():
|
||||||
|
if 'calc' == idx:
|
||||||
|
continue
|
||||||
addr = idx & 0xffff
|
addr = idx & 0xffff
|
||||||
ftype = (idx >> 16) & 0xff
|
ftype = (idx >> 16) & 0xff
|
||||||
mtype = (idx >> 24) & 0xff
|
mtype = (idx >> 24) & 0xff
|
||||||
@@ -123,30 +267,50 @@ class InfosG3P(Infos):
|
|||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
info_id = row['reg']
|
info_id = row['reg']
|
||||||
result = self.__get_value(buf, addr, row)
|
result = Fmt.get_value(buf, addr, row)
|
||||||
|
yield from self.__update_val(node_id, "GEN3PLUS", info_id, result)
|
||||||
|
yield from self.calc(sensor, node_id)
|
||||||
|
|
||||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
def calc(self, sensor: int = 0, node_id: str = '') \
|
||||||
|
-> Generator[tuple[str, bool], None, None]:
|
||||||
|
'''calculate meta values from the
|
||||||
|
stored values in Infos.db
|
||||||
|
|
||||||
if keys:
|
sensor: sensor_list number
|
||||||
name, update = self.update_db(keys, must_incr, result)
|
node_id: id-string for the node'''
|
||||||
yield keys[0], update
|
|
||||||
else:
|
|
||||||
name = str(f'info-id.0x{addr:x}')
|
|
||||||
update = False
|
|
||||||
|
|
||||||
|
reg_map = RegisterSel.get(sensor)
|
||||||
|
if 'calc' in reg_map:
|
||||||
|
for row in reg_map['calc'].values():
|
||||||
|
info_id = row['reg']
|
||||||
|
result = row['func'](self, row['params'])
|
||||||
|
yield from self.__update_val(node_id, "CALC", info_id, result)
|
||||||
|
|
||||||
|
def __update_val(self, node_id, source: str, info_id, result):
|
||||||
|
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||||
|
if keys:
|
||||||
|
name, update = self.update_db(keys, must_incr, result)
|
||||||
|
yield keys[0], update
|
||||||
if update:
|
if update:
|
||||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
self.tracer.log(level, f'[{node_id}] {source}: {name}'
|
||||||
f' : {result}{unit}')
|
f' : {result}{unit}')
|
||||||
|
|
||||||
def __get_value(self, buf, idx, row):
|
def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0):
|
||||||
'''Get a value from buf and interpret as in row'''
|
buf = bytearray(len)
|
||||||
fmt = row['fmt']
|
for idx, row in RegisterSel.get(sensor).items():
|
||||||
res = struct.unpack_from(fmt, buf, idx)
|
addr = idx & 0xffff
|
||||||
result = res[0]
|
ftype = (idx >> 16) & 0xff
|
||||||
if isinstance(result, (bytearray, bytes)):
|
mtype = (idx >> 24) & 0xff
|
||||||
result = result.decode().split('\x00')[0]
|
if ftype != rcv_ftype or mtype != msg_type:
|
||||||
if 'eval' in row:
|
continue
|
||||||
result = eval(row['eval'])
|
if not isinstance(row, dict):
|
||||||
if 'ratio' in row:
|
continue
|
||||||
result = round(result * row['ratio'], 2)
|
if 'const' in row:
|
||||||
return result
|
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
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
|
||||||
if __name__ == "app.src.gen3plus.inverter_g3p":
|
from inverter_base import InverterBase
|
||||||
from app.src.inverter_base import InverterBase
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
from gen3plus.solarman_emu import SolarmanEmu
|
||||||
else: # pragma: no cover
|
|
||||||
from inverter_base import InverterBase
|
|
||||||
from gen3plus.solarman_v5 import SolarmanV5
|
|
||||||
|
|
||||||
|
|
||||||
class InverterG3P(InverterBase):
|
class InverterG3P(InverterBase):
|
||||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
client_mode: bool = False):
|
client_mode: bool = False):
|
||||||
|
remote_prot = None
|
||||||
|
if client_mode:
|
||||||
|
remote_prot = SolarmanEmu
|
||||||
super().__init__(reader, writer, 'solarman',
|
super().__init__(reader, writer, 'solarman',
|
||||||
SolarmanV5, client_mode)
|
SolarmanV5, client_mode, remote_prot)
|
||||||
|
|||||||
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, 0x02b0)
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -2,24 +2,16 @@ import struct
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from itertools import chain
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
from proxy import Proxy
|
||||||
from app.src.async_ifc import AsyncIfc
|
from async_ifc import AsyncIfc
|
||||||
from app.src.messages import hex_dump_memory, Message, State
|
from messages import hex_dump_memory, Message, State
|
||||||
from app.src.modbus import Modbus
|
from cnf.config import Config
|
||||||
from app.src.my_timer import Timer
|
from modbus import Modbus
|
||||||
from app.src.config import Config
|
from gen3plus.infos_g3p import InfosG3P
|
||||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
from infos import Register, Fmt
|
||||||
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
|
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -50,195 +42,22 @@ class Sequence():
|
|||||||
return f'{self.rcv_idx:02x}:{self.snd_idx:02x}'
|
return f'{self.rcv_idx:02x}:{self.snd_idx:02x}'
|
||||||
|
|
||||||
|
|
||||||
class SolarmanV5(Message):
|
class SolarmanBase(Message):
|
||||||
AT_CMD = 1
|
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||||
MB_RTU_CMD = 2
|
_send_modbus_cb, mb_timeout: int):
|
||||||
MB_START_TIMEOUT = 40
|
super().__init__('G3P', ifc, server_side, _send_modbus_cb,
|
||||||
'''start delay for Modbus polling in server mode'''
|
mb_timeout)
|
||||||
MB_REGULAR_TIMEOUT = 60
|
|
||||||
'''regular Modbus polling time in server mode'''
|
|
||||||
MB_CLIENT_DATA_UP = 30
|
|
||||||
'''Data up time in client mode'''
|
|
||||||
HDR_FMT = '<BLLL'
|
|
||||||
'''format string for packing of the header'''
|
|
||||||
|
|
||||||
def __init__(self, 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.rx_set_cb(self.read)
|
||||||
ifc.prot_set_timeout_cb(self._timeout)
|
ifc.prot_set_timeout_cb(self._timeout)
|
||||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||||
ifc.prot_set_update_header_cb(self._update_header)
|
ifc.prot_set_update_header_cb(self.__update_header)
|
||||||
|
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.ifc = ifc
|
|
||||||
self.conn_no = ifc.get_conn_no()
|
self.conn_no = ifc.get_conn_no()
|
||||||
self.header_len = 11 # overwrite construcor in class Message
|
self.header_len = 11 # overwrite construcor in class Message
|
||||||
self.control = 0
|
self.control = 0
|
||||||
self.seq = Sequence(server_side)
|
self.seq = Sequence(server_side)
|
||||||
self.snr = 0
|
self.snr = 0
|
||||||
self.db = InfosG3P(client_mode)
|
|
||||||
self.time_ofs = 0
|
self.time_ofs = 0
|
||||||
self.forward_at_cmd_resp = False
|
|
||||||
self.no_forwarding = False
|
|
||||||
'''not allowed to connect to TSUN cloud by connection type'''
|
|
||||||
self.switch = {
|
|
||||||
|
|
||||||
0x4210: self.msg_data_ind, # real time data
|
|
||||||
0x1210: self.msg_response, # at least every 5 minutes
|
|
||||||
|
|
||||||
0x4710: self.msg_hbeat_ind, # heatbeat
|
|
||||||
0x1710: self.msg_response, # every 2 minutes
|
|
||||||
|
|
||||||
# every 3 hours comes a sync seuqence:
|
|
||||||
# 00:00:00 0x4110 device data ftype: 0x02
|
|
||||||
# 00:00:02 0x4210 real time data ftype: 0x01
|
|
||||||
# 00:00:03 0x4210 real time data ftype: 0x81
|
|
||||||
# 00:00:05 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 0c # noqa: E501
|
|
||||||
# 00:00:06 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 1c # noqa: E501
|
|
||||||
# 00:00:07 0x4310 wifi data ftype: 0x01 sub-id 0x0018: 0c # noqa: E501
|
|
||||||
# 00:00:08 0x4810 options? ftype: 0x01
|
|
||||||
|
|
||||||
0x4110: self.msg_dev_ind, # device data, sync start
|
|
||||||
0x1110: self.msg_response, # every 3 hours
|
|
||||||
|
|
||||||
0x4310: self.msg_sync_start, # regulary after 3-6 hours
|
|
||||||
0x1310: self.msg_response,
|
|
||||||
0x4810: self.msg_sync_end, # sync end
|
|
||||||
0x1810: self.msg_response,
|
|
||||||
|
|
||||||
#
|
|
||||||
# MODbus or AT cmd
|
|
||||||
0x4510: self.msg_command_req, # from server
|
|
||||||
0x1510: self.msg_command_rsp, # from inverter
|
|
||||||
# 0x0510: self.msg_command_rsp, # from inverter
|
|
||||||
}
|
|
||||||
|
|
||||||
self.log_lvl = {
|
|
||||||
|
|
||||||
0x4210: logging.INFO, # real time data
|
|
||||||
0x1210: logging.INFO, # at least every 5 minutes
|
|
||||||
|
|
||||||
0x4710: logging.DEBUG, # heatbeat
|
|
||||||
0x1710: logging.DEBUG, # every 2 minutes
|
|
||||||
|
|
||||||
0x4110: logging.INFO, # device data, sync start
|
|
||||||
0x1110: logging.INFO, # every 3 hours
|
|
||||||
|
|
||||||
0x4310: logging.INFO, # regulary after 3-6 hours
|
|
||||||
0x1310: logging.INFO,
|
|
||||||
|
|
||||||
0x4810: logging.INFO, # sync end
|
|
||||||
0x1810: logging.INFO,
|
|
||||||
|
|
||||||
#
|
|
||||||
# MODbus or AT cmd
|
|
||||||
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
|
|
||||||
|
|
||||||
'''
|
|
||||||
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,
|
|
||||||
start_timeout=MB_CLIENT_DATA_UP):
|
|
||||||
self.no_forwarding = True
|
|
||||||
self.snr = snr
|
|
||||||
self.__set_serial_no(snr)
|
|
||||||
self.mb_timeout = start_timeout
|
|
||||||
self.db.set_db_def_value(Register.IP_ADDRESS, host)
|
|
||||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
|
||||||
self.mb_timeout)
|
|
||||||
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
|
||||||
120)
|
|
||||||
self.new_data['controller'] = True
|
|
||||||
|
|
||||||
self.state = State.up
|
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
|
||||||
self.mb_timer.start(self.mb_timeout)
|
|
||||||
|
|
||||||
def new_state_up(self):
|
|
||||||
if self.state is not State.up:
|
|
||||||
self.state = State.up
|
|
||||||
if (self.modbus_polling):
|
|
||||||
self.mb_timer.start(self.mb_first_timeout)
|
|
||||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
|
||||||
self.mb_timeout)
|
|
||||||
|
|
||||||
def __set_config_parms(self, inv: dict):
|
|
||||||
'''init connection with params from the configuration'''
|
|
||||||
self.node_id = inv['node_id']
|
|
||||||
self.sug_area = inv['suggested_area']
|
|
||||||
self.modbus_polling = inv['modbus_polling']
|
|
||||||
self.sensor_list = inv['sensor_list']
|
|
||||||
|
|
||||||
def __set_serial_no(self, snr: int):
|
|
||||||
'''check the serial number and configure the inverter connection'''
|
|
||||||
serial_no = str(snr)
|
|
||||||
if self.unique_id == serial_no:
|
|
||||||
logger.debug(f'SerialNo: {serial_no}')
|
|
||||||
else:
|
|
||||||
inverters = Config.get('inverters')
|
|
||||||
# logger.debug(f'Inverters: {inverters}')
|
|
||||||
|
|
||||||
for key, inv in inverters.items():
|
|
||||||
# logger.debug(f'key: {key} -> {inv}')
|
|
||||||
if (type(inv) is dict and 'monitor_sn' in inv
|
|
||||||
and inv['monitor_sn'] == snr):
|
|
||||||
self.__set_config_parms(inv)
|
|
||||||
self.db.set_pv_module_details(inv)
|
|
||||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
|
||||||
|
|
||||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.node_id = ''
|
|
||||||
self.sug_area = ''
|
|
||||||
if 'allow_all' not in inverters or not inverters['allow_all']:
|
|
||||||
self.inc_counter('Unknown_SNR')
|
|
||||||
self.unique_id = None
|
|
||||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
|
||||||
return
|
|
||||||
logger.warning(f'SerialNo {serial_no} not known but accepted!')
|
|
||||||
|
|
||||||
self.unique_id = serial_no
|
|
||||||
|
|
||||||
def read(self) -> float:
|
def read(self) -> float:
|
||||||
'''process all received messages in the _recv_buffer'''
|
'''process all received messages in the _recv_buffer'''
|
||||||
@@ -254,42 +73,10 @@ class SolarmanV5(Message):
|
|||||||
self.__flush_recv_msg()
|
self.__flush_recv_msg()
|
||||||
else:
|
else:
|
||||||
return 0 # wait 0s before sending a response
|
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:
|
|
||||||
return
|
|
||||||
tsun = Config.get('solarman')
|
|
||||||
if tsun['enabled']:
|
|
||||||
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') +
|
|
||||||
f' Ctl: {int(self.control):#04x}'
|
|
||||||
f' Msg: {fnc.__name__!r}')
|
|
||||||
|
|
||||||
def _init_new_client_conn(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our private methods
|
Our public methods
|
||||||
'''
|
'''
|
||||||
def __flow_str(self, server_side: bool, type: str): # noqa: F821
|
def _flow_str(self, server_side: bool, type: str): # noqa: F821
|
||||||
switch = {
|
switch = {
|
||||||
'rx': ' <',
|
'rx': ' <',
|
||||||
'tx': ' >',
|
'tx': ' >',
|
||||||
@@ -304,12 +91,73 @@ class SolarmanV5(Message):
|
|||||||
type += 'S'
|
type += 'S'
|
||||||
return switch.get(type, '???')
|
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):
|
def _timestamp(self):
|
||||||
# utc as epoche
|
# utc as epoche
|
||||||
return int(time.time()) # pragma: no cover
|
return int(time.time()) # pragma: no cover
|
||||||
|
|
||||||
def _heartbeat(self) -> int:
|
def _emu_timestamp(self):
|
||||||
return 60 # pragma: no cover
|
'''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:
|
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||||
|
|
||||||
@@ -362,95 +210,295 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
return True
|
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:
|
def __flush_recv_msg(self) -> None:
|
||||||
self.ifc.rx_get(self.header_len + self.data_len+2)
|
self.ifc.rx_get(self.header_len + self.data_len+2)
|
||||||
self.header_valid = False
|
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
|
||||||
|
AT_CMD_RSP = 8
|
||||||
|
MB_CLIENT_DATA_UP = 30
|
||||||
|
'''Data up time in client mode'''
|
||||||
|
HDR_FMT = '<BLLL'
|
||||||
|
'''format string for packing of the header'''
|
||||||
|
|
||||||
|
def __init__(self, addr, ifc: "AsyncIfc",
|
||||||
|
server_side: bool, client_mode: bool):
|
||||||
|
super().__init__(addr, ifc, server_side, self.send_modbus_cb,
|
||||||
|
mb_timeout=8)
|
||||||
|
|
||||||
|
self.db = InfosG3P(client_mode)
|
||||||
|
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
|
||||||
|
0x1210: self.msg_response, # at least every 5 minutes
|
||||||
|
|
||||||
|
0x4710: self.msg_hbeat_ind, # heatbeat
|
||||||
|
0x1710: self.msg_response, # every 2 minutes
|
||||||
|
|
||||||
|
# every 3 hours comes a sync seuqence:
|
||||||
|
# 00:00:00 0x4110 device data ftype: 0x02
|
||||||
|
# 00:00:02 0x4210 real time data ftype: 0x01
|
||||||
|
# 00:00:03 0x4210 real time data ftype: 0x81
|
||||||
|
# 00:00:05 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 0c # noqa: E501
|
||||||
|
# 00:00:06 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 1c # noqa: E501
|
||||||
|
# 00:00:07 0x4310 wifi data ftype: 0x01 sub-id 0x0018: 0c # noqa: E501
|
||||||
|
# 00:00:08 0x4810 options? ftype: 0x01
|
||||||
|
|
||||||
|
0x4110: self.msg_dev_ind, # device data, sync start
|
||||||
|
0x1110: self.msg_response, # every 3 hours
|
||||||
|
|
||||||
|
0x4310: self.msg_sync_start, # regulary after 3-6 hours
|
||||||
|
0x1310: self.msg_response,
|
||||||
|
0x4810: self.msg_sync_end, # sync end
|
||||||
|
0x1810: self.msg_response,
|
||||||
|
|
||||||
|
#
|
||||||
|
# MODbus or AT cmd
|
||||||
|
0x4510: self.msg_command_req, # from server
|
||||||
|
0x1510: self.msg_command_rsp, # from inverter
|
||||||
|
0x0510: self.msg_command_rsp, # from inverter
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log_lvl = {
|
||||||
|
|
||||||
|
0x4210: logging.INFO, # real time data
|
||||||
|
0x1210: logging.INFO, # at least every 5 minutes
|
||||||
|
|
||||||
|
0x4710: logging.DEBUG, # heatbeat
|
||||||
|
0x1710: logging.DEBUG, # every 2 minutes
|
||||||
|
|
||||||
|
0x4110: logging.INFO, # device data, sync start
|
||||||
|
0x1110: logging.INFO, # every 3 hours
|
||||||
|
|
||||||
|
0x4310: logging.INFO, # regulary after 3-6 hours
|
||||||
|
0x1310: logging.INFO,
|
||||||
|
|
||||||
|
0x4810: logging.INFO, # sync end
|
||||||
|
0x1810: logging.INFO,
|
||||||
|
|
||||||
|
#
|
||||||
|
# MODbus or AT cmd
|
||||||
|
0x4510: logging.INFO, # from server
|
||||||
|
0x1510: self.get_cmd_rsp_log_lvl,
|
||||||
|
}
|
||||||
|
g3p_cnf = Config.get('gen3plus')
|
||||||
|
|
||||||
|
if 'at_acl' in g3p_cnf: # pragma: no cover
|
||||||
|
self.at_acl = g3p_cnf['at_acl']
|
||||||
|
|
||||||
|
self.sensor_list = 0
|
||||||
|
self.mb_regs = [{'addr': 0x3000, 'len': 48},
|
||||||
|
{'addr': 0x2000, 'len': 96}]
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our puplic methods
|
||||||
|
'''
|
||||||
|
def close(self) -> None:
|
||||||
|
logging.debug('Solarman.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()
|
||||||
|
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.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
|
||||||
|
|
||||||
|
if self.mb_scan:
|
||||||
|
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
|
||||||
|
self.mb_start_reg, self.mb_bytes,
|
||||||
|
logging.INFO)
|
||||||
|
else:
|
||||||
|
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
|
||||||
|
self.mb_regs[0]['addr'],
|
||||||
|
self.mb_regs[0]['len'], logging.DEBUG)
|
||||||
|
|
||||||
|
self.mb_timer.start(self.mb_timeout)
|
||||||
|
|
||||||
|
def new_state_up(self):
|
||||||
|
if self.state is not State.up:
|
||||||
|
self.state = State.up
|
||||||
|
if (self.modbus_polling):
|
||||||
|
self.mb_timer.start(self.mb_first_timeout)
|
||||||
|
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||||
|
self.mb_timeout)
|
||||||
|
|
||||||
|
def 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, serial_no: str = ""):
|
||||||
|
'''init connection with params from the configuration'''
|
||||||
|
super()._set_config_parms(inv)
|
||||||
|
snr = serial_no[:3]
|
||||||
|
if '410' == snr:
|
||||||
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL,
|
||||||
|
'TSOL-DC1000')
|
||||||
|
|
||||||
|
self.sensor_list = inv['sensor_list']
|
||||||
|
if 0 == self.sensor_list:
|
||||||
|
if '410' == snr:
|
||||||
|
self.sensor_list = 0x3026
|
||||||
|
self.mb_regs = [{'addr': 0x0000, 'len': 45}]
|
||||||
|
else:
|
||||||
|
self.sensor_list = 0x02b0
|
||||||
|
self.db.set_db_def_value(Register.SENSOR_LIST,
|
||||||
|
f"{self.sensor_list:04x}")
|
||||||
|
logging.debug(f"Use sensor-list: {self.sensor_list:#04x}"
|
||||||
|
f" for '{serial_no}'")
|
||||||
|
|
||||||
|
def _set_serial_no(self, snr: int):
|
||||||
|
'''check the serial number and configure the inverter connection'''
|
||||||
|
serial_no = str(snr)
|
||||||
|
if self.unique_id == serial_no:
|
||||||
|
logger.debug(f'SerialNo: {serial_no}')
|
||||||
|
else:
|
||||||
|
inverters = Config.get('inverters')
|
||||||
|
batteries = Config.get('batteries')
|
||||||
|
# logger.debug(f'Inverters: {inverters}')
|
||||||
|
|
||||||
|
for key, inv in chain(inverters.items(), batteries.items()):
|
||||||
|
# logger.debug(f'key: {key} -> {inv}')
|
||||||
|
if (type(inv) is dict and 'monitor_sn' in inv
|
||||||
|
and inv['monitor_sn'] == snr):
|
||||||
|
self._set_config_parms(inv, key)
|
||||||
|
self.db.set_pv_module_details(inv)
|
||||||
|
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||||
|
|
||||||
|
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 = ''
|
||||||
|
self.sug_area = ''
|
||||||
|
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||||
|
self.inc_counter('Unknown_SNR')
|
||||||
|
self.unique_id = None
|
||||||
|
logging.error(f"Ignore message from unknow inverter with Monitoring-SN: {serial_no})!\n" # noqa: E501
|
||||||
|
" !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501
|
||||||
|
return
|
||||||
|
logging.warning(f"Monitoring-SN: {serial_no} not configured but accepted!" # noqa: E501
|
||||||
|
" !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501
|
||||||
|
|
||||||
|
self.unique_id = serial_no
|
||||||
|
|
||||||
|
def forward(self, buffer, buflen) -> None:
|
||||||
|
'''add the actual receive msg to the forwarding queue'''
|
||||||
|
if self.no_forwarding:
|
||||||
|
return
|
||||||
|
tsun = Config.get('solarman')
|
||||||
|
if tsun['enabled']:
|
||||||
|
self.ifc.fwd_add(buffer[:buflen])
|
||||||
|
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
|
||||||
|
|
||||||
|
_, _str = self.get_fnc_handler(self.control)
|
||||||
|
logger.info(self._flow_str(self.server_side, 'forwrd') +
|
||||||
|
f' Ctl: {int(self.control):#04x}'
|
||||||
|
f' Msg: {_str}')
|
||||||
|
|
||||||
|
def _init_new_client_conn(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _heartbeat(self) -> int:
|
||||||
|
return 60 # pragma: no cover
|
||||||
|
|
||||||
def __send_ack_rsp(self, msgtype, ftype, ack=1):
|
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.ifc.tx_add(struct.pack('<BBLL', ftype, ack,
|
||||||
self._timestamp(),
|
self._timestamp(),
|
||||||
self._heartbeat()))
|
self._heartbeat()))
|
||||||
self.__finish_send_msg()
|
self._finish_send_msg()
|
||||||
|
|
||||||
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||||
if self.state != State.up:
|
if self.state != State.up:
|
||||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
' cause the state is not UP anymore')
|
' cause the state is not UP anymore')
|
||||||
return
|
return
|
||||||
self.__build_header(0x4510)
|
self._build_header(0x4510)
|
||||||
self.ifc.tx_add(struct.pack('<BHLLL', self.MB_RTU_CMD,
|
self.ifc.tx_add(struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||||
self.sensor_list, 0, 0, 0))
|
self.sensor_list, 0, 0, 0))
|
||||||
self.ifc.tx_add(pdu)
|
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_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||||
self.ifc.tx_flush()
|
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):
|
def mb_timout_cb(self, exp_cnt):
|
||||||
self.mb_timer.start(self.mb_timeout)
|
self.mb_timer.start(self.mb_timeout)
|
||||||
|
if self.mb_scan:
|
||||||
|
self._send_modbus_scan()
|
||||||
|
else:
|
||||||
|
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
|
||||||
|
self.mb_regs[0]['addr'],
|
||||||
|
self.mb_regs[0]['len'], logging.INFO)
|
||||||
|
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
if 1 == (exp_cnt % 30) and len(self.mb_regs) > 1:
|
||||||
|
# logging.info("Regular Modbus Status request")
|
||||||
if 1 == (exp_cnt % 30):
|
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
|
||||||
# logging.info("Regular Modbus Status request")
|
self.mb_regs[1]['addr'],
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
self.mb_regs[1]['len'], logging.INFO)
|
||||||
|
|
||||||
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||||
@@ -468,15 +516,15 @@ class SolarmanV5(Message):
|
|||||||
node_id = self.node_id
|
node_id = self.node_id
|
||||||
key = 'at_resp'
|
key = 'at_resp'
|
||||||
logger.info(f'{key}: {data_json}')
|
logger.info(f'{key}: {data_json}')
|
||||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
await Proxy.mqtt.publish(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
return
|
return
|
||||||
|
|
||||||
self.forward_at_cmd_resp = False
|
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,
|
self.ifc.tx_add(struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
|
||||||
0x0002, 0, 0, 0,
|
0x0002, 0, 0, 0,
|
||||||
at_cmd.encode('utf-8'), b'\r'))
|
at_cmd.encode('utf-8'), b'\r'))
|
||||||
self.__finish_send_msg()
|
self._finish_send_msg()
|
||||||
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
|
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
|
||||||
try:
|
try:
|
||||||
self.ifc.tx_flush()
|
self.ifc.tx_flush()
|
||||||
@@ -502,11 +550,11 @@ class SolarmanV5(Message):
|
|||||||
logger.info(f'Model: {model}')
|
logger.info(f'Model: {model}')
|
||||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
||||||
|
|
||||||
def __process_data(self, ftype, ts):
|
def __process_data(self, ftype, ts, sensor=0):
|
||||||
inv_update = False
|
inv_update = False
|
||||||
msg_type = self.control >> 8
|
msg_type = self.control >> 8
|
||||||
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype,
|
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type,
|
||||||
self.node_id):
|
ftype, sensor, self.node_id):
|
||||||
if update:
|
if update:
|
||||||
if key == 'inverter':
|
if key == 'inverter':
|
||||||
inv_update = True
|
inv_update = True
|
||||||
@@ -567,7 +615,7 @@ class SolarmanV5(Message):
|
|||||||
else:
|
else:
|
||||||
ts = None
|
ts = None
|
||||||
|
|
||||||
self.__process_data(ftype, ts)
|
self.__process_data(ftype, ts, sensor)
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1210, ftype)
|
self.__send_ack_rsp(0x1210, ftype)
|
||||||
self.new_state_up()
|
self.new_state_up()
|
||||||
@@ -612,7 +660,7 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
def publish_mqtt(self, key, data): # pragma: no cover
|
def publish_mqtt(self, key, data): # pragma: no cover
|
||||||
asyncio.ensure_future(
|
asyncio.ensure_future(
|
||||||
self.mqtt.publish(key, data))
|
Proxy.mqtt.publish(key, data))
|
||||||
|
|
||||||
def get_cmd_rsp_log_lvl(self) -> int:
|
def get_cmd_rsp_log_lvl(self) -> int:
|
||||||
ftype = self.ifc.rx_peek()[self.header_len]
|
ftype = self.ifc.rx_peek()[self.header_len]
|
||||||
@@ -630,19 +678,41 @@ class SolarmanV5(Message):
|
|||||||
data = self.ifc.rx_peek()[self.header_len:
|
data = self.ifc.rx_peek()[self.header_len:
|
||||||
self.header_len+self.data_len]
|
self.header_len+self.data_len]
|
||||||
ftype = data[0]
|
ftype = data[0]
|
||||||
if ftype == self.AT_CMD:
|
if ftype == self.AT_CMD or \
|
||||||
|
ftype == self.AT_CMD_RSP:
|
||||||
if not self.forward_at_cmd_resp:
|
if not self.forward_at_cmd_resp:
|
||||||
data_json = data[14:].decode("utf-8")
|
data_json = data[14:].decode("utf-8")
|
||||||
node_id = self.node_id
|
node_id = self.node_id
|
||||||
key = 'at_resp'
|
key = 'at_resp'
|
||||||
logger.info(f'{key}: {data_json}')
|
logger.info(f'{key}: {data_json}')
|
||||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
return
|
return
|
||||||
elif ftype == self.MB_RTU_CMD:
|
elif ftype == self.MB_RTU_CMD:
|
||||||
self.__modbus_command_rsp(data)
|
self.__modbus_command_rsp(data)
|
||||||
return
|
return
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
|
|
||||||
|
def __parse_modbus_rsp(self, data, modbus_msg_len):
|
||||||
|
inv_update = False
|
||||||
|
self.modbus_elms = 0
|
||||||
|
if (self.mb_scan):
|
||||||
|
self._dump_modbus_scan(data, 14, modbus_msg_len)
|
||||||
|
|
||||||
|
ts = self._timestamp()
|
||||||
|
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
|
||||||
|
self.modbus_elms += 1
|
||||||
|
if update:
|
||||||
|
if key == 'inverter':
|
||||||
|
inv_update = True
|
||||||
|
self._set_mqtt_timestamp(key, ts)
|
||||||
|
self.new_data[key] = True
|
||||||
|
for key, update in self.db.calc(self.sensor_list, self.node_id):
|
||||||
|
if update:
|
||||||
|
self._set_mqtt_timestamp(key, ts)
|
||||||
|
self.new_data[key] = True
|
||||||
|
|
||||||
|
return inv_update
|
||||||
|
|
||||||
def __modbus_command_rsp(self, data):
|
def __modbus_command_rsp(self, data):
|
||||||
'''precess MODBUS RTU response'''
|
'''precess MODBUS RTU response'''
|
||||||
valid = data[1]
|
valid = data[1]
|
||||||
@@ -650,19 +720,13 @@ class SolarmanV5(Message):
|
|||||||
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||||
if valid == 1 and modbus_msg_len > 4:
|
if valid == 1 and modbus_msg_len > 4:
|
||||||
# logger.info(f'first byte modbus:{data[14]}')
|
# logger.info(f'first byte modbus:{data[14]}')
|
||||||
inv_update = False
|
inv_update = self.__parse_modbus_rsp(data, modbus_msg_len)
|
||||||
self.modbus_elms = 0
|
|
||||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
|
||||||
self.node_id):
|
|
||||||
self.modbus_elms += 1
|
|
||||||
if update:
|
|
||||||
if key == 'inverter':
|
|
||||||
inv_update = True
|
|
||||||
self._set_mqtt_timestamp(key, self._timestamp())
|
|
||||||
self.new_data[key] = True
|
|
||||||
if inv_update:
|
if inv_update:
|
||||||
self.__build_model_name()
|
self.__build_model_name()
|
||||||
|
|
||||||
|
if self.establish_inv_emu and not self.ifc.remote.stream:
|
||||||
|
self.establish_emu()
|
||||||
|
|
||||||
def msg_hbeat_ind(self):
|
def msg_hbeat_ind(self):
|
||||||
data = self.ifc.rx_peek()[self.header_len:]
|
data = self.ifc.rx_peek()[self.header_len:]
|
||||||
result = struct.unpack_from('<B', data, 0)
|
result = struct.unpack_from('<B', data, 0)
|
||||||
@@ -684,16 +748,3 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1810, ftype)
|
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")}')
|
|
||||||
|
|||||||
470
app/src/infos.py
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import struct
|
||||||
import os
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
@@ -25,7 +26,11 @@ class Register(Enum):
|
|||||||
EQUIPMENT_MODEL = 24
|
EQUIPMENT_MODEL = 24
|
||||||
NO_INPUTS = 25
|
NO_INPUTS = 25
|
||||||
MAX_DESIGNED_POWER = 26
|
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
|
INVERTER_CNT = 50
|
||||||
UNKNOWN_SNR = 51
|
UNKNOWN_SNR = 51
|
||||||
UNKNOWN_MSG = 52
|
UNKNOWN_MSG = 52
|
||||||
@@ -38,10 +43,13 @@ class Register(Enum):
|
|||||||
AT_COMMAND = 59
|
AT_COMMAND = 59
|
||||||
MODBUS_COMMAND = 60
|
MODBUS_COMMAND = 60
|
||||||
AT_COMMAND_BLOCKED = 61
|
AT_COMMAND_BLOCKED = 61
|
||||||
|
CLOUD_CONN_CNT = 62
|
||||||
OUTPUT_POWER = 83
|
OUTPUT_POWER = 83
|
||||||
RATED_POWER = 84
|
RATED_POWER = 84
|
||||||
INVERTER_TEMP = 85
|
INVERTER_TEMP = 85
|
||||||
INVERTER_STATUS = 86
|
INVERTER_STATUS = 86
|
||||||
|
DETECT_STATUS_1 = 87
|
||||||
|
DETECT_STATUS_2 = 88
|
||||||
PV1_VOLTAGE = 100
|
PV1_VOLTAGE = 100
|
||||||
PV1_CURRENT = 101
|
PV1_CURRENT = 101
|
||||||
PV1_POWER = 102
|
PV1_POWER = 102
|
||||||
@@ -84,6 +92,12 @@ class Register(Enum):
|
|||||||
PV5_TOTAL_GENERATION = 241
|
PV5_TOTAL_GENERATION = 241
|
||||||
PV6_DAILY_GENERATION = 250
|
PV6_DAILY_GENERATION = 250
|
||||||
PV6_TOTAL_GENERATION = 251
|
PV6_TOTAL_GENERATION = 251
|
||||||
|
INV_UNKNOWN_1 = 252
|
||||||
|
BOOT_STATUS = 253
|
||||||
|
DSP_STATUS = 254
|
||||||
|
WORK_MODE = 255
|
||||||
|
OUTPUT_SHUTDOWN = 256
|
||||||
|
|
||||||
GRID_VOLTAGE = 300
|
GRID_VOLTAGE = 300
|
||||||
GRID_CURRENT = 301
|
GRID_CURRENT = 301
|
||||||
GRID_FREQUENCY = 302
|
GRID_FREQUENCY = 302
|
||||||
@@ -99,30 +113,180 @@ class Register(Enum):
|
|||||||
IP_ADDRESS = 407
|
IP_ADDRESS = 407
|
||||||
POLLING_INTERVAL = 408
|
POLLING_INTERVAL = 408
|
||||||
SENSOR_LIST = 409
|
SENSOR_LIST = 409
|
||||||
EVENT_401 = 500
|
SSID = 410
|
||||||
EVENT_402 = 501
|
EVENT_ALARM = 500
|
||||||
EVENT_403 = 502
|
EVENT_FAULT = 501
|
||||||
EVENT_404 = 503
|
EVENT_BF1 = 502
|
||||||
EVENT_405 = 504
|
EVENT_BF2 = 503
|
||||||
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
|
|
||||||
TS_INPUT = 600
|
TS_INPUT = 600
|
||||||
TS_GRID = 601
|
TS_GRID = 601
|
||||||
TS_TOTAL = 602
|
TS_TOTAL = 602
|
||||||
|
BATT_PV1_VOLT = 1000
|
||||||
|
BATT_PV1_CUR = 1001
|
||||||
|
BATT_PV2_VOLT = 1002
|
||||||
|
BATT_PV2_CUR = 1003
|
||||||
|
BATT_38 = 1004
|
||||||
|
BATT_TOTAL_GEN = 1005
|
||||||
|
BATT_STATUS_1 = 1006
|
||||||
|
BATT_STATUS_2 = 1007
|
||||||
|
BATT_VOLT = 1010
|
||||||
|
BATT_CUR = 1011
|
||||||
|
BATT_SOC = 1012
|
||||||
|
BATT_CELL1_VOLT = 1013
|
||||||
|
BATT_CELL2_VOLT = 1014
|
||||||
|
BATT_CELL3_VOLT = 1015
|
||||||
|
BATT_CELL4_VOLT = 1016
|
||||||
|
BATT_CELL5_VOLT = 1017
|
||||||
|
BATT_CELL6_VOLT = 1018
|
||||||
|
BATT_CELL7_VOLT = 1019
|
||||||
|
BATT_CELL8_VOLT = 1020
|
||||||
|
BATT_CELL9_VOLT = 1021
|
||||||
|
BATT_CELL10_VOLT = 1022
|
||||||
|
BATT_CELL11_VOLT = 1023
|
||||||
|
BATT_CELL12_VOLT = 1024
|
||||||
|
BATT_CELL13_VOLT = 1025
|
||||||
|
BATT_CELL14_VOLT = 1026
|
||||||
|
BATT_CELL15_VOLT = 1027
|
||||||
|
BATT_CELL16_VOLT = 1028
|
||||||
|
BATT_TEMP_1 = 1029
|
||||||
|
BATT_TEMP_2 = 1030
|
||||||
|
BATT_TEMP_3 = 1031
|
||||||
|
BATT_OUT_VOLT = 1032
|
||||||
|
BATT_OUT_CUR = 1033
|
||||||
|
BATT_OUT_STATUS = 1034
|
||||||
|
BATT_TEMP_4 = 1035
|
||||||
|
BATT_74 = 1036
|
||||||
|
BATT_76 = 1037
|
||||||
|
BATT_78 = 1038
|
||||||
|
BATT_PV_PWR = 1040
|
||||||
|
BATT_PWR = 1041
|
||||||
|
BATT_OUT_PWR = 1042
|
||||||
|
|
||||||
|
TEST_VAL_0 = 2000
|
||||||
|
TEST_VAL_1 = 2001
|
||||||
|
TEST_VAL_2 = 2002
|
||||||
|
TEST_VAL_3 = 2003
|
||||||
|
TEST_VAL_4 = 2004
|
||||||
|
TEST_VAL_5 = 2005
|
||||||
|
TEST_VAL_6 = 2006
|
||||||
|
TEST_VAL_7 = 2007
|
||||||
|
TEST_VAL_8 = 2008
|
||||||
|
TEST_VAL_9 = 2009
|
||||||
|
TEST_VAL_10 = 2010
|
||||||
|
TEST_VAL_11 = 2011
|
||||||
|
TEST_VAL_12 = 2012
|
||||||
|
TEST_VAL_13 = 2013
|
||||||
|
TEST_VAL_14 = 2014
|
||||||
|
TEST_VAL_15 = 2015
|
||||||
|
TEST_VAL_16 = 2016
|
||||||
|
TEST_VAL_17 = 2017
|
||||||
|
TEST_VAL_18 = 2018
|
||||||
|
TEST_VAL_19 = 2019
|
||||||
|
TEST_VAL_20 = 2020
|
||||||
|
TEST_VAL_21 = 2021
|
||||||
|
TEST_VAL_22 = 2022
|
||||||
|
TEST_VAL_23 = 2023
|
||||||
|
TEST_VAL_24 = 2024
|
||||||
|
TEST_VAL_25 = 2025
|
||||||
|
TEST_VAL_26 = 2026
|
||||||
|
TEST_VAL_27 = 2027
|
||||||
|
TEST_VAL_28 = 2028
|
||||||
|
TEST_VAL_29 = 2029
|
||||||
|
TEST_VAL_30 = 2030
|
||||||
|
TEST_VAL_31 = 2031
|
||||||
|
TEST_VAL_32 = 2032
|
||||||
|
|
||||||
|
TEST_IVAL_1 = 2041
|
||||||
|
TEST_IVAL_2 = 2042
|
||||||
|
TEST_IVAL_3 = 2043
|
||||||
|
TEST_IVAL_4 = 2044
|
||||||
|
TEST_IVAL_5 = 2045
|
||||||
|
TEST_IVAL_6 = 2046
|
||||||
|
TEST_IVAL_7 = 2047
|
||||||
|
TEST_IVAL_8 = 2048
|
||||||
|
TEST_IVAL_9 = 2049
|
||||||
|
TEST_IVAL_10 = 2050
|
||||||
|
TEST_IVAL_11 = 2051
|
||||||
|
TEST_IVAL_12 = 2052
|
||||||
|
|
||||||
VALUE_1 = 9000
|
VALUE_1 = 9000
|
||||||
TEST_REG1 = 10000
|
TEST_REG1 = 10000
|
||||||
TEST_REG2 = 10001
|
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']
|
||||||
|
try:
|
||||||
|
res = struct.unpack_from(fmt, buf, idx)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
result = res[0]
|
||||||
|
if isinstance(result, (bytearray, bytes)):
|
||||||
|
result = result.decode().split('\x00')[0]
|
||||||
|
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:
|
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
|
__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 = {}
|
db = {}
|
||||||
@@ -157,6 +321,7 @@ class Infos:
|
|||||||
LIGHTNING = 'mdi:lightning-bolt'
|
LIGHTNING = 'mdi:lightning-bolt'
|
||||||
COUNTER = 'mdi:counter'
|
COUNTER = 'mdi:counter'
|
||||||
GAUGE = 'mdi:gauge'
|
GAUGE = 'mdi:gauge'
|
||||||
|
POWER = 'mdi:power'
|
||||||
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
|
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
|
||||||
SOLAR_POWER = 'mdi:solar-power'
|
SOLAR_POWER = 'mdi:solar-power'
|
||||||
WIFI = 'mdi:wifi'
|
WIFI = 'mdi:wifi'
|
||||||
@@ -193,6 +358,7 @@ class Infos:
|
|||||||
__info_devs = {
|
__info_devs = {
|
||||||
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
||||||
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION, 'mac': Register.MAC_ADDR, 'sn': Register.COLLECTOR_SNR}, # noqa: E501
|
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION, 'mac': Register.MAC_ADDR, 'sn': Register.COLLECTOR_SNR}, # noqa: E501
|
||||||
|
|
||||||
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
|
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
|
||||||
'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
|
'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
|
||||||
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
||||||
@@ -200,10 +366,18 @@ class Infos:
|
|||||||
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
|
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
|
||||||
'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501
|
'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501
|
||||||
'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501
|
'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501
|
||||||
|
|
||||||
|
'batterie': {'via': 'controller', 'name': 'Batterie', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
|
||||||
|
'bat_inp_pv1': {'via': 'batterie', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
|
||||||
|
'bat_inp_pv2': {'via': 'batterie', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER}, # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
__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
|
__status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
|
||||||
|
__mppt1_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_1']|int(0)]|default(value_json['Status_1'])}}" # noqa: E501
|
||||||
|
__mppt2_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_2']|int(0)]|default(value_json['Status_2'])}}" # noqa: E501
|
||||||
|
__out_status_type_val_tpl = "{%set out_status = ['Off', 'On'] %}{{out_status[value_json['out']['Out_Status']|int(0)]|default(value_json['out']['Out_Status'])}}" # noqa: E501
|
||||||
__rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
__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 = '''
|
__designed_power_val_tpl = '''
|
||||||
{% if 'Max_Designed_Power' in value_json and
|
{% if 'Max_Designed_Power' in value_json and
|
||||||
@@ -217,6 +391,121 @@ class Infos:
|
|||||||
{{ this.state }}
|
{{ this.state }}
|
||||||
{% endif %}
|
{% 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
|
__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 = {
|
__info_defs = {
|
||||||
@@ -239,6 +528,8 @@ class Infos:
|
|||||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.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': 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.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_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
@@ -252,9 +543,11 @@ class Infos:
|
|||||||
Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
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_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.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:
|
# 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.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_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.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': FMT_INT, 'name': 'Unknown Msg Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': FMT_INT, 'name': 'Invalid Data Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.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
|
||||||
@@ -269,22 +562,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
|
# 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
|
# events
|
||||||
Register.EVENT_401: {'name': ['events', '401_'], '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_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # 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_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_BF1: {'name': ['events', 'Inverter_Bitfield_1'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_BF2: {'name': ['events', 'Inverter_bitfield_2'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
# Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
# Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], '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
|
|
||||||
|
|
||||||
# grid measures:
|
# grid measures:
|
||||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
@@ -293,7 +576,9 @@ class Infos:
|
|||||||
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
Register.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_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
||||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': POWER}}, # noqa: E501
|
||||||
|
Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
|
||||||
# input measures:
|
# input measures:
|
||||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
@@ -333,16 +618,111 @@ class Infos:
|
|||||||
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||||
|
|
||||||
# controller:
|
# controller:
|
||||||
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
|
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
|
||||||
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.INFO, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
|
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
|
||||||
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.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.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.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.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.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
|
||||||
|
|
||||||
|
Register.BATT_PV1_VOLT: {'name': ['batterie', 'pv1', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_PV1_CUR: {'name': ['batterie', 'pv1', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_PV2_VOLT: {'name': ['batterie', 'pv2', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_PV2_CUR: {'name': ['batterie', 'pv2', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_38: {'name': ['batterie', 'Reg_38'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_38_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_TOTAL_GEN: {'name': ['batterie', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'batterie', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
||||||
|
Register.BATT_STATUS_1: {'name': ['batterie', 'Status_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status1_', 'name': 'MPPT-1 Status', 'val_tpl': __mppt1_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
|
||||||
|
Register.BATT_STATUS_2: {'name': ['batterie', 'Status_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status2_', 'name': 'MPPT-2 Status', 'val_tpl': __mppt2_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
|
||||||
|
Register.BATT_VOLT: {'name': ['batterie', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CUR: {'name': ['batterie', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_SOC: {'name': ['batterie', 'SOC'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'batterie', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'soc_', 'fmt': FMT_FLOAT, 'name': 'State of Charge (SOC)', 'icon': 'mdi:battery-90'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL1_VOLT: {'name': ['batterie', 'Cell', 'Volt1'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell1_', 'val_tpl': "{{ (value_json['Cell']['Volt1'] | float)}}", 'name': 'Cell-01 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL3_VOLT: {'name': ['batterie', 'Cell', 'Volt3'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell3_', 'val_tpl': "{{ (value_json['Cell']['Volt2'] | float)}}", 'name': 'Cell-03 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL4_VOLT: {'name': ['batterie', 'Cell', 'Volt4'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell4_', 'val_tpl': "{{ (value_json['Cell']['Volt3'] | float)}}", 'name': 'Cell-04 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL2_VOLT: {'name': ['batterie', 'Cell', 'Volt2'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell2_', 'val_tpl': "{{ (value_json['Cell']['Volt4'] | float)}}", 'name': 'Cell-02 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL5_VOLT: {'name': ['batterie', 'Cell', 'Volt5'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell5_', 'val_tpl': "{{ (value_json['Cell']['Volt5'] | float)}}", 'name': 'Cell-05 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL6_VOLT: {'name': ['batterie', 'Cell', 'Volt6'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell6_', 'val_tpl': "{{ (value_json['Cell']['Volt6'] | float)}}", 'name': 'Cell-06 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL7_VOLT: {'name': ['batterie', 'Cell', 'Volt7'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell7_', 'val_tpl': "{{ (value_json['Cell']['Volt7'] | float)}}", 'name': 'Cell-07 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL8_VOLT: {'name': ['batterie', 'Cell', 'Volt8'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell8_', 'val_tpl': "{{ (value_json['Cell']['Volt8'] | float)}}", 'name': 'Cell-08 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL9_VOLT: {'name': ['batterie', 'Cell', 'Volt9'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell9_', 'val_tpl': "{{ (value_json['Cell']['Volt9'] | float)}}", 'name': 'Cell-09 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL10_VOLT: {'name': ['batterie', 'Cell', 'Volt10'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell10_', 'val_tpl': "{{ (value_json['Cell']['Volt10'] | float)}}", 'name': 'Cell-10 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL11_VOLT: {'name': ['batterie', 'Cell', 'Volt11'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell11_', 'val_tpl': "{{ (value_json['Cell']['Volt11'] | float)}}", 'name': 'Cell-11 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL12_VOLT: {'name': ['batterie', 'Cell', 'Volt12'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell12_', 'val_tpl': "{{ (value_json['Cell']['Volt12'] | float)}}", 'name': 'Cell-12 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL13_VOLT: {'name': ['batterie', 'Cell', 'Volt13'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell13_', 'val_tpl': "{{ (value_json['Cell']['Volt13'] | float)}}", 'name': 'Cell-13 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL14_VOLT: {'name': ['batterie', 'Cell', 'Volt14'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell14_', 'val_tpl': "{{ (value_json['Cell']['Volt14'] | float)}}", 'name': 'Cell-14 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL15_VOLT: {'name': ['batterie', 'Cell', 'Volt15'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell15_', 'val_tpl': "{{ (value_json['Cell']['Volt15'] | float)}}", 'name': 'Cell-15 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_CELL16_VOLT: {'name': ['batterie', 'Cell', 'Volt16'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell16_', 'val_tpl': "{{ (value_json['Cell']['Volt16'] | float)}}", 'name': 'Cell-16 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_TEMP_1: {'name': ['batterie', 'Temp_1'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_1_', 'fmt': FMT_INT, 'name': 'Batterie Temp-1', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_TEMP_2: {'name': ['batterie', 'Temp_2'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_2_', 'fmt': FMT_INT, 'name': 'Batterie Temp-2', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_TEMP_3: {'name': ['batterie', 'Temp_3'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_3_', 'fmt': FMT_INT, 'name': 'Batterie Temp-3', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Output Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Output Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_OUT_STATUS: {'name': ['batterie', 'out', 'Out_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'out_status_', 'name': 'Output Status', 'val_tpl': __out_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
|
||||||
|
Register.BATT_TEMP_4: {'name': ['batterie', 'Controller_Temp'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_4_', 'fmt': FMT_INT, 'name': 'Ctrl Temperature'}}, # noqa: E501
|
||||||
|
Register.BATT_74: {'name': ['batterie', 'Reg_74'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_74_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_76: {'name': ['batterie', 'Reg_76'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_76_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_78: {'name': ['batterie', 'Reg_78'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_78_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.BATT_PV_PWR: {'name': ['batterie', 'PV_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'pv_power_', 'fmt': FMT_INT, 'name': 'PV Power'}}, # noqa: E501
|
||||||
|
Register.BATT_PWR: {'name': ['batterie', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_', 'fmt': FMT_INT, 'name': 'Batterie Power'}}, # noqa: E501
|
||||||
|
Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Output Power'}}, # noqa: E501
|
||||||
|
|
||||||
|
Register.TEST_VAL_0: {'name': ['input', 'Val_0'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_0_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_1: {'name': ['input', 'Val_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_2: {'name': ['input', 'Val_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_3: {'name': ['input', 'Val_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_4: {'name': ['input', 'Val_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_5: {'name': ['input', 'Val_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_6: {'name': ['input', 'Val_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_7: {'name': ['input', 'Val_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_8: {'name': ['input', 'Val_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_9: {'name': ['input', 'Val_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_10: {'name': ['input', 'Val_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_11: {'name': ['input', 'Val_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_12: {'name': ['input', 'Val_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_13: {'name': ['input', 'Val_13'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_13_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_14: {'name': ['input', 'Val_14'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_14_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_15: {'name': ['input', 'Val_15'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_15_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_16: {'name': ['input', 'Val_16'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_16_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_17: {'name': ['input', 'Val_17'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_17_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_18: {'name': ['input', 'Val_18'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_18_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_19: {'name': ['input', 'Val_19'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_19_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_20: {'name': ['input', 'Val_20'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_20_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_21: {'name': ['input', 'Val_21'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_21_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_22: {'name': ['input', 'Val_22'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_22_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_23: {'name': ['input', 'Val_23'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_23_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_24: {'name': ['input', 'Val_24'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_24_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_25: {'name': ['input', 'Val_25'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_25_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_26: {'name': ['input', 'Val_26'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_26_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_27: {'name': ['input', 'Val_27'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_27_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_28: {'name': ['input', 'Val_28'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_28_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_29: {'name': ['input', 'Val_29'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_29_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_30: {'name': ['input', 'Val_30'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_30_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_31: {'name': ['input', 'Val_31'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_31_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_VAL_32: {'name': ['input', 'Val_32'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_32_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
|
||||||
|
Register.TEST_IVAL_1: {'name': ['input', 'iVal_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_2: {'name': ['input', 'iVal_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_3: {'name': ['input', 'iVal_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_4: {'name': ['input', 'iVal_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_5: {'name': ['input', 'iVal_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_6: {'name': ['input', 'iVal_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_7: {'name': ['input', 'iVal_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_8: {'name': ['input', 'iVal_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_9: {'name': ['input', 'iVal_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_10: {'name': ['input', 'iVal_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_11: {'name': ['input', 'iVal_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.TEST_IVAL_12: {'name': ['input', 'iVal_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -652,6 +1032,8 @@ class Infos:
|
|||||||
|
|
||||||
def get_db_value(self, id: Register, not_found_result: any = None):
|
def get_db_value(self, id: Register, not_found_result: any = None):
|
||||||
'''get database value'''
|
'''get database value'''
|
||||||
|
if id not in self.info_defs:
|
||||||
|
return not_found_result
|
||||||
row = self.info_defs[id]
|
row = self.info_defs[id]
|
||||||
if isinstance(row, dict):
|
if isinstance(row, dict):
|
||||||
keys = row['name']
|
keys = row['name']
|
||||||
|
|||||||
@@ -6,23 +6,15 @@ import json
|
|||||||
import gc
|
import gc
|
||||||
from aiomqtt import MqttCodeError
|
from aiomqtt import MqttCodeError
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
if __name__ == "app.src.inverter_base":
|
from inverter_ifc import InverterIfc
|
||||||
from app.src.inverter_ifc import InverterIfc
|
from proxy import Proxy
|
||||||
from app.src.proxy import Proxy
|
from async_stream import StreamPtr
|
||||||
from app.src.async_stream import StreamPtr
|
from async_stream import AsyncStreamClient
|
||||||
from app.src.async_stream import AsyncStreamClient
|
from async_stream import AsyncStreamServer
|
||||||
from app.src.async_stream import AsyncStreamServer
|
from cnf.config import Config
|
||||||
from app.src.config import Config
|
from infos import Infos
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from inverter_ifc import InverterIfc
|
|
||||||
from proxy import Proxy
|
|
||||||
from async_stream import StreamPtr
|
|
||||||
from async_stream import AsyncStreamClient
|
|
||||||
from async_stream import AsyncStreamServer
|
|
||||||
from config import Config
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
@@ -31,12 +23,16 @@ class InverterBase(InverterIfc, Proxy):
|
|||||||
|
|
||||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
config_id: str, prot_class,
|
config_id: str, prot_class,
|
||||||
client_mode: bool = False):
|
client_mode: bool = False,
|
||||||
|
remote_prot_class=None):
|
||||||
Proxy.__init__(self)
|
Proxy.__init__(self)
|
||||||
self._registry.append(weakref.ref(self))
|
self._registry.append(weakref.ref(self))
|
||||||
self.addr = writer.get_extra_info('peername')
|
self.addr = writer.get_extra_info('peername')
|
||||||
self.config_id = config_id
|
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.__ha_restarts = -1
|
||||||
self.remote = StreamPtr(None)
|
self.remote = StreamPtr(None)
|
||||||
ifc = AsyncStreamServer(reader, writer,
|
ifc = AsyncStreamServer(reader, writer,
|
||||||
@@ -45,7 +41,7 @@ class InverterBase(InverterIfc, Proxy):
|
|||||||
self.remote)
|
self.remote)
|
||||||
|
|
||||||
self.local = StreamPtr(
|
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):
|
def __enter__(self):
|
||||||
@@ -106,6 +102,20 @@ class InverterBase(InverterIfc, Proxy):
|
|||||||
logging.info(f'[{stream.node_id}] Connect to {addr}')
|
logging.info(f'[{stream.node_id}] Connect to {addr}')
|
||||||
connect = asyncio.open_connection(host, port)
|
connect = asyncio.open_connection(host, port)
|
||||||
reader, writer = await connect
|
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(
|
ifc = AsyncStreamClient(
|
||||||
reader, writer, self.local, self.__del_remote)
|
reader, writer, self.local, self.__del_remote)
|
||||||
|
|
||||||
@@ -141,6 +151,8 @@ class InverterBase(InverterIfc, Proxy):
|
|||||||
# home assistant has changed the status back to online
|
# home assistant has changed the status back to online
|
||||||
try:
|
try:
|
||||||
if (('inverter' in stream.new_data and stream.new_data['inverter'])
|
if (('inverter' in stream.new_data and stream.new_data['inverter'])
|
||||||
|
or ('batterie' in stream.new_data and
|
||||||
|
stream.new_data['batterie'])
|
||||||
or ('collector' in stream.new_data and
|
or ('collector' in stream.new_data and
|
||||||
stream.new_data['collector'])
|
stream.new_data['collector'])
|
||||||
or self.mqtt.ha_restarts != self.__ha_restarts):
|
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ from abc import abstractmethod
|
|||||||
import logging
|
import logging
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
|
||||||
if __name__ == "app.src.inverter_ifc":
|
from iter_registry import AbstractIterMeta
|
||||||
from app.src.iter_registry import AbstractIterMeta
|
|
||||||
else: # pragma: no cover
|
|
||||||
from iter_registry import AbstractIterMeta
|
|
||||||
|
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|||||||
@@ -58,19 +58,19 @@ formatter=console_formatter
|
|||||||
class=handlers.TimedRotatingFileHandler
|
class=handlers.TimedRotatingFileHandler
|
||||||
level=INFO
|
level=INFO
|
||||||
formatter=file_formatter
|
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]
|
[handler_file_handler_name2]
|
||||||
class=handlers.TimedRotatingFileHandler
|
class=handlers.TimedRotatingFileHandler
|
||||||
level=NOTSET
|
level=NOTSET
|
||||||
formatter=file_formatter
|
formatter=file_formatter
|
||||||
args=('log/trace.log', when:='midnight')
|
args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups)
|
||||||
|
|
||||||
[formatter_console_formatter]
|
[formatter_console_formatter]
|
||||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s
|
||||||
datefmt='%Y-%m-%d %H:%M:%S
|
datefmt=%Y-%m-%d %H:%M:%S
|
||||||
|
|
||||||
[formatter_file_formatter]
|
[formatter_file_formatter]
|
||||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s
|
||||||
datefmt='%Y-%m-%d %H:%M:%S
|
datefmt=%Y-%m-%d %H:%M:%S
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,11 @@ import weakref
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from async_ifc import AsyncIfc
|
||||||
if __name__ == "app.src.messages":
|
from protocol_ifc import ProtocolIfc
|
||||||
from app.src.protocol_ifc import ProtocolIfc
|
from infos import Infos, Register
|
||||||
from app.src.infos import Infos, Register
|
from modbus import Modbus
|
||||||
from app.src.modbus import Modbus
|
from my_timer import Timer
|
||||||
else: # pragma: no cover
|
|
||||||
from protocol_ifc import ProtocolIfc
|
|
||||||
from infos import Infos, Register
|
|
||||||
from modbus import Modbus
|
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -89,26 +85,43 @@ class Message(ProtocolIfc):
|
|||||||
'''maximum time without a received msg from the inverter in sec'''
|
'''maximum time without a received msg from the inverter in sec'''
|
||||||
MAX_DEF_IDLE_TIME = 360
|
MAX_DEF_IDLE_TIME = 360
|
||||||
'''maximum default time without a received msg in sec'''
|
'''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:
|
def __init__(self, node_id, ifc: "AsyncIfc", server_side: bool,
|
||||||
Callable[[bytes, int, str], None], mb_timeout: int):
|
send_modbus_cb: Callable[[bytes, int, str], None],
|
||||||
|
mb_timeout: int):
|
||||||
self._registry.append(weakref.ref(self))
|
self._registry.append(weakref.ref(self))
|
||||||
|
|
||||||
self.server_side = server_side
|
self.server_side = server_side
|
||||||
|
self.ifc = ifc
|
||||||
|
self.node_id = node_id
|
||||||
if server_side:
|
if server_side:
|
||||||
self.mb = Modbus(send_modbus_cb, mb_timeout)
|
self.mb = Modbus(send_modbus_cb, mb_timeout)
|
||||||
|
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||||
else:
|
else:
|
||||||
self.mb = None
|
self.mb = None
|
||||||
|
self.mb_timer = None
|
||||||
self.header_valid = False
|
self.header_valid = False
|
||||||
self.header_len = 0
|
self.header_len = 0
|
||||||
self.data_len = 0
|
self.data_len = 0
|
||||||
self.unique_id = 0
|
self.unique_id = 0
|
||||||
self._node_id = ''
|
|
||||||
self.sug_area = ''
|
self.sug_area = ''
|
||||||
self.new_data = {}
|
self.new_data = {}
|
||||||
self.state = State.init
|
self.state = State.init
|
||||||
self.shutdown_started = False
|
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
|
||||||
|
self.mb_start_reg = 0
|
||||||
|
self.mb_step = 0
|
||||||
|
self.mb_bytes = 0
|
||||||
|
self.mb_inv_no = 1
|
||||||
|
self.mb_scan = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node_id(self):
|
def node_id(self):
|
||||||
@@ -127,6 +140,25 @@ class Message(ProtocolIfc):
|
|||||||
# to our _recv_buffer
|
# to our _recv_buffer
|
||||||
return # pragma: no cover
|
return # pragma: no cover
|
||||||
|
|
||||||
|
def _set_config_parms(self, inv: dict):
|
||||||
|
'''init connection with params from the configuration'''
|
||||||
|
self.node_id = inv['node_id']
|
||||||
|
self.sug_area = inv['suggested_area']
|
||||||
|
self.modbus_polling = inv['modbus_polling']
|
||||||
|
if 'modbus_scanning' in inv:
|
||||||
|
scan = inv['modbus_scanning']
|
||||||
|
self.mb_scan = True
|
||||||
|
self.mb_start_reg = scan['start']
|
||||||
|
self.mb_step = scan['step']
|
||||||
|
self.mb_bytes = scan['bytes']
|
||||||
|
if 'client_mode' in inv:
|
||||||
|
self.mb_start_reg = scan['start']
|
||||||
|
else:
|
||||||
|
self.mb_start_reg = scan['start'] - scan['step']
|
||||||
|
self.mb_start_reg &= 0xffff
|
||||||
|
if self.mb:
|
||||||
|
self.mb.set_node_id(self.node_id)
|
||||||
|
|
||||||
def _set_mqtt_timestamp(self, key, ts: float | None):
|
def _set_mqtt_timestamp(self, key, ts: float | None):
|
||||||
if key not in self.new_data or \
|
if key not in self.new_data or \
|
||||||
not self.new_data[key]:
|
not self.new_data[key]:
|
||||||
@@ -152,10 +184,59 @@ class Message(ProtocolIfc):
|
|||||||
to = self.MAX_DEF_IDLE_TIME
|
to = self.MAX_DEF_IDLE_TIME
|
||||||
return to
|
return to
|
||||||
|
|
||||||
|
def _send_modbus_cmd(self, dev_id, func, addr, val, log_lvl) -> None:
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
|
' as the state is not UP')
|
||||||
|
return
|
||||||
|
self.mb.build_msg(dev_id, func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||||
|
self._send_modbus_cmd(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
def _send_modbus_scan(self):
|
||||||
|
self.mb_start_reg += self.mb_step
|
||||||
|
if self.mb_start_reg > 0xffff:
|
||||||
|
self.mb_start_reg = self.mb_start_reg & 0xffff
|
||||||
|
self.mb_inv_no += 1
|
||||||
|
logging.info(f"Next Round: inv:{self.mb_inv_no}"
|
||||||
|
f" reg:{self.mb_start_reg:04x}")
|
||||||
|
if (self.mb_start_reg & 0xfffc) % 0x80 == 0:
|
||||||
|
logging.info(f"[{self.node_id}] Scan info: "
|
||||||
|
f"inv:{self.mb_inv_no}"
|
||||||
|
f" reg:{self.mb_start_reg:04x}")
|
||||||
|
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
|
||||||
|
self.mb_start_reg, self.mb_bytes,
|
||||||
|
logging.INFO)
|
||||||
|
|
||||||
|
def _dump_modbus_scan(self, data, hdr_len, modbus_msg_len):
|
||||||
|
if (data[hdr_len] == self.mb_inv_no and
|
||||||
|
data[hdr_len+1] == Modbus.READ_REGS):
|
||||||
|
logging.info(f'[{self.node_id}] Valid MODBUS data '
|
||||||
|
f'(reg: 0x{self.mb.last_reg:04x}):')
|
||||||
|
hex_dump_memory(logging.INFO, 'Valid MODBUS data '
|
||||||
|
f'(reg: 0x{self.mb.last_reg:04x}):',
|
||||||
|
data[hdr_len:], modbus_msg_len)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
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:
|
if self.mb:
|
||||||
self.mb.close()
|
self.mb.close()
|
||||||
self.mb = None
|
self.mb = None
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Generator, Callable
|
from typing import Generator, Callable
|
||||||
|
|
||||||
if __name__ == "app.src.modbus":
|
from infos import Register, Fmt
|
||||||
from app.src.infos import Register
|
|
||||||
else: # pragma: no cover
|
|
||||||
from infos import Register
|
|
||||||
|
|
||||||
logger = logging.getLogger('data')
|
logger = logging.getLogger('data')
|
||||||
|
|
||||||
@@ -40,15 +37,69 @@ class Modbus():
|
|||||||
|
|
||||||
__crc_tab = []
|
__crc_tab = []
|
||||||
mb_reg_mapping = {
|
mb_reg_mapping = {
|
||||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x0000: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||||
|
0x0008: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage
|
||||||
|
0x0009: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current
|
||||||
|
0x000a: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage
|
||||||
|
0x000b: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current
|
||||||
|
0x000c: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x000d: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x000e: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x000f: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x0010: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x0011: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x0012: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent
|
||||||
|
0x0013: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0014: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0015: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0016: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0017: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0018: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0019: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001a: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001b: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001c: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001d: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001e: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x001f: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0020: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0021: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0022: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
|
||||||
|
0x0023: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x0024: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x0025: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x0026: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x0027: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x0028: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x0029: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501
|
||||||
|
0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501
|
||||||
|
|
||||||
|
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||||
|
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||||
|
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||||
|
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
|
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||||
|
|
||||||
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # 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
|
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # 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
|
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
|
# 0x300d
|
||||||
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
@@ -74,6 +125,7 @@ class Modbus():
|
|||||||
0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
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
|
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
|
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
# 0x302a
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
||||||
@@ -117,6 +169,9 @@ class Modbus():
|
|||||||
while not self.que.empty():
|
while not self.que.empty():
|
||||||
self.que.get_nowait()
|
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,
|
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||||
log_lvl=logging.DEBUG) -> None:
|
log_lvl=logging.DEBUG) -> None:
|
||||||
"""Build MODBUS RTU request frame and add it to the tx queue
|
"""Build MODBUS RTU request frame and add it to the tx queue
|
||||||
@@ -160,14 +215,13 @@ class Modbus():
|
|||||||
|
|
||||||
return True
|
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[tuple[str, bool, int | float | str], None, None]:
|
||||||
"""Generator which check and parse a received MODBUS response.
|
"""Generator which check and parse a received MODBUS response.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
info_db: database for info lockups
|
info_db: database for info lockups
|
||||||
buf: received Modbus RTU response frame
|
buf: received Modbus RTU response frame
|
||||||
node_id: string for logging which identifies the slave
|
|
||||||
|
|
||||||
Returns on error and set Self.err to:
|
Returns on error and set Self.err to:
|
||||||
1: CRC error
|
1: CRC error
|
||||||
@@ -177,7 +231,6 @@ class Modbus():
|
|||||||
5: No MODBUS request pending
|
5: No MODBUS request pending
|
||||||
"""
|
"""
|
||||||
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||||
self.node_id = node_id
|
|
||||||
|
|
||||||
fcode = buf[1]
|
fcode = buf[1]
|
||||||
data_available = self.last_addr == self.INV_ADDR and \
|
data_available = self.last_addr == self.INV_ADDR and \
|
||||||
@@ -228,17 +281,6 @@ class Modbus():
|
|||||||
|
|
||||||
return False
|
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):
|
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
|
||||||
'''Generator over received registers, updates the db'''
|
'''Generator over received registers, updates the db'''
|
||||||
for i in range(0, elmlen):
|
for i in range(0, elmlen):
|
||||||
@@ -248,7 +290,7 @@ class Modbus():
|
|||||||
info_id = row['reg']
|
info_id = row['reg']
|
||||||
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||||
if keys:
|
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,
|
name, update = info_db.update_db(keys, must_incr,
|
||||||
result)
|
result)
|
||||||
yield keys[0], update, result
|
yield keys[0], update, result
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
if __name__ == "app.src.modbus_tcp":
|
from cnf.config import Config
|
||||||
from app.src.config import Config
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
from infos import Infos
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from config import Config
|
|
||||||
from gen3plus.inverter_g3p import InverterG3P
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
@@ -47,25 +43,28 @@ class ModbusTcp():
|
|||||||
self.tim_restart = tim_restart
|
self.tim_restart = tim_restart
|
||||||
|
|
||||||
inverters = Config.get('inverters')
|
inverters = Config.get('inverters')
|
||||||
|
batteries = Config.get('batteries')
|
||||||
# logging.info(f'Inverters: {inverters}')
|
# logging.info(f'Inverters: {inverters}')
|
||||||
|
|
||||||
for inv in inverters.values():
|
for _, inv in chain(inverters.items(), batteries.items()):
|
||||||
if (type(inv) is dict
|
if (type(inv) is dict
|
||||||
and 'monitor_sn' in inv
|
and 'monitor_sn' in inv
|
||||||
and 'client_mode' in inv):
|
and 'client_mode' in inv):
|
||||||
client = inv['client_mode']
|
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 Monitoring-SN: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501
|
||||||
loop.create_task(self.modbus_loop(client['host'],
|
loop.create_task(self.modbus_loop(client['host'],
|
||||||
client['port'],
|
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)'''
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with ModbusConn(host, port) as inverter:
|
async with ModbusConn(host, port) as inverter:
|
||||||
stream = inverter.local.stream
|
stream = inverter.local.stream
|
||||||
await stream.send_start_cmd(snr, host)
|
await stream.send_start_cmd(snr, host, forward)
|
||||||
await stream.ifc.loop()
|
await stream.ifc.loop()
|
||||||
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||||
f'Connection closed - Shutdown: '
|
f'Connection closed - Shutdown: '
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import aiomqtt
|
import aiomqtt
|
||||||
import traceback
|
import traceback
|
||||||
if __name__ == "app.src.mqtt":
|
|
||||||
from app.src.modbus import Modbus
|
from modbus import Modbus
|
||||||
from app.src.messages import Message
|
from messages import Message
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
else: # pragma: no cover
|
|
||||||
from modbus import Modbus
|
|
||||||
from messages import Message
|
|
||||||
from config import Config
|
|
||||||
from singleton import Singleton
|
|
||||||
|
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
if __name__ == "app.src.protocol_ifc":
|
from async_ifc import AsyncIfc
|
||||||
from app.src.iter_registry import AbstractIterMeta
|
from iter_registry import AbstractIterMeta
|
||||||
from app.src.async_ifc import AsyncIfc
|
|
||||||
else: # pragma: no cover
|
|
||||||
from iter_registry import AbstractIterMeta
|
|
||||||
from async_ifc import AsyncIfc
|
|
||||||
|
|
||||||
|
|
||||||
class ProtocolIfc(metaclass=AbstractIterMeta):
|
class ProtocolIfc(metaclass=AbstractIterMeta):
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
if __name__ == "app.src.proxy":
|
from cnf.config import Config
|
||||||
from app.src.config import Config
|
from mqtt import Mqtt
|
||||||
from app.src.mqtt import Mqtt
|
from infos import Infos
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from config import Config
|
|
||||||
from mqtt import Mqtt
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
@@ -61,8 +57,9 @@ class Proxy():
|
|||||||
# reset at midnight when you restart the proxy just before
|
# reset at midnight when you restart the proxy just before
|
||||||
# midnight!
|
# midnight!
|
||||||
inverters = Config.get('inverters')
|
inverters = Config.get('inverters')
|
||||||
|
batteries = Config.get('batteries')
|
||||||
# logger.debug(f'Proxys: {inverters}')
|
# logger.debug(f'Proxys: {inverters}')
|
||||||
for inv in inverters.values():
|
for _, inv in chain(inverters.items(), batteries.items()):
|
||||||
if (type(inv) is dict):
|
if (type(inv) is dict):
|
||||||
node_id = inv['node_id']
|
node_id = inv['node_id']
|
||||||
cls.db_stat.reg_clr_at_midnight(f'{cls.entity_prfx}{node_id}',
|
cls.db_stat.reg_clr_at_midnight(f'{cls.entity_prfx}{node_id}',
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging.handlers
|
||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
|
import argparse
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from logging import config # noqa F401
|
from logging import config # noqa F401
|
||||||
@@ -10,7 +12,10 @@ from inverter_ifc import InverterIfc
|
|||||||
from gen3.inverter_g3 import InverterG3
|
from gen3.inverter_g3 import InverterG3
|
||||||
from gen3plus.inverter_g3p import InverterG3P
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
from scheduler import Schedule
|
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
|
from modbus_tcp import ModbusTcp
|
||||||
|
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
@@ -77,7 +82,7 @@ async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
|||||||
await inv.local.ifc.server_loop()
|
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'''
|
'''Close all TCP connections and stop the event loop'''
|
||||||
|
|
||||||
logging.info('Shutdown due to SIGTERM')
|
logging.info('Shutdown due to SIGTERM')
|
||||||
@@ -112,46 +117,89 @@ async def handle_shutdown(web_task):
|
|||||||
loop.stop()
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
def get_log_level() -> int:
|
def get_log_level() -> int | None:
|
||||||
'''checks if LOG_LVL is set in the environment and returns the
|
'''checks if LOG_LVL is set in the environment and returns the
|
||||||
corresponding logging.LOG_LEVEL'''
|
corresponding logging.LOG_LEVEL'''
|
||||||
log_level = os.getenv('LOG_LVL', 'INFO')
|
switch = {
|
||||||
if log_level == 'DEBUG':
|
'DEBUG': logging.DEBUG,
|
||||||
log_level = logging.DEBUG
|
'WARN': logging.WARNING,
|
||||||
elif log_level == 'WARN':
|
'INFO': logging.INFO,
|
||||||
log_level = logging.WARNING
|
'ERROR': logging.ERROR,
|
||||||
else:
|
}
|
||||||
log_level = logging.INFO
|
log_level = os.getenv('LOG_LVL', None)
|
||||||
return log_level
|
logging.info(f"LOG_LVL : {log_level}")
|
||||||
|
|
||||||
|
return switch.get(log_level, None)
|
||||||
|
|
||||||
|
|
||||||
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
|
# Setup our daily, rotating logger
|
||||||
#
|
#
|
||||||
serv_name = os.getenv('SERVICE_NAME', 'proxy')
|
serv_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||||
version = os.getenv('VERSION', 'unknown')
|
version = os.getenv('VERSION', 'unknown')
|
||||||
|
|
||||||
logging.config.fileConfig('logging.ini')
|
setattr(logging.handlers, "log_path", args.log_path)
|
||||||
logging.info(f'Server "{serv_name} - {version}" will be started')
|
setattr(logging.handlers, "log_backups", args.log_backups)
|
||||||
|
os.makedirs(args.log_path, exist_ok=True)
|
||||||
|
|
||||||
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
src_dir = os.path.dirname(__file__) + '/'
|
||||||
|
logging.config.fileConfig(src_dir + 'logging.ini')
|
||||||
|
logging.info(f'Server "{serv_name} - {version}" will be started')
|
||||||
|
logging.info(f'current dir: {os.getcwd()}')
|
||||||
|
logging.info(f"config_path: {args.config_path}")
|
||||||
|
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()
|
log_level = get_log_level()
|
||||||
logging.getLogger().setLevel(log_level)
|
logging.info('******')
|
||||||
logging.getLogger('msg').setLevel(log_level)
|
if log_level:
|
||||||
logging.getLogger('conn').setLevel(log_level)
|
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
||||||
logging.getLogger('data').setLevel(log_level)
|
logging.getLogger().setLevel(log_level)
|
||||||
logging.getLogger('tracer').setLevel(log_level)
|
logging.getLogger('msg').setLevel(log_level)
|
||||||
logging.getLogger('asyncio').setLevel(log_level)
|
logging.getLogger('conn').setLevel(log_level)
|
||||||
# logging.getLogger('mqtt').setLevel(log_level)
|
logging.getLogger('data').setLevel(log_level)
|
||||||
|
logging.getLogger('tracer').setLevel(log_level)
|
||||||
|
logging.getLogger('asyncio').setLevel(log_level)
|
||||||
|
# logging.getLogger('mqtt').setLevel(log_level)
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
# read config file
|
# read config file
|
||||||
ConfigErr = Config.class_init()
|
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"))
|
||||||
if ConfigErr is not None:
|
ConfigReadEnv()
|
||||||
logging.info(f'ConfigErr: {ConfigErr}')
|
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()
|
Proxy.class_init()
|
||||||
Schedule.start()
|
Schedule.start()
|
||||||
ModbusTcp(loop)
|
ModbusTcp(loop)
|
||||||
@@ -162,6 +210,7 @@ if __name__ == "__main__":
|
|||||||
# and we can't receive and handle the UNIX signals!
|
# and we can't receive and handle the UNIX signals!
|
||||||
#
|
#
|
||||||
for inv_class, port in [(InverterG3, 5005), (InverterG3P, 10000)]:
|
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:
|
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
|
||||||
handle_client(r, w, i),
|
handle_client(r, w, i),
|
||||||
'0.0.0.0', port))
|
'0.0.0.0', port))
|
||||||
@@ -174,12 +223,12 @@ if __name__ == "__main__":
|
|||||||
for signame in ('SIGINT', 'SIGTERM'):
|
for signame in ('SIGINT', 'SIGTERM'):
|
||||||
loop.add_signal_handler(getattr(signal, signame),
|
loop.add_signal_handler(getattr(signal, signame),
|
||||||
lambda loop=loop: asyncio.create_task(
|
lambda loop=loop: asyncio.create_task(
|
||||||
handle_shutdown(web_task)))
|
handle_shutdown(loop, web_task)))
|
||||||
|
|
||||||
loop.set_debug(log_level == logging.DEBUG)
|
loop.set_debug(log_level == logging.DEBUG)
|
||||||
try:
|
try:
|
||||||
if ConfigErr is None:
|
global proxy_is_up
|
||||||
proxy_is_up = True
|
proxy_is_up = True
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
@@ -189,3 +238,7 @@ if __name__ == "__main__":
|
|||||||
logging.debug('Close event loop')
|
logging.debug('Close event loop')
|
||||||
loop.close()
|
loop.close()
|
||||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import asyncio
|
|||||||
import gc
|
import gc
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from app.src.infos import Infos
|
from infos import Infos
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||||
from app.src.messages import Message
|
from 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 test_modbus_tcp import FakeReader, FakeWriter
|
||||||
|
from test_inverter_base import config_conn, patch_open_connection
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
@@ -17,10 +18,13 @@ pytest_plugins = ('pytest_asyncio',)
|
|||||||
Infos.static_init()
|
Infos.static_init()
|
||||||
|
|
||||||
class FakeProto(Message):
|
class FakeProto(Message):
|
||||||
def __init__(self, server_side):
|
def __init__(self, ifc, server_side):
|
||||||
super().__init__(server_side, None, 10)
|
super().__init__('G3F', ifc, server_side, None, 10)
|
||||||
self.conn_no = 0
|
self.conn_no = 0
|
||||||
|
|
||||||
|
def mb_timout_cb(self, exp_cnt):
|
||||||
|
pass # empty callback
|
||||||
|
|
||||||
def fake_reader_fwd():
|
def fake_reader_fwd():
|
||||||
reader = FakeReader()
|
reader = FakeReader()
|
||||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
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:
|
elif test_type == TestType.FWD_RUNTIME_ERROR_NO_STREAM:
|
||||||
remote.stream = None
|
remote.stream = None
|
||||||
raise RuntimeError("Peer closed")
|
raise RuntimeError("Peer closed")
|
||||||
|
return True
|
||||||
|
|
||||||
def close():
|
def close():
|
||||||
return
|
return
|
||||||
@@ -349,7 +354,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False):
|
|||||||
FakeReader(), FakeWriter(), StreamPtr(None), close_hndl)
|
FakeReader(), FakeWriter(), StreamPtr(None), close_hndl)
|
||||||
remote.ifc.prot_set_update_header_cb(update_hdr)
|
remote.ifc.prot_set_update_header_cb(update_hdr)
|
||||||
remote.ifc.prot_set_init_new_client_conn_cb(callback)
|
remote.ifc.prot_set_init_new_client_conn_cb(callback)
|
||||||
remote.stream = FakeProto(False)
|
remote.stream = FakeProto(remote.ifc, False)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_forward():
|
async def test_forward():
|
||||||
@@ -530,3 +535,39 @@ async def test_forward_runtime_error3():
|
|||||||
await ifc.server_loop()
|
await ifc.server_loop()
|
||||||
assert cnt == 1
|
assert cnt == 1
|
||||||
del ifc
|
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
|
# test_with_pytest.py
|
||||||
|
|
||||||
from app.src.byte_fifo import ByteFifo
|
from byte_fifo import ByteFifo
|
||||||
|
|
||||||
def test_fifo():
|
def test_fifo():
|
||||||
read = ByteFifo()
|
read = ByteFifo()
|
||||||
|
|||||||
@@ -1,16 +1,56 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import tomllib
|
import pytest
|
||||||
|
import json
|
||||||
|
from mock import patch
|
||||||
from schema import SchemaMissingKeyError
|
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
|
@classmethod
|
||||||
def set(cls, cnf):
|
def __init__(cls, cnf):
|
||||||
cls.act_config = cnf
|
cls.act_config = cnf
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _read_config_file(cls) -> dict:
|
def add_config(cls) -> dict:
|
||||||
return cls.act_config
|
return cls.act_config
|
||||||
|
|
||||||
|
|
||||||
@@ -22,15 +62,139 @@ def test_empty_config():
|
|||||||
except SchemaMissingKeyError:
|
except SchemaMissingKeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_default_config():
|
@pytest.fixture
|
||||||
with open("app/config/default_config.toml", "rb") as f:
|
def ConfigDefault():
|
||||||
cnf = tomllib.load(f)
|
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': {
|
||||||
|
'suggested_area': '',
|
||||||
|
'modbus_polling': False,
|
||||||
|
'monitor_sn': 0,
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-395M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-395M'},
|
||||||
|
'sensor_list': 0
|
||||||
|
},
|
||||||
|
'Y170000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 2000000000,
|
||||||
|
'suggested_area': '',
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv3': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv4': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'suggested_area': '',
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
@pytest.fixture
|
||||||
validated = Config.conf_schema.validate(cnf)
|
def ConfigComplete():
|
||||||
except Exception:
|
return {
|
||||||
assert False
|
'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},
|
||||||
|
'Y170000000000002': {'modbus_polling': False,
|
||||||
|
'modbus_scanning': {
|
||||||
|
'bytes': 16,
|
||||||
|
'start': 2048,
|
||||||
|
'step': 1024
|
||||||
|
},
|
||||||
|
'monitor_sn': 2000000001,
|
||||||
|
'node_id': 'PV-Garage3/',
|
||||||
|
'suggested_area': 'Garage3',
|
||||||
|
'sensor_list': 688}
|
||||||
|
},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'suggested_area': 'Garage3',
|
||||||
|
'node_id': 'Bat-Garage3/',
|
||||||
|
'pv1': {'manufacturer': 'man5',
|
||||||
|
'type': 'type5'},
|
||||||
|
'pv2': {'manufacturer': 'man6',
|
||||||
|
'type': 'type6'},
|
||||||
|
'sensor_list': 12326}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_default_config():
|
||||||
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
|
validated = Config.def_config
|
||||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0,
|
||||||
|
'suggested_area': ''
|
||||||
|
}
|
||||||
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': False,
|
'allow_all': False,
|
||||||
'R170000000000001': {
|
'R170000000000001': {
|
||||||
@@ -42,7 +206,7 @@ def test_default_config():
|
|||||||
'modbus_polling': False,
|
'modbus_polling': False,
|
||||||
'monitor_sn': 0,
|
'monitor_sn': 0,
|
||||||
'suggested_area': '',
|
'suggested_area': '',
|
||||||
'sensor_list': 688},
|
'sensor_list': 0},
|
||||||
'Y170000000000001': {
|
'Y170000000000001': {
|
||||||
'modbus_polling': True,
|
'modbus_polling': True,
|
||||||
'monitor_sn': 2000000000,
|
'monitor_sn': 2000000000,
|
||||||
@@ -56,100 +220,93 @@ def test_default_config():
|
|||||||
'pv4': {'manufacturer': 'Risen',
|
'pv4': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'suggested_area': '',
|
'suggested_area': '',
|
||||||
'sensor_list': 688}}}
|
'sensor_list': 0}}}
|
||||||
|
|
||||||
def test_full_config():
|
def test_full_config(ConfigComplete):
|
||||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []},
|
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}},
|
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': ['AT+SUPDATE']}}},
|
||||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
'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'},
|
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
'inverters': {'allow_all': True,
|
'batteries': {
|
||||||
'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'}},
|
'4100000000000001': {'modbus_polling': True, 'monitor_sn': 3000000000, 'node_id': 'Bat-Garage3/', 'sensor_list': 0x3026, 'suggested_area': 'Garage3', 'pv1': {'type': 'type5', 'manufacturer': 'man5'}, 'pv2': {'type': 'type6', 'manufacturer': 'man6'}}
|
||||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'sensor_list': 0x1511, 'suggested_area': ''}}}
|
},
|
||||||
|
'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'}},
|
||||||
|
'Y170000000000002': {'modbus_polling': False, 'monitor_sn': 2000000001, 'node_id': 'PV-Garage3/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage3', 'modbus_scanning': {'start': 2048, 'step': 1024, 'bytes': 16}}
|
||||||
|
}
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
validated = Config.conf_schema.validate(cnf)
|
validated = Config.conf_schema.validate(cnf)
|
||||||
except Exception:
|
except Exception:
|
||||||
assert False
|
assert False
|
||||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': 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}}}
|
assert validated == ConfigComplete
|
||||||
|
|
||||||
def test_mininum_config():
|
def test_read_empty(ConfigDefault):
|
||||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
test_buffer.rd = ""
|
||||||
'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'},
|
|
||||||
'inverters': {
|
|
||||||
'allow_all': False,
|
|
||||||
'R170000000000001': {
|
|
||||||
'suggested_area': '',
|
|
||||||
'modbus_polling': False,
|
|
||||||
'monitor_sn': 0,
|
|
||||||
'node_id': '',
|
|
||||||
'pv1': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-395M'},
|
|
||||||
'pv2': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-395M'},
|
|
||||||
'sensor_list': 688
|
|
||||||
},
|
|
||||||
'Y170000000000001': {
|
|
||||||
'modbus_polling': True,
|
|
||||||
'monitor_sn': 2000000000,
|
|
||||||
'suggested_area': '',
|
|
||||||
'node_id': '',
|
|
||||||
'pv1': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-410M'},
|
|
||||||
'pv2': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-410M'},
|
|
||||||
'pv3': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-410M'},
|
|
||||||
'pv4': {'manufacturer': 'Risen',
|
|
||||||
'type': 'RSM40-8-410M'},
|
|
||||||
'sensor_list': 688
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defcnf = TstConfig.def_config.get('solarman')
|
Config.init(ConfigReadToml("app/src/cnf/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 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():
|
def test_no_file():
|
||||||
cnf = {}
|
Config.init(ConfigReadToml("default_config.toml"))
|
||||||
TstConfig.set(cnf)
|
err = Config.get_error()
|
||||||
err = TstConfig.read('')
|
|
||||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||||
cnf = TstConfig.get()
|
cnf = Config.get()
|
||||||
assert cnf == {}
|
assert cnf == {}
|
||||||
defcnf = TstConfig.def_config.get('solarman')
|
defcnf = Config.def_config.get('solarman')
|
||||||
assert defcnf == None
|
assert defcnf == None
|
||||||
|
|
||||||
def test_read_cnf1():
|
def test_no_file2():
|
||||||
cnf = {'solarman' : {'enabled': False}}
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
TstConfig.set(cnf)
|
assert Config.err == None
|
||||||
err = TstConfig.read('app/config/')
|
ConfigReadToml("_no__file__no_")
|
||||||
|
err = Config.get_error()
|
||||||
assert err == None
|
assert err == None
|
||||||
cnf = TstConfig.get()
|
|
||||||
|
def test_invalid_filename():
|
||||||
|
Config.init(ConfigReadToml("app/src/cnf/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/src/cnf/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'},
|
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0,
|
||||||
|
'suggested_area': ''
|
||||||
|
}
|
||||||
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': False,
|
'allow_all': False,
|
||||||
'R170000000000001': {
|
'R170000000000001': {
|
||||||
@@ -161,7 +318,7 @@ def test_read_cnf1():
|
|||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'pv2': {'manufacturer': 'Risen',
|
'pv2': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
},
|
},
|
||||||
'Y170000000000001': {
|
'Y170000000000001': {
|
||||||
'modbus_polling': True,
|
'modbus_polling': True,
|
||||||
@@ -176,23 +333,40 @@ def test_read_cnf1():
|
|||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'pv4': {'manufacturer': 'Risen',
|
'pv4': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cnf = TstConfig.get('solarman')
|
cnf = Config.get('solarman')
|
||||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
defcnf = TstConfig.def_config.get('solarman')
|
defcnf = Config.def_config.get('solarman')
|
||||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
assert False == TstConfig.is_default('solarman')
|
assert False == Config.is_default('solarman')
|
||||||
|
|
||||||
def test_read_cnf2():
|
def test_read_cnf2():
|
||||||
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
test_buffer.rd = "solarman.enabled = 'FALSE'"
|
||||||
TstConfig.set(cnf)
|
|
||||||
err = TstConfig.read('app/config/')
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
|
for _ in patch_open():
|
||||||
|
ConfigReadToml("config/config.toml")
|
||||||
|
err = Config.get_error()
|
||||||
|
|
||||||
assert err == None
|
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'},
|
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0,
|
||||||
|
'suggested_area': ''
|
||||||
|
}
|
||||||
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': False,
|
'allow_all': False,
|
||||||
'R170000000000001': {
|
'R170000000000001': {
|
||||||
@@ -204,7 +378,7 @@ def test_read_cnf2():
|
|||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'pv2': {'manufacturer': 'Risen',
|
'pv2': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
},
|
},
|
||||||
'Y170000000000001': {
|
'Y170000000000001': {
|
||||||
'modbus_polling': True,
|
'modbus_polling': True,
|
||||||
@@ -219,27 +393,48 @@ def test_read_cnf2():
|
|||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'pv4': {'manufacturer': 'Risen',
|
'pv4': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert True == TstConfig.is_default('solarman')
|
assert True == Config.is_default('solarman')
|
||||||
|
|
||||||
def test_read_cnf3():
|
def test_read_cnf3(ConfigDefault):
|
||||||
cnf = {'solarman' : {'port': 'FALSE'}}
|
test_buffer.rd = "solarman.port = 'FALSE'"
|
||||||
TstConfig.set(cnf)
|
|
||||||
err = TstConfig.read('app/config/')
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
for _ in patch_open():
|
||||||
cnf = TstConfig.get()
|
ConfigReadToml("config/config.toml")
|
||||||
assert cnf == {'solarman': {'port': 'FALSE'}}
|
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():
|
def test_read_cnf4():
|
||||||
cnf = {'solarman' : {'port': 5000}}
|
test_buffer.rd = "solarman.port = 5000"
|
||||||
TstConfig.set(cnf)
|
|
||||||
err = TstConfig.read('app/config/')
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
|
for _ in patch_open():
|
||||||
|
ConfigReadToml("config/config.toml")
|
||||||
|
err = Config.get_error()
|
||||||
|
|
||||||
assert err == None
|
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'},
|
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'batteries': {
|
||||||
|
'4100000000000001': {
|
||||||
|
'modbus_polling': True,
|
||||||
|
'monitor_sn': 3000000000,
|
||||||
|
'node_id': '',
|
||||||
|
'pv1': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'pv2': {'manufacturer': 'Risen',
|
||||||
|
'type': 'RSM40-8-410M'},
|
||||||
|
'sensor_list': 0,
|
||||||
|
'suggested_area': ''
|
||||||
|
}
|
||||||
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': False,
|
'allow_all': False,
|
||||||
'R170000000000001': {
|
'R170000000000001': {
|
||||||
@@ -251,7 +446,7 @@ def test_read_cnf4():
|
|||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'pv2': {'manufacturer': 'Risen',
|
'pv2': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-395M'},
|
'type': 'RSM40-8-395M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
},
|
},
|
||||||
'Y170000000000001': {
|
'Y170000000000001': {
|
||||||
'modbus_polling': True,
|
'modbus_polling': True,
|
||||||
@@ -266,20 +461,26 @@ def test_read_cnf4():
|
|||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'pv4': {'manufacturer': 'Risen',
|
'pv4': {'manufacturer': 'Risen',
|
||||||
'type': 'RSM40-8-410M'},
|
'type': 'RSM40-8-410M'},
|
||||||
'sensor_list': 688
|
'sensor_list': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert False == TstConfig.is_default('solarman')
|
assert False == Config.is_default('solarman')
|
||||||
|
|
||||||
def test_read_cnf5():
|
def test_read_cnf5():
|
||||||
cnf = {'solarman' : {'port': 1023}}
|
test_buffer.rd = "solarman.port = 1023"
|
||||||
TstConfig.set(cnf)
|
|
||||||
err = TstConfig.read('app/config/')
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
|
for _ in patch_open():
|
||||||
|
ConfigReadToml("config/config.toml")
|
||||||
|
err = Config.get_error()
|
||||||
assert err != None
|
assert err != None
|
||||||
|
|
||||||
def test_read_cnf6():
|
def test_read_cnf6():
|
||||||
cnf = {'solarman' : {'port': 65536}}
|
test_buffer.rd = "solarman.port = 65536"
|
||||||
TstConfig.set(cnf)
|
|
||||||
err = TstConfig.read('app/config/')
|
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
|
||||||
|
for _ in patch_open():
|
||||||
|
ConfigReadToml("config/config.toml")
|
||||||
|
err = Config.get_error()
|
||||||
assert err != None
|
assert err != None
|
||||||
|
|||||||
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/src/cnf/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'}
|
||||||
434
app/tests/test_config_read_json.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# 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/src/cnf/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/src/cnf/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/src/cnf/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/src/cnf/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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serial": "Y170000000000002",
|
||||||
|
"monitor_sn": 2000000001,
|
||||||
|
"modbus_polling": false,
|
||||||
|
"modbus_scanning.start": 2048,
|
||||||
|
"node_id": "PV-Garage3",
|
||||||
|
"suggested_area": "Garage3",
|
||||||
|
"sensor_list": 688
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batteries": [
|
||||||
|
{
|
||||||
|
"serial": "4100000000000001",
|
||||||
|
"modbus_polling": true,
|
||||||
|
"monitor_sn": 3000000000,
|
||||||
|
"node_id": "Bat-Garage3",
|
||||||
|
"suggested_area": "Garage3",
|
||||||
|
"pv1.manufacturer": "man5",
|
||||||
|
"pv1.type": "type5",
|
||||||
|
"pv2.manufacturer": "man6",
|
||||||
|
"pv2.type": "type6",
|
||||||
|
"sensor_list": 12326
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tsun.enabled": true,
|
||||||
|
"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/src/cnf/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 pytest
|
||||||
import json, math
|
import json, math
|
||||||
import logging
|
import logging
|
||||||
from app.src.infos import Register, ClrAtMidnight
|
from infos import Register, ClrAtMidnight
|
||||||
from app.src.infos import Infos
|
from infos import Infos, Fmt
|
||||||
|
|
||||||
def test_statistic_counter():
|
def test_statistic_counter():
|
||||||
i = Infos()
|
i = Infos()
|
||||||
@@ -17,13 +17,13 @@ def test_statistic_counter():
|
|||||||
assert val == None or val == 0
|
assert val == None or val == 0
|
||||||
|
|
||||||
i.static_init() # initialize counter
|
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
|
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
i.inc_counter('Inverter_Cnt')
|
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)
|
val = i.dev_value(Register.INVERTER_CNT)
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
@@ -256,3 +256,24 @@ def test_key_obj():
|
|||||||
assert level == logging.DEBUG
|
assert level == logging.DEBUG
|
||||||
assert unit == 'kWh'
|
assert unit == 'kWh'
|
||||||
assert must_incr == True
|
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
|
# test_with_pytest.py
|
||||||
import pytest, json, math
|
import pytest, json, math
|
||||||
from app.src.infos import Register
|
from infos import Register
|
||||||
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
|
from gen3.infos_g3 import InfosG3, RegisterMap
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def contr_data_seq(): # Get Time Request message
|
def contr_data_seq(): # Get Time Request message
|
||||||
@@ -93,6 +93,86 @@ def contr2_data_seq(): # Get Time Request message
|
|||||||
msg += b'\x00\x00\x00'
|
msg += b'\x00\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def contr3_data_seq(): # Get Time Request message
|
||||||
|
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' # | ..^.....9..+.T.R
|
||||||
|
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x32\x2e\x30\x31\x2e\x31\x33\x00' # | SW_400_V2.01.13.
|
||||||
|
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' # | .'.T.Raymon../.T
|
||||||
|
msg += b'\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88' # | .RSW-1-10001..Z.
|
||||||
|
msg += b'\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f' # | T.t.raymoniot.co
|
||||||
|
msg += b'\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61' # | m..Z.T.logger.ta
|
||||||
|
msg += b'\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e' # | lent-monitoring.
|
||||||
|
msg += b'\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | com../.T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32\xe8\x54\x10\xff' # | ...........2.T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .6.T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff\xff\xff\xff\xff' # | .......:.T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3e\xa0\x54' # | .............>.T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J
|
||||||
|
msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i
|
||||||
|
msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................
|
||||||
|
msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I..
|
||||||
|
msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I
|
||||||
|
msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I.......
|
||||||
|
msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I.....
|
||||||
|
msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI...
|
||||||
|
msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I.
|
||||||
|
msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........
|
||||||
|
msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I......
|
||||||
|
msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I....
|
||||||
|
msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I..
|
||||||
|
msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L.........
|
||||||
|
msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI...
|
||||||
|
msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I.
|
||||||
|
msg += b'\x00\x00\xff'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_seq(): # Data indication from the controller
|
def inv_data_seq(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||||
@@ -140,6 +220,153 @@ def inv_data_seq2(): # Data indication from the controller
|
|||||||
msg += b'\x53\x00\x00'
|
msg += b'\x53\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inv_data_seq3(): # Inverter indication from MS-2000
|
||||||
|
|
||||||
|
msg = b'\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
|
||||||
|
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
|
||||||
|
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
|
||||||
|
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
|
||||||
|
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
|
||||||
|
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
|
||||||
|
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
|
||||||
|
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
|
||||||
|
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
|
||||||
|
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
|
||||||
|
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
|
||||||
|
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
|
||||||
|
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
|
||||||
|
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
|
||||||
|
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
|
||||||
|
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
|
||||||
|
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
|
||||||
|
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
|
||||||
|
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
|
||||||
|
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
|
||||||
|
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
|
||||||
|
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
|
||||||
|
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
|
||||||
|
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
|
||||||
|
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
|
||||||
|
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
|
||||||
|
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
|
||||||
|
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
|
||||||
|
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
|
||||||
|
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
|
||||||
|
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
|
||||||
|
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
|
||||||
|
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
|
||||||
|
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
|
||||||
|
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
|
||||||
|
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
|
||||||
|
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
|
||||||
|
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
|
||||||
|
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
|
||||||
|
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
|
||||||
|
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
|
||||||
|
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
|
||||||
|
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
|
||||||
|
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
|
||||||
|
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
|
||||||
|
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
|
||||||
|
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
|
||||||
|
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
|
||||||
|
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
|
||||||
|
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
|
||||||
|
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
|
||||||
|
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
|
||||||
|
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
|
||||||
|
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
|
||||||
|
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
|
||||||
|
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
|
||||||
|
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
|
||||||
|
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
|
||||||
|
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
|
||||||
|
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
|
||||||
|
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
|
||||||
|
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
|
||||||
|
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
|
||||||
|
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
|
||||||
|
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
|
||||||
|
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
|
||||||
|
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
|
||||||
|
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
|
||||||
|
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
|
||||||
|
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
|
||||||
|
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
|
||||||
|
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
|
||||||
|
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
|
||||||
|
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
|
||||||
|
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
|
||||||
|
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
|
||||||
|
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
|
||||||
|
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
|
||||||
|
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
|
||||||
|
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
|
||||||
|
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
|
||||||
|
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
|
||||||
|
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
|
||||||
|
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
|
||||||
|
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
|
||||||
|
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
|
||||||
|
msg += b'\x53\x00\x66' # | S.f'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_new(): # Data indication from DSP V5.0.17
|
def inv_data_new(): # Data indication from DSP V5.0.17
|
||||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
||||||
@@ -254,7 +481,7 @@ def inv_data_seq2_zero(): # Data indication from the controller
|
|||||||
|
|
||||||
def test_parse_control(contr_data_seq):
|
def test_parse_control(contr_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
@@ -262,15 +489,23 @@ def test_parse_control(contr_data_seq):
|
|||||||
|
|
||||||
def test_parse_control2(contr2_data_seq):
|
def test_parse_control2(contr2_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr2_data_seq):
|
for key, result in i.parse (contr2_data_seq, sensor=0x0e100000):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
|
def test_parse_control3(contr3_data_seq):
|
||||||
|
i = InfosG3()
|
||||||
|
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
|
||||||
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
|
assert json.dumps(i.db) == json.dumps(
|
||||||
|
{"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
def test_parse_inverter(inv_data_seq):
|
def test_parse_inverter(inv_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
@@ -278,10 +513,10 @@ def test_parse_inverter(inv_data_seq):
|
|||||||
|
|
||||||
def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
@@ -289,10 +524,29 @@ def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
|||||||
"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300},
|
"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300},
|
||||||
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||||
|
|
||||||
|
def test_parse_cont_and_invert2(contr3_data_seq, inv_data_seq3):
|
||||||
|
i = InfosG3()
|
||||||
|
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
|
||||||
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
|
for key, result in i.parse (inv_data_seq3, sensor=0x01900000):
|
||||||
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
|
assert json.dumps(i.db) == json.dumps(
|
||||||
|
{
|
||||||
|
"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300},
|
||||||
|
"env": {"Inverter_Status": 0},
|
||||||
|
"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0},
|
||||||
|
"input": {"iVal_1": 1, "Val_0": 655.28, "Val_1": 327.4, "Val_2": 0.0, "Val_3": 0.0, "iVal_2": 3, "iVal_3": 12, "iVal_4": 80, "Val_4": 327.4, "iVal_5": 0, "Val_10": 30.25, "Val_11": 1.35, "iVal_6": 12, "pv1": {"Voltage": 78.6, "Current": 0.0, "Power": 0.0}, "Val_5": 110.0, "pv2": {"Voltage": 58.1, "Current": 0.0, "Power": 0.0}, "Val_6": 110.0, "pv3": {"Voltage": 58.8, "Current": 0.0, "Power": 0.0}, "Val_7": 110.0, "Val_14": 0.01, "Val_15": 0.0, "Val_16": 33.0, "Val_17": 0.0, "Val_18": 0.0, "iVal_8": 2, "pv4": {"Voltage": 17.4, "Current": 0.02, "Power": 0.3}, "Val_8": 4.8, "iVal_10": 1, "pv5": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv6": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "Val_24": 0.0, "Val_25": 0.0, "Val_26": 32.99, "Val_27": 0.0, "Val_28": 0.0, "iVal_11": 2, "iVal_12": 3},
|
||||||
|
"grid": {"Voltage": 238.5, "Current": 0.05, "Frequency": 50.0, "Output_Power": 0.0},
|
||||||
|
"inverter": {"Max_Designed_Power": 3000},
|
||||||
|
"total": {"Total_Generation": 29.86}
|
||||||
|
})
|
||||||
|
|
||||||
def test_build_ha_conf1(contr_data_seq):
|
def test_build_ha_conf1(contr_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||||
@@ -328,6 +582,7 @@ def test_build_ha_conf1(contr_data_seq):
|
|||||||
def test_build_ha_conf2(contr_data_seq):
|
def test_build_ha_conf2(contr_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
@@ -352,11 +607,12 @@ def test_build_ha_conf2(contr_data_seq):
|
|||||||
|
|
||||||
def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
|
||||||
|
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
for key, result in i.parse (inv_data_seq2):
|
for key, result in i.parse (inv_data_seq2, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
@@ -390,9 +646,10 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
|||||||
|
|
||||||
def test_build_ha_conf4(contr_data_seq, inv_data_seq):
|
def test_build_ha_conf4(contr_data_seq, inv_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
|
||||||
|
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
|
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
|
||||||
|
|
||||||
@@ -414,19 +671,46 @@ def test_build_ha_conf4(contr_data_seq, inv_data_seq):
|
|||||||
tests +=1
|
tests +=1
|
||||||
assert tests==1
|
assert tests==1
|
||||||
|
|
||||||
|
def test_build_ha_conf5(contr3_data_seq, inv_data_seq3):
|
||||||
|
i = InfosG3()
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "01900000")
|
||||||
|
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
|
||||||
|
pass # side effect in calling i.parse()
|
||||||
|
for key, result in i.parse (inv_data_seq3, sensor=0x01900000):
|
||||||
|
pass # side effect in calling i.parse()
|
||||||
|
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
|
||||||
|
|
||||||
|
tests = 0
|
||||||
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||||
|
if id == 'signal_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:56"]]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
|
tests +=1
|
||||||
|
assert tests==1
|
||||||
|
|
||||||
|
i.set_db_def_value(Register.MAC_ADDR, "00:a0:57:12:34:57")
|
||||||
|
|
||||||
|
tests = 0
|
||||||
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||||
|
if id == 'signal_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:57"]]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
|
tests +=1
|
||||||
|
assert tests==1
|
||||||
|
|
||||||
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
|
||||||
if key == 'total' or key == 'inverter' or key == 'env':
|
if key == 'total' or key == 'inverter' or key == 'env':
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
assert tests==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['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total' or key == 'env':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -435,10 +719,10 @@ 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['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "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
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -453,7 +737,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -467,7 +751,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total' or key == 'env':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -478,7 +762,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total' or key == 'env':
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
@@ -489,7 +773,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
def test_new_data_types(inv_data_new):
|
def test_new_data_types(inv_data_new):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_new):
|
for key, update in i.parse (inv_data_new, sensor=0x01900001):
|
||||||
if key == 'events':
|
if key == 'events':
|
||||||
tests +=1
|
tests +=1
|
||||||
elif key == 'inverter':
|
elif key == 'inverter':
|
||||||
@@ -501,10 +785,10 @@ def test_new_data_types(inv_data_new):
|
|||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
assert tests==15
|
assert tests==7
|
||||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
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['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):
|
def test_invalid_data_type(invalid_data_seq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
@@ -514,21 +798,9 @@ def test_invalid_data_type(invalid_data_seq):
|
|||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
|
|
||||||
for key, result in i.parse (invalid_data_seq):
|
for key, result in i.parse (invalid_data_seq, sensor=0x01900001):
|
||||||
pass # side effect in calling i.parse()
|
pass # side effect in calling i.parse()
|
||||||
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
||||||
|
|
||||||
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
def test_result_eval(inv_data_seq2: bytes):
|
|
||||||
|
|
||||||
# add eval to convert temperature from °F to °C
|
|
||||||
RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
|
|
||||||
|
|
||||||
i = InfosG3()
|
|
||||||
|
|
||||||
for _, _ in i.parse (inv_data_seq2):
|
|
||||||
pass # side effect is calling generator i.parse()
|
|
||||||
assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
|
||||||
del RegisterMap.map[0x00000514]['eval'] # remove eval
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest, json, math, random
|
import pytest, json, math, random
|
||||||
from app.src.infos import Register
|
from infos import Register
|
||||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
from gen3plus.infos_g3p import InfosG3P
|
||||||
from app.src.gen3plus.infos_g3p import RegisterMap
|
from gen3plus.infos_g3p import RegisterMap
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def str_test_ip():
|
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'\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'\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\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\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'\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'
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
@@ -69,6 +70,28 @@ def inverter_data(): # 0x4210 ftype: 0x01
|
|||||||
msg += b'\x00\x00\x00\x00'
|
msg += b'\x00\x00\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def batterie_data(): # 0x4210 ftype: 0x01
|
||||||
|
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x26\x30\xc7\xde'
|
||||||
|
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
|
||||||
|
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
|
||||||
|
msg += b'\x0d\x3a\x00\x70\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
|
||||||
|
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def batterie_data2(): # 0x4210 ftype: 0x01
|
||||||
|
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x26\x30\xc7\xde'
|
||||||
|
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
|
||||||
|
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
|
||||||
|
msg += b'\x0d\x3a\x00\x70\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
|
||||||
|
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e'
|
||||||
|
return msg
|
||||||
|
|
||||||
def test_default_db():
|
def test_default_db():
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
@@ -85,32 +108,101 @@ def test_parse_4110(str_test_ip, device_data: bytes):
|
|||||||
pass # side effect is calling generator i.parse()
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
|
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0", "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"},
|
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_parse_4210(inverter_data: bytes):
|
def test_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_02b0(inverter_data: bytes):
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
i.db.clear()
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
|
||||||
pass # side effect is calling generator i.parse()
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
||||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
"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, "Inverter_Temp": 14},
|
"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},
|
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||||
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
||||||
"pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91},
|
"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},
|
"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}},
|
"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_parse_4210_3026(batterie_data: bytes):
|
||||||
|
i = InfosG3P(client_mode=False)
|
||||||
|
i.db.clear()
|
||||||
|
|
||||||
|
for key, update in i.parse (batterie_data, 0x42, 1, 0x3026):
|
||||||
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
|
assert json.dumps(i.db) == json.dumps({
|
||||||
|
"controller": {"Sensor_List": "3026", "Power_On_Time": 4684},
|
||||||
|
"inverter": {"Serial_Number": "4101240701490314"},
|
||||||
|
"batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12},
|
||||||
|
"pv2": {"Voltage": 33.72, "Current": 0.0},
|
||||||
|
"Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0,
|
||||||
|
"Voltage": 51.34, "Current": -0.02, "SOC": 10.0,
|
||||||
|
"Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21},
|
||||||
|
"Temp_1": 15, "Temp_2": 15, "Temp_3": 15,
|
||||||
|
"out": {"Voltage": 0.14, "Current": 0.0, "Out_Status": 0, "Power": 0.0},
|
||||||
|
"Controller_Temp": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513,
|
||||||
|
"PV_Power": 37.9232, "Power": -1.0268000000000002},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_parse_4210_3026_incomplete(batterie_data2: bytes):
|
||||||
|
i = InfosG3P(client_mode=False)
|
||||||
|
i.db.clear()
|
||||||
|
|
||||||
|
for key, update in i.parse (batterie_data2, 0x42, 1, 0x3026):
|
||||||
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
|
assert json.dumps(i.db) == json.dumps({
|
||||||
|
"controller": {"Sensor_List": "3026", "Power_On_Time": 4684},
|
||||||
|
"inverter": {"Serial_Number": "4101240701490314"},
|
||||||
|
"batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12},
|
||||||
|
"pv2": {"Voltage": 33.72, "Current": 0.0},
|
||||||
|
"Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0,
|
||||||
|
"Voltage": 51.34, "Current": -0.02, "SOC": 10.0,
|
||||||
|
"Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21},
|
||||||
|
"Temp_1": 15, "Temp_2": 15, "Temp_3": 15,
|
||||||
|
"out": {"Voltage": 0.14, "Current": None, "Out_Status": None, "Power": None},
|
||||||
|
"Controller_Temp": None, "Reg_74": None, "Reg_76": None, "Reg_78": None,
|
||||||
|
"PV_Power": 37.9232, "Power": -1.0268000000000002},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_build_4210(inverter_data: bytes):
|
||||||
|
i = InfosG3P(client_mode=False)
|
||||||
|
i.db.clear()
|
||||||
|
|
||||||
|
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
|
||||||
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
|
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
|
||||||
|
for i in range(11, 31):
|
||||||
|
build_msg[i] = inverter_data[i]
|
||||||
|
assert inverter_data == build_msg
|
||||||
|
|
||||||
def test_build_ha_conf1():
|
def test_build_ha_conf1():
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "02b0")
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||||
@@ -185,6 +277,7 @@ def test_build_ha_conf2():
|
|||||||
def test_build_ha_conf3():
|
def test_build_ha_conf3():
|
||||||
i = InfosG3P(client_mode=True)
|
i = InfosG3P(client_mode=True)
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "02b0")
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||||
@@ -256,27 +349,89 @@ def test_build_ha_conf4():
|
|||||||
|
|
||||||
assert tests==1
|
assert tests==1
|
||||||
|
|
||||||
def test_exception_and_eval(inverter_data: bytes):
|
def test_build_ha_conf5():
|
||||||
|
i = InfosG3P(client_mode=True)
|
||||||
|
i.static_init() # initialize counter
|
||||||
|
i.set_db_def_value(Register.SENSOR_LIST, "3026")
|
||||||
|
|
||||||
# add eval to convert temperature from °F to °C
|
tests = 0
|
||||||
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||||
|
|
||||||
|
if id == 'out_power_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({"name": "Output Power", "stat_t": "tsun/garagendach/batterie", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{ (value_json['out']['Power'] | int)}}", "unit_of_meas": "W", "dev": {"name": "Batterie", "sa": "Batterie", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["batterie_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
|
tests +=1
|
||||||
|
elif id == 'daily_gen_123':
|
||||||
|
assert False
|
||||||
|
elif id == 'volt_pv1_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv1_123", "val_tpl": "{{ (value_json['pv1']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "batterie_123", "ids": ["bat_inp_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
|
tests +=1
|
||||||
|
elif id == 'volt_pv2_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv2_123", "val_tpl": "{{ (value_json['pv2']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "batterie_123", "ids": ["bat_inp_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
|
tests +=1
|
||||||
|
elif id == 'signal_123':
|
||||||
|
assert comp == 'sensor'
|
||||||
|
assert d_json == json.dumps({})
|
||||||
|
tests +=1
|
||||||
|
elif id == 'inv_count_456':
|
||||||
|
assert False
|
||||||
|
else:
|
||||||
|
print(id)
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
|
||||||
|
def test_exception_and_calc(inverter_data: bytes):
|
||||||
|
|
||||||
|
# patch table to convert temperature from °F to °C
|
||||||
|
ofs = RegisterMap.map_02b0[0x420100d8]['offset']
|
||||||
|
RegisterMap.map_02b0[0x420100d8]['quotient'] = 1.8
|
||||||
|
RegisterMap.map_02b0[0x420100d8]['offset'] = -32/1.8
|
||||||
# map PV1_VOLTAGE to invalid register
|
# map PV1_VOLTAGE to invalid register
|
||||||
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
|
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.TEST_REG2
|
||||||
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
||||||
backup = RegisterMap.map[0x420100de]
|
backup = RegisterMap.map_02b0[0x420100de]
|
||||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
RegisterMap.map_02b0[0x420100de] = 'invalid_entry'
|
||||||
|
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
# i.db.clear()
|
i.db.clear()
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
|
||||||
pass # side effect is calling generator i.parse()
|
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)
|
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
|
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
|
||||||
|
assert build_msg[32:0xde] == inverter_data[32:0xde]
|
||||||
|
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
||||||
|
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||||
|
|
||||||
|
|
||||||
|
# remove a table entry and test parsing and building
|
||||||
|
del RegisterMap.map_02b0[0x420100d8]['quotient']
|
||||||
|
del RegisterMap.map_02b0[0x420100d8]['offset']
|
||||||
|
|
||||||
|
i.db.clear()
|
||||||
|
|
||||||
|
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
|
||||||
pass # side effect is calling generator i.parse()
|
pass # side effect is calling generator i.parse()
|
||||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||||
|
|
||||||
|
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
|
||||||
|
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
|
||||||
|
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
||||||
|
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||||
|
|
||||||
|
# test restore table
|
||||||
|
RegisterMap.map_02b0[0x420100d8]['offset'] = ofs
|
||||||
|
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||||
|
RegisterMap.map_02b0[0x420100de] = backup # reset mapping
|
||||||
|
|
||||||
|
# test orginial table
|
||||||
|
i.db.clear()
|
||||||
|
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
|
||||||
|
pass # side effect is calling generator i.parse()
|
||||||
|
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||||
|
|
||||||
|
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
|
||||||
|
assert build_msg[32:-1] == inverter_data[32:-1]
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import gc
|
|||||||
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from app.src.infos import Infos
|
from infos import Infos
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.gen3.talent import Talent
|
from gen3.talent import Talent
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.async_stream import AsyncStream, AsyncStreamClient
|
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',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
@@ -54,11 +54,12 @@ class FakeReader():
|
|||||||
|
|
||||||
|
|
||||||
class FakeWriter():
|
class FakeWriter():
|
||||||
|
peer = ('47.1.2.3', 10000)
|
||||||
def write(self, buf: bytes):
|
def write(self, buf: bytes):
|
||||||
return
|
return
|
||||||
def get_extra_info(self, sel: str):
|
def get_extra_info(self, sel: str):
|
||||||
if sel == 'peername':
|
if sel == 'peername':
|
||||||
return 'remote.intern'
|
return self.peer
|
||||||
elif sel == 'sockname':
|
elif sel == 'sockname':
|
||||||
return 'sock:1234'
|
return 'sock:1234'
|
||||||
assert False
|
assert False
|
||||||
@@ -69,13 +70,13 @@ class FakeWriter():
|
|||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
class TestType(Enum):
|
class MockType(Enum):
|
||||||
RD_TEST_0_BYTES = 1
|
RD_TEST_0_BYTES = 1
|
||||||
RD_TEST_TIMEOUT = 2
|
RD_TEST_TIMEOUT = 2
|
||||||
RD_TEST_EXCEPT = 3
|
RD_TEST_EXCEPT = 3
|
||||||
|
|
||||||
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
test = MockType.RD_TEST_0_BYTES
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patch_open_connection():
|
def patch_open_connection():
|
||||||
@@ -85,9 +86,9 @@ def patch_open_connection():
|
|||||||
|
|
||||||
def new_open(host: str, port: int):
|
def new_open(host: str, port: int):
|
||||||
global test
|
global test
|
||||||
if test == TestType.RD_TEST_TIMEOUT:
|
if test == MockType.RD_TEST_TIMEOUT:
|
||||||
raise ConnectionRefusedError
|
raise ConnectionRefusedError
|
||||||
elif test == TestType.RD_TEST_EXCEPT:
|
elif test == MockType.RD_TEST_EXCEPT:
|
||||||
raise ValueError("Value cannot be negative") # Compliant
|
raise ValueError("Value cannot be negative") # Compliant
|
||||||
return new_conn(None)
|
return new_conn(None)
|
||||||
|
|
||||||
@@ -241,6 +242,118 @@ async def test_remote_conn(config_conn, patch_open_connection):
|
|||||||
cnt += 1
|
cnt += 1
|
||||||
assert cnt == 0
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
|
async def test_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
|
||||||
_ = config_conn
|
_ = config_conn
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import sys,gc
|
|||||||
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from app.src.infos import Infos
|
from infos import Infos
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.proxy import Proxy
|
from proxy import Proxy
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.gen3.inverter_g3 import InverterG3
|
from gen3.inverter_g3 import InverterG3
|
||||||
from app.src.async_stream import AsyncStream
|
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',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class FakeWriter():
|
|||||||
return
|
return
|
||||||
def get_extra_info(self, sel: str):
|
def get_extra_info(self, sel: str):
|
||||||
if sel == 'peername':
|
if sel == 'peername':
|
||||||
return 'remote.intern'
|
return ('47.1.2.3', 10000)
|
||||||
elif sel == 'sockname':
|
elif sel == 'sockname':
|
||||||
return 'sock:1234'
|
return 'sock:1234'
|
||||||
assert False
|
assert False
|
||||||
@@ -70,13 +70,13 @@ class FakeWriter():
|
|||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
class TestType(Enum):
|
class MockType(Enum):
|
||||||
RD_TEST_0_BYTES = 1
|
RD_TEST_0_BYTES = 1
|
||||||
RD_TEST_TIMEOUT = 2
|
RD_TEST_TIMEOUT = 2
|
||||||
RD_TEST_EXCEPT = 3
|
RD_TEST_EXCEPT = 3
|
||||||
|
|
||||||
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
test = MockType.RD_TEST_0_BYTES
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patch_open_connection():
|
def patch_open_connection():
|
||||||
@@ -86,9 +86,9 @@ def patch_open_connection():
|
|||||||
|
|
||||||
def new_open(host: str, port: int):
|
def new_open(host: str, port: int):
|
||||||
global test
|
global test
|
||||||
if test == TestType.RD_TEST_TIMEOUT:
|
if test == MockType.RD_TEST_TIMEOUT:
|
||||||
raise ConnectionRefusedError
|
raise ConnectionRefusedError
|
||||||
elif test == TestType.RD_TEST_EXCEPT:
|
elif test == MockType.RD_TEST_EXCEPT:
|
||||||
raise ValueError("Value cannot be negative") # Compliant
|
raise ValueError("Value cannot be negative") # Compliant
|
||||||
return new_conn(None)
|
return new_conn(None)
|
||||||
|
|
||||||
@@ -144,14 +144,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
|||||||
assert asyncio.get_running_loop()
|
assert asyncio.get_running_loop()
|
||||||
|
|
||||||
global test
|
global test
|
||||||
test = TestType.RD_TEST_TIMEOUT
|
test = MockType.RD_TEST_TIMEOUT
|
||||||
|
|
||||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||||
await inverter.create_remote()
|
await inverter.create_remote()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
assert inverter.remote.stream==None
|
assert inverter.remote.stream==None
|
||||||
|
|
||||||
test = TestType.RD_TEST_EXCEPT
|
test = MockType.RD_TEST_EXCEPT
|
||||||
await inverter.create_remote()
|
await inverter.create_remote()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
assert inverter.remote.stream==None
|
assert inverter.remote.stream==None
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import asyncio
|
|||||||
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from app.src.infos import Infos
|
from infos import Infos
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.proxy import Proxy
|
from proxy import Proxy
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
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',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
@@ -58,7 +58,7 @@ class FakeWriter():
|
|||||||
return
|
return
|
||||||
def get_extra_info(self, sel: str):
|
def get_extra_info(self, sel: str):
|
||||||
if sel == 'peername':
|
if sel == 'peername':
|
||||||
return 'remote.intern'
|
return ('47.1.2.3', 10000)
|
||||||
elif sel == 'sockname':
|
elif sel == 'sockname':
|
||||||
return 'sock:1234'
|
return 'sock:1234'
|
||||||
assert False
|
assert False
|
||||||
@@ -69,13 +69,13 @@ class FakeWriter():
|
|||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
class TestType(Enum):
|
class MockType(Enum):
|
||||||
RD_TEST_0_BYTES = 1
|
RD_TEST_0_BYTES = 1
|
||||||
RD_TEST_TIMEOUT = 2
|
RD_TEST_TIMEOUT = 2
|
||||||
RD_TEST_EXCEPT = 3
|
RD_TEST_EXCEPT = 3
|
||||||
|
|
||||||
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
test = MockType.RD_TEST_0_BYTES
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def patch_open_connection():
|
def patch_open_connection():
|
||||||
@@ -85,16 +85,17 @@ def patch_open_connection():
|
|||||||
|
|
||||||
def new_open(host: str, port: int):
|
def new_open(host: str, port: int):
|
||||||
global test
|
global test
|
||||||
if test == TestType.RD_TEST_TIMEOUT:
|
if test == MockType.RD_TEST_TIMEOUT:
|
||||||
raise ConnectionRefusedError
|
raise ConnectionRefusedError
|
||||||
elif test == TestType.RD_TEST_EXCEPT:
|
elif test == MockType.RD_TEST_EXCEPT:
|
||||||
raise ValueError("Value cannot be negative") # Compliant
|
raise ValueError("Value cannot be negative") # Compliant
|
||||||
return new_conn(None)
|
return new_conn(None)
|
||||||
|
|
||||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
def test_method_calls():
|
def test_method_calls(config_conn):
|
||||||
|
_ = config_conn
|
||||||
reader = FakeReader()
|
reader = FakeReader()
|
||||||
writer = FakeWriter()
|
writer = FakeWriter()
|
||||||
InverterBase._registry.clear()
|
InverterBase._registry.clear()
|
||||||
@@ -121,14 +122,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
|||||||
assert asyncio.get_running_loop()
|
assert asyncio.get_running_loop()
|
||||||
|
|
||||||
global test
|
global test
|
||||||
test = TestType.RD_TEST_TIMEOUT
|
test = MockType.RD_TEST_TIMEOUT
|
||||||
|
|
||||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||||
await inverter.create_remote()
|
await inverter.create_remote()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
assert inverter.remote.stream==None
|
assert inverter.remote.stream==None
|
||||||
|
|
||||||
test = TestType.RD_TEST_EXCEPT
|
test = MockType.RD_TEST_EXCEPT
|
||||||
await inverter.create_remote()
|
await inverter.create_remote()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
assert inverter.remote.stream==None
|
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:
|
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||||
stream = inverter.local.stream
|
stream = inverter.local.stream
|
||||||
await inverter.async_publ_mqtt() # check call with invalid unique_id
|
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.new_data['inverter'] = True
|
||||||
stream.db.db['inverter'] = {}
|
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:
|
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||||
stream = inverter.local.stream
|
stream = inverter.local.stream
|
||||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
stream._set_serial_no(snr= 123344)
|
||||||
stream.new_data['inverter'] = True
|
stream.new_data['inverter'] = True
|
||||||
stream.db.db['inverter'] = {}
|
stream.db.db['inverter'] = {}
|
||||||
await inverter.async_publ_mqtt()
|
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:
|
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||||
stream = inverter.local.stream
|
stream = inverter.local.stream
|
||||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
stream._set_serial_no(snr= 123344)
|
||||||
|
|
||||||
stream.new_data['inverter'] = True
|
stream.new_data['inverter'] = True
|
||||||
stream.db.db['inverter'] = {}
|
stream.db.db['inverter'] = {}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
from app.src.modbus import Modbus
|
from modbus import Modbus
|
||||||
from app.src.infos import Infos, Register
|
from infos import Infos, Register
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
@@ -77,9 +77,10 @@ def test_recv_resp_crc_err():
|
|||||||
mb.last_fcode = 3
|
mb.last_fcode = 3
|
||||||
mb.last_reg = 0x300e
|
mb.last_reg = 0x300e
|
||||||
mb.last_len = 2
|
mb.last_len = 2
|
||||||
|
mb.set_node_id('test')
|
||||||
# check matching response, but with CRC error
|
# check matching response, but with CRC error
|
||||||
call = 0
|
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
|
call += 1
|
||||||
assert mb.err == 1
|
assert mb.err == 1
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
@@ -97,10 +98,11 @@ def test_recv_resp_invalid_addr():
|
|||||||
mb.last_fcode = 3
|
mb.last_fcode = 3
|
||||||
mb.last_reg = 0x300e
|
mb.last_reg = 0x300e
|
||||||
mb.last_len = 2
|
mb.last_len = 2
|
||||||
|
mb.set_node_id('test')
|
||||||
|
|
||||||
# check not matching response, with wrong server addr
|
# check not matching response, with wrong server addr
|
||||||
call = 0
|
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
|
call += 1
|
||||||
assert mb.err == 2
|
assert mb.err == 2
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
@@ -120,7 +122,8 @@ def test_recv_recv_fcode():
|
|||||||
|
|
||||||
# check not matching response, with wrong function code
|
# check not matching response, with wrong function code
|
||||||
call = 0
|
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
|
call += 1
|
||||||
|
|
||||||
assert mb.err == 3
|
assert mb.err == 3
|
||||||
@@ -142,7 +145,8 @@ def test_recv_resp_len():
|
|||||||
|
|
||||||
# check not matching response, with wrong data length
|
# check not matching response, with wrong data length
|
||||||
call = 0
|
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
|
call += 1
|
||||||
|
|
||||||
assert mb.err == 4
|
assert mb.err == 4
|
||||||
@@ -161,7 +165,8 @@ def test_recv_unexpect_resp():
|
|||||||
|
|
||||||
# check unexpected response, which must be dropped
|
# check unexpected response, which must be dropped
|
||||||
call = 0
|
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
|
call += 1
|
||||||
|
|
||||||
assert mb.err == 5
|
assert mb.err == 5
|
||||||
@@ -177,8 +182,9 @@ def test_parse_resp():
|
|||||||
assert mb.req_pend
|
assert mb.req_pend
|
||||||
|
|
||||||
call = 0
|
call = 0
|
||||||
|
mb.set_node_id('test')
|
||||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
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':
|
if key == 'grid':
|
||||||
assert update == True
|
assert update == True
|
||||||
elif key == 'inverter':
|
elif key == 'inverter':
|
||||||
@@ -226,8 +232,9 @@ def test_queue2():
|
|||||||
assert mb.send_calls == 1
|
assert mb.send_calls == 1
|
||||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
call = 0
|
call = 0
|
||||||
|
mb.set_node_id('test')
|
||||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
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':
|
if key == 'grid':
|
||||||
assert update == True
|
assert update == True
|
||||||
elif key == 'inverter':
|
elif key == 'inverter':
|
||||||
@@ -245,14 +252,14 @@ def test_queue2():
|
|||||||
assert mb.send_calls == 2
|
assert mb.send_calls == 2
|
||||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
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()
|
pass # call generator mb.recv_resp()
|
||||||
|
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
assert mb.send_calls == 3
|
assert mb.send_calls == 3
|
||||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
call = 0
|
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
|
call += 1
|
||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert 5 == call
|
assert 5 == call
|
||||||
@@ -276,8 +283,9 @@ def test_queue3():
|
|||||||
assert mb.recv_responses == 0
|
assert mb.recv_responses == 0
|
||||||
|
|
||||||
call = 0
|
call = 0
|
||||||
|
mb.set_node_id('test')
|
||||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
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':
|
if key == 'grid':
|
||||||
assert update == True
|
assert update == True
|
||||||
elif key == 'inverter':
|
elif key == 'inverter':
|
||||||
@@ -296,7 +304,7 @@ def test_queue3():
|
|||||||
assert mb.send_calls == 2
|
assert mb.send_calls == 2
|
||||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
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
|
pass # no code in loop is OK; calling the generator is the purpose
|
||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert mb.recv_responses == 2
|
assert mb.recv_responses == 2
|
||||||
@@ -305,7 +313,7 @@ def test_queue3():
|
|||||||
assert mb.send_calls == 3
|
assert mb.send_calls == 3
|
||||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
call = 0
|
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
|
call += 1
|
||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert mb.recv_responses == 2
|
assert mb.recv_responses == 2
|
||||||
@@ -373,7 +381,8 @@ def test_recv_unknown_data():
|
|||||||
|
|
||||||
# check matching response, but with CRC error
|
# check matching response, but with CRC error
|
||||||
call = 0
|
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
|
call += 1
|
||||||
assert mb.err == 0
|
assert mb.err == 0
|
||||||
assert 0 == call
|
assert 0 == call
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ from aiomqtt import MqttCodeError
|
|||||||
|
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.infos import Infos
|
from infos import Infos
|
||||||
from app.src.mqtt import Mqtt
|
from mqtt import Mqtt
|
||||||
from app.src.inverter_base import InverterBase
|
from inverter_base import InverterBase
|
||||||
from app.src.messages import Message, State
|
from messages import Message, State
|
||||||
from app.src.proxy import Proxy
|
from proxy import Proxy
|
||||||
from app.src.modbus_tcp import ModbusConn, ModbusTcp
|
from modbus_tcp import ModbusConn, ModbusTcp
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
@@ -52,6 +52,10 @@ def config_conn(test_hostname, test_port):
|
|||||||
'proxy_node_id': 'test_1',
|
'proxy_node_id': 'test_1',
|
||||||
'proxy_unique_id': ''
|
'proxy_unique_id': ''
|
||||||
},
|
},
|
||||||
|
'solarman':{
|
||||||
|
'host': 'access1.solarmanpv.com',
|
||||||
|
'port': 10000
|
||||||
|
},
|
||||||
'inverters':{
|
'inverters':{
|
||||||
'allow_all': True,
|
'allow_all': True,
|
||||||
"R170000000000001":{
|
"R170000000000001":{
|
||||||
@@ -65,7 +69,8 @@ def config_conn(test_hostname, test_port):
|
|||||||
'sensor_list': 0x2b0,
|
'sensor_list': 0x2b0,
|
||||||
'client_mode':{
|
'client_mode':{
|
||||||
'host': '192.168.0.1',
|
'host': '192.168.0.1',
|
||||||
'port': 8899
|
'port': 8899,
|
||||||
|
'forward': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import aiomqtt
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mock import patch, Mock
|
from mock import patch, Mock
|
||||||
from app.src.async_stream import AsyncIfcImpl
|
from async_stream import AsyncIfcImpl
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.mqtt import Mqtt
|
from mqtt import Mqtt
|
||||||
from app.src.modbus import Modbus
|
from modbus import Modbus
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
|
|
||||||
|
NO_MOSQUITTO_TEST = False
|
||||||
|
'''disable all tests with connections to test.mosquitto.org'''
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
@@ -69,23 +71,79 @@ def spy_modbus_cmd_client():
|
|||||||
|
|
||||||
def test_native_client(test_hostname, test_port):
|
def test_native_client(test_hostname, test_port):
|
||||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
"""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 paho.mqtt.client as mqtt
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
c = mqtt.Client()
|
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||||
c.loop_start()
|
c.loop_start()
|
||||||
try:
|
try:
|
||||||
# Just make sure the client connects successfully
|
# Just make sure the client connects successfully
|
||||||
on_connect = threading.Event()
|
on_connect = threading.Event()
|
||||||
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
||||||
c.connect_async(test_hostname, test_port)
|
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:
|
finally:
|
||||||
c.loop_stop()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_mqtt_no_config(config_no_conn):
|
async def test_mqtt_no_config(config_no_conn):
|
||||||
_ = config_no_conn
|
_ = config_no_conn
|
||||||
@@ -110,29 +168,6 @@ async def test_mqtt_no_config(config_no_conn):
|
|||||||
finally:
|
finally:
|
||||||
await m.close()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||||
_ = config_mqtt_conn
|
_ = config_mqtt_conn
|
||||||
@@ -209,26 +244,6 @@ async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
|||||||
finally:
|
finally:
|
||||||
await m.close()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_ignore_unknown_func(config_mqtt_conn):
|
async def test_ignore_unknown_func(config_mqtt_conn):
|
||||||
'''don't dispatch for unknwon function names'''
|
'''don't dispatch for unknwon function names'''
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import aiomqtt
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mock import patch, Mock
|
from mock import patch, Mock
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
from app.src.proxy import Proxy
|
from proxy import Proxy
|
||||||
from app.src.mqtt import Mqtt
|
from mqtt import Mqtt
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|||||||
32
app/tests/test_server.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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 = get_log_level()
|
||||||
|
assert log_lvl == None
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
|
||||||
|
log_lvl = get_log_level()
|
||||||
|
assert log_lvl == logging.DEBUG
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'LOG_LVL': 'INFO'}):
|
||||||
|
log_lvl = get_log_level()
|
||||||
|
assert log_lvl == logging.INFO
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||||
|
log_lvl = get_log_level()
|
||||||
|
assert log_lvl == logging.WARNING
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'LOG_LVL': 'ERROR'}):
|
||||||
|
log_lvl = get_log_level()
|
||||||
|
assert log_lvl == logging.ERROR
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||||
|
log_lvl = get_log_level()
|
||||||
|
assert log_lvl == None
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest
|
import pytest
|
||||||
from app.src.singleton import Singleton
|
from singleton import Singleton
|
||||||
|
|
||||||
class Test(metaclass=Singleton):
|
class Example(metaclass=Singleton):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass # is a dummy test class
|
pass # is a dummy test class
|
||||||
|
|
||||||
def test_singleton_metaclass():
|
def test_singleton_metaclass():
|
||||||
Singleton._instances.clear()
|
Singleton._instances.clear()
|
||||||
a = Test()
|
a = Example()
|
||||||
assert 1 == len(Singleton._instances)
|
assert 1 == len(Singleton._instances)
|
||||||
b = Test()
|
b = Example()
|
||||||
assert 1 == len(Singleton._instances)
|
assert 1 == len(Singleton._instances)
|
||||||
assert a is b
|
assert a is b
|
||||||
del a
|
del a
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
from async_stream import AsyncIfcImpl, StreamPtr
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.infos import Infos, Register
|
from infos import Infos, Register
|
||||||
from app.src.modbus import Modbus
|
from modbus import Modbus
|
||||||
from app.src.messages import State, Message
|
from messages import State, Message
|
||||||
|
from proxy import Proxy
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
@@ -24,6 +25,8 @@ heartbeat = 60
|
|||||||
|
|
||||||
class Mqtt():
|
class Mqtt():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.clear()
|
||||||
|
def clear(self):
|
||||||
self.key = ''
|
self.key = ''
|
||||||
self.data = ''
|
self.data = ''
|
||||||
|
|
||||||
@@ -37,6 +40,9 @@ class FakeIfc(AsyncIfcImpl):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.remote = StreamPtr(None)
|
self.remote = StreamPtr(None)
|
||||||
|
|
||||||
|
async def create_remote(self):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
class MemoryStream(SolarmanV5):
|
class MemoryStream(SolarmanV5):
|
||||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||||
_ifc = FakeIfc()
|
_ifc = FakeIfc()
|
||||||
@@ -47,7 +53,6 @@ class MemoryStream(SolarmanV5):
|
|||||||
self.mb_timeout = 0.5
|
self.mb_timeout = 0.5
|
||||||
self.sent_pdu = b''
|
self.sent_pdu = b''
|
||||||
self.ifc.tx_fifo.reg_trigger(self.write_cb)
|
self.ifc.tx_fifo.reg_trigger(self.write_cb)
|
||||||
self.mqtt = Mqtt()
|
|
||||||
self.__msg = msg
|
self.__msg = msg
|
||||||
self.__msg_len = len(msg)
|
self.__msg_len = len(msg)
|
||||||
self.__chunks = chunks
|
self.__chunks = chunks
|
||||||
@@ -59,7 +64,6 @@ class MemoryStream(SolarmanV5):
|
|||||||
self.db.stat['proxy']['AT_Command'] = 0
|
self.db.stat['proxy']['AT_Command'] = 0
|
||||||
self.db.stat['proxy']['AT_Command_Blocked'] = 0
|
self.db.stat['proxy']['AT_Command_Blocked'] = 0
|
||||||
self.test_exception_async_write = False
|
self.test_exception_async_write = False
|
||||||
self.entity_prfx = ''
|
|
||||||
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
|
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
|
||||||
self.key = ''
|
self.key = ''
|
||||||
self.data = ''
|
self.data = ''
|
||||||
@@ -82,8 +86,8 @@ class MemoryStream(SolarmanV5):
|
|||||||
self.__chunk_idx = 0
|
self.__chunk_idx = 0
|
||||||
|
|
||||||
def publish_mqtt(self, key, data):
|
def publish_mqtt(self, key, data):
|
||||||
self.key = key
|
Proxy.mqtt.key = key
|
||||||
self.data = data
|
Proxy.mqtt.data = data
|
||||||
|
|
||||||
def _read(self) -> int:
|
def _read(self) -> int:
|
||||||
copied_bytes = 0
|
copied_bytes = 0
|
||||||
@@ -109,7 +113,7 @@ class MemoryStream(SolarmanV5):
|
|||||||
c.ifc.remote.stream = self
|
c.ifc.remote.stream = self
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def _SolarmanV5__flush_recv_msg(self) -> None:
|
def _SolarmanBase__flush_recv_msg(self) -> None:
|
||||||
self.msg_recvd.append(
|
self.msg_recvd.append(
|
||||||
{
|
{
|
||||||
'control': self.control,
|
'control': self.control,
|
||||||
@@ -117,7 +121,7 @@ class MemoryStream(SolarmanV5):
|
|||||||
'data_len': self.data_len
|
'data_len': self.data_len
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
super()._SolarmanV5__flush_recv_msg()
|
super()._SolarmanBase__flush_recv_msg()
|
||||||
self.msg_count += 1
|
self.msg_count += 1
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +131,12 @@ def get_sn() -> bytes:
|
|||||||
def get_sn_int() -> int:
|
def get_sn_int() -> int:
|
||||||
return 2070233889
|
return 2070233889
|
||||||
|
|
||||||
|
def get_dcu_sn() -> bytes:
|
||||||
|
return b'\x20\x43\x65\x7b'
|
||||||
|
|
||||||
|
def get_dcu_sn_int() -> int:
|
||||||
|
return 2070233888
|
||||||
|
|
||||||
def get_inv_no() -> bytes:
|
def get_inv_no() -> bytes:
|
||||||
return b'T170000000000001'
|
return b'T170000000000001'
|
||||||
|
|
||||||
@@ -557,6 +567,17 @@ def at_command_rsp_msg(): # 0x1510
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def at_command_interim_rsp_msg(): # 0x0510
|
||||||
|
msg = b'\xa5\x25\x00\x10\x05\x03\x03' +get_sn() +b'\x08\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += b'\x00\x00\x00\x00+ok=10\x2c'
|
||||||
|
msg += b'start download\x0d\x0a'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def heartbeat_ind_msg(): # 0x4710
|
def heartbeat_ind_msg(): # 0x4710
|
||||||
msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn()
|
msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn()
|
||||||
@@ -612,6 +633,15 @@ def msg_modbus_cmd_fwd():
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_modbus_cmd_seq():
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x05\x26\x30\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x06\x01\x00\x01'
|
||||||
|
msg += b'\x03\xe8'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_modbus_cmd_crc_err():
|
def msg_modbus_cmd_crc_err():
|
||||||
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
|
||||||
@@ -634,6 +664,32 @@ def msg_modbus_rsp(): # 0x1510
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_modbus_interim_rsp(): # 0x0510
|
||||||
|
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += b'\x0a\xe2\xfa\x33\x01\x03\x28\x40\x10\x08\xd8'
|
||||||
|
msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02'
|
||||||
|
msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04'
|
||||||
|
msg += b'\x00\x01\x00\x00\x6c\x68'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_modbus_rsp_inv_id2(): # 0x1510
|
||||||
|
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += b'\x0a\xe2\xfa\x33\x02\x03\x28\x40\x10\x08\xd8'
|
||||||
|
msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02'
|
||||||
|
msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04'
|
||||||
|
msg += b'\x00\x01\x00\x00\x2a\xaa'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_modbus_invalid(): # 0x1510
|
def msg_modbus_invalid(): # 0x1510
|
||||||
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00'
|
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00'
|
||||||
@@ -669,9 +725,94 @@ def msg_unknown_cmd_rsp(): # 0x1510
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_modbus_rsp(): # 0x1510
|
||||||
|
msg = b'\xa5\x6d\x00\x10\x15\x03\x03' +get_dcu_sn() +b'\x02\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += b'\x4d\x0d\x84\x34\x01\x03\x5a\x34\x31\x30\x31'
|
||||||
|
msg += b'\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34\x00\x32\x00\x00'
|
||||||
|
msg += b'\x00\x32\x00\x00\x00\x00\x10\x7b\x00\x02\x00\x02\x14\x9b\xfe\xfd'
|
||||||
|
msg += b'\x25\x28\x0c\xe1\x0c\xde\x0c\xe1\x0c\xe1\x0c\xe0\x0c\xe1\x0c\xe3'
|
||||||
|
msg += b'\x0c\xdf\x0c\xe0\x0c\xe2\x0c\xe1\x0c\xe1\x0c\xe2\x0c\xe2\x0c\xe3'
|
||||||
|
msg += b'\x0c\xdf\x00\x14\x00\x14\x00\x13\x0f\x94\x01\x4a\x00\x01\x00\x15'
|
||||||
|
msg += b'\x00\x00\x02\x05\x02\x01\x14\xab'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_dev_ind_msg(): # 0x4110
|
||||||
|
msg = b'\xa5\x3a\x01\x10\x41\x91\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32'
|
||||||
|
msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35'
|
||||||
|
msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26'
|
||||||
|
msg += b'\x30\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x7a\x75\x68\x61\x75\x73\x65\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x01\x01\x01\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_dev_rsp_msg(): # 0x1110
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_data_ind_msg(): # 0x4210
|
||||||
|
msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde'
|
||||||
|
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
|
||||||
|
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
|
||||||
|
msg += b'\x0d\x3a\x00\x00\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
|
||||||
|
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_data_rsp_msg(): # 0x1210
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_tsun_allow_all():
|
def config_tsun_allow_all():
|
||||||
Config.act_config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
|
Config.act_config = {
|
||||||
|
'ha':{
|
||||||
|
'auto_conf_prefix': 'homeassistant',
|
||||||
|
'discovery_prefix': 'homeassistant',
|
||||||
|
'entity_prefix': 'tsun',
|
||||||
|
'proxy_node_id': 'test_1',
|
||||||
|
'proxy_unique_id': ''
|
||||||
|
},
|
||||||
|
'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
|
||||||
|
Proxy.class_init()
|
||||||
|
Proxy.mqtt = Mqtt() # set dummy mqtt instance
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_no_tsun_inv1():
|
def config_no_tsun_inv1():
|
||||||
@@ -679,7 +820,29 @@ def config_no_tsun_inv1():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_tsun_inv1():
|
def config_tsun_inv1():
|
||||||
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
|
Config.act_config = {
|
||||||
|
'ha':{
|
||||||
|
'auto_conf_prefix': 'homeassistant',
|
||||||
|
'discovery_prefix': 'homeassistant',
|
||||||
|
'entity_prefix': 'tsun',
|
||||||
|
'proxy_node_id': 'test_1',
|
||||||
|
'proxy_unique_id': ''
|
||||||
|
},
|
||||||
|
'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||||
|
Proxy.class_init()
|
||||||
|
Proxy.mqtt = Mqtt()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_tsun_scan():
|
||||||
|
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xffc0, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_tsun_scan_dcu():
|
||||||
|
Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'client_mode': {'host': '192.168.1.1.'}, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_tsun_dcu1():
|
||||||
|
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||||
|
|
||||||
def test_read_message(device_ind_msg):
|
def test_read_message(device_ind_msg):
|
||||||
Config.act_config = {'solarman':{'enabled': True}}
|
Config.act_config = {'solarman':{'enabled': True}}
|
||||||
@@ -960,6 +1123,34 @@ def test_read_two_messages3(config_tsun_allow_all, device_ind_msg2, device_rsp_m
|
|||||||
assert m.ifc.tx_fifo.get()==b''
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_read_two_messages4(config_tsun_dcu1, dcu_dev_ind_msg, dcu_dev_rsp_msg, dcu_data_ind_msg, dcu_data_rsp_msg):
|
||||||
|
_ = config_tsun_dcu1
|
||||||
|
m = MemoryStream(dcu_dev_ind_msg, (0,))
|
||||||
|
m.append_msg(dcu_data_ind_msg)
|
||||||
|
assert 0 == m.sensor_list
|
||||||
|
m._init_new_client_conn()
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233888
|
||||||
|
assert m.unique_id == '2070233888'
|
||||||
|
assert m.msg_recvd[0]['control']==0x4110
|
||||||
|
assert m.msg_recvd[0]['seq']=='01:92'
|
||||||
|
assert m.msg_recvd[0]['data_len']==314
|
||||||
|
assert m.msg_recvd[1]['control']==0x4210
|
||||||
|
assert m.msg_recvd[1]['seq']=='02:93'
|
||||||
|
assert m.msg_recvd[1]['data_len']==111
|
||||||
|
assert '3026' == m.db.get_db_value(Register.SENSOR_LIST, None)
|
||||||
|
assert 0x3026 == m.sensor_list
|
||||||
|
assert m.ifc.fwd_fifo.get()==dcu_dev_ind_msg+dcu_data_ind_msg
|
||||||
|
assert m.ifc.tx_fifo.get()==dcu_dev_rsp_msg+dcu_data_rsp_msg
|
||||||
|
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_unkown_frame_code(config_tsun_inv1, inverter_ind_msg_81, inverter_rsp_msg_81):
|
def test_unkown_frame_code(config_tsun_inv1, inverter_ind_msg_81, inverter_rsp_msg_81):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
m = MemoryStream(inverter_ind_msg_81, (0,))
|
m = MemoryStream(inverter_ind_msg_81, (0,))
|
||||||
@@ -1102,7 +1293,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
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
|
||||||
m.seq.server_side = False # simulate forawding to TSUN cloud
|
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 str(m.seq) == '0d:0e' # value after forwarding indication
|
||||||
assert m.ifc.fwd_fifo.get()==sync_start_fwd_msg
|
assert m.ifc.fwd_fifo.get()==sync_start_fwd_msg
|
||||||
|
|
||||||
@@ -1328,8 +1519,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
|
|||||||
assert m.ifc.fwd_fifo.get()==b''
|
assert m.ifc.fwd_fifo.get()==b''
|
||||||
assert m.sent_pdu == b''
|
assert m.sent_pdu == b''
|
||||||
assert str(m.seq) == '01:01'
|
assert str(m.seq) == '01:01'
|
||||||
assert m.mqtt.key == ''
|
assert Proxy.mqtt.key == ''
|
||||||
assert m.mqtt.data == ""
|
assert Proxy.mqtt.data == ""
|
||||||
|
|
||||||
m.append_msg(inverter_ind_msg)
|
m.append_msg(inverter_ind_msg)
|
||||||
m.read() # read inverter ind
|
m.read() # read inverter ind
|
||||||
@@ -1345,8 +1536,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
|
|||||||
m.sent_pdu = bytearray()
|
m.sent_pdu = bytearray()
|
||||||
|
|
||||||
assert str(m.seq) == '02:03'
|
assert str(m.seq) == '02:03'
|
||||||
assert m.mqtt.key == ''
|
assert Proxy.mqtt.key == ''
|
||||||
assert m.mqtt.data == ""
|
assert Proxy.mqtt.data == ""
|
||||||
|
|
||||||
m.append_msg(at_command_rsp_msg)
|
m.append_msg(at_command_rsp_msg)
|
||||||
m.read() # read at resp
|
m.read() # read at resp
|
||||||
@@ -1355,8 +1546,9 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
|
|||||||
assert m.ifc.rx_get()==b''
|
assert m.ifc.rx_get()==b''
|
||||||
assert m.ifc.tx_fifo.get()==b''
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
assert m.ifc.fwd_fifo.get()==b''
|
assert m.ifc.fwd_fifo.get()==b''
|
||||||
assert m.key == 'at_resp'
|
assert Proxy.mqtt.key == 'tsun/at_resp'
|
||||||
assert m.data == "+ok"
|
assert Proxy.mqtt.data == "+ok"
|
||||||
|
Proxy.mqtt.clear() # clear last test result
|
||||||
|
|
||||||
m.sent_pdu = bytearray()
|
m.sent_pdu = bytearray()
|
||||||
m.test_exception_async_write = True
|
m.test_exception_async_write = True
|
||||||
@@ -1368,8 +1560,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
|
|||||||
assert m.sent_pdu == b''
|
assert m.sent_pdu == b''
|
||||||
assert str(m.seq) == '03:04'
|
assert str(m.seq) == '03:04'
|
||||||
assert m.forward_at_cmd_resp == False
|
assert m.forward_at_cmd_resp == False
|
||||||
assert m.mqtt.key == ''
|
assert Proxy.mqtt.key == ''
|
||||||
assert m.mqtt.data == ""
|
assert Proxy.mqtt.data == ""
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -1386,8 +1578,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
|
|||||||
assert m.ifc.tx_fifo.get()==b''
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
assert m.ifc.fwd_fifo.get()==b''
|
assert m.ifc.fwd_fifo.get()==b''
|
||||||
assert str(m.seq) == '01:01'
|
assert str(m.seq) == '01:01'
|
||||||
assert m.mqtt.key == ''
|
assert Proxy.mqtt.key == ''
|
||||||
assert m.mqtt.data == ""
|
assert Proxy.mqtt.data == ""
|
||||||
|
|
||||||
m.append_msg(inverter_ind_msg)
|
m.append_msg(inverter_ind_msg)
|
||||||
m.read()
|
m.read()
|
||||||
@@ -1403,8 +1595,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
|
|||||||
assert m.ifc.fwd_fifo.get()==b''
|
assert m.ifc.fwd_fifo.get()==b''
|
||||||
assert str(m.seq) == '02:02'
|
assert str(m.seq) == '02:02'
|
||||||
assert m.forward_at_cmd_resp == False
|
assert m.forward_at_cmd_resp == False
|
||||||
assert m.mqtt.key == 'at_resp'
|
assert Proxy.mqtt.key == 'tsun/at_resp'
|
||||||
assert m.mqtt.data == "'AT+WEBU' is forbidden"
|
assert Proxy.mqtt.data == "'AT+WEBU' is forbidden"
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
|
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
|
||||||
@@ -1493,6 +1685,29 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
|
|||||||
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg):
|
||||||
|
_ = config_tsun_inv1
|
||||||
|
m = MemoryStream(at_command_interim_rsp_msg)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.db.stat['proxy']['Invalid_Msg_Format'] = 0
|
||||||
|
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
|
m.forward_at_cmd_resp = True
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.control == 0x0510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==37
|
||||||
|
assert m.ifc.fwd_fifo.get()==at_command_interim_rsp_msg
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m.db.stat['proxy']['Unknown_Msg'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
|
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
m = MemoryStream(b'')
|
m = MemoryStream(b'')
|
||||||
@@ -1521,6 +1736,34 @@ def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
|
|||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req_seq(config_tsun_inv1, msg_modbus_cmd_seq):
|
||||||
|
_ = config_tsun_inv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.snr = get_sn_int()
|
||||||
|
m.sensor_list = 0x2b0
|
||||||
|
m.state = State.up
|
||||||
|
c = m.createClientStream(msg_modbus_cmd_seq)
|
||||||
|
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['AT_Command'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.db.stat['proxy']['Invalid_Msg_Format'] = 0
|
||||||
|
c.read() # read complete msg, and dispatch msg
|
||||||
|
assert not c.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert c.msg_count == 1
|
||||||
|
assert c.control == 0x4510
|
||||||
|
assert str(c.seq) == '03:02'
|
||||||
|
assert c.header_len==11
|
||||||
|
assert c.data_len==23
|
||||||
|
assert c.ifc.fwd_fifo.get()==msg_modbus_cmd_seq
|
||||||
|
assert c.ifc.tx_fifo.get()==b''
|
||||||
|
assert m.sent_pdu == b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
|
def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
m = MemoryStream(b'')
|
m = MemoryStream(b'')
|
||||||
@@ -1759,6 +2002,79 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
|
|||||||
assert next(m.mb_timer.exp_count) == 4
|
assert next(m.mb_timer.exp_count) == 4
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modbus_scaning(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp_msg, msg_modbus_rsp, msg_modbus_rsp_inv_id2):
|
||||||
|
_ = config_tsun_scan
|
||||||
|
assert asyncio.get_running_loop()
|
||||||
|
|
||||||
|
m = MemoryStream(heartbeat_ind_msg, (0x15,0x56,0))
|
||||||
|
m.append_msg(msg_modbus_rsp)
|
||||||
|
m.append_msg(msg_modbus_rsp_inv_id2)
|
||||||
|
assert m.mb_scan == False
|
||||||
|
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
assert m.mb_timer.tim == None
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert m.mb_scan == True
|
||||||
|
assert m.mb_start_reg == 0xff80
|
||||||
|
assert m.mb_step == 0x40
|
||||||
|
assert m.mb_bytes == 0x14
|
||||||
|
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||||
|
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.control == 0x4710
|
||||||
|
|
||||||
|
assert m.msg_recvd[0]['control']==0x4710
|
||||||
|
assert m.msg_recvd[0]['seq']=='84:11'
|
||||||
|
assert m.msg_recvd[0]['data_len']==0x1
|
||||||
|
|
||||||
|
assert m.ifc.tx_fifo.get()==heartbeat_rsp_msg
|
||||||
|
assert m.ifc.fwd_fifo.get()==heartbeat_ind_msg
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
|
||||||
|
m.ifc.tx_clear() # clear send buffer for next test
|
||||||
|
assert isclose(m.mb_timeout, 0.5)
|
||||||
|
assert next(m.mb_timer.exp_count) == 0
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
assert m.sent_pdu==b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \
|
||||||
|
b'\x00\x00\x00\x00\x00\x00\x01\x03\xff\xc0\x00\x14\x75\xed\x33\x15'
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.msg_recvd[1]['control']==0x1510
|
||||||
|
assert m.msg_recvd[1]['seq']=='03:03'
|
||||||
|
assert m.msg_recvd[1]['data_len']==0x3b
|
||||||
|
assert m.mb.last_addr == 1
|
||||||
|
assert m.mb.last_fcode == 3
|
||||||
|
assert m.mb.last_reg == 0xffc0 # mb_start_reg + mb_step
|
||||||
|
assert m.mb.last_len == 20
|
||||||
|
assert m.mb.err == 0
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
assert m.sent_pdu==b'\xa5\x17\x00\x10E\x04\x03!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \
|
||||||
|
b'\x00\x00\x00\x00\x00\x00\x02\x03\x00\x00\x00\x14\x45\xf6\xbf\x15'
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 3
|
||||||
|
assert m.msg_recvd[2]['control']==0x1510
|
||||||
|
assert m.msg_recvd[2]['seq']=='03:03'
|
||||||
|
assert m.msg_recvd[2]['data_len']==0x3b
|
||||||
|
assert m.mb.last_addr == 2
|
||||||
|
assert m.mb.last_fcode == 3
|
||||||
|
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
|
||||||
|
assert m.mb.last_len == 20
|
||||||
|
assert m.mb.err == 0
|
||||||
|
|
||||||
|
assert next(m.mb_timer.exp_count) == 3
|
||||||
|
m.close()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
@@ -1768,7 +2084,7 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
|||||||
assert m.no_forwarding == False
|
assert m.no_forwarding == False
|
||||||
assert m.mb_timer.tim == None
|
assert m.mb_timer.tim == None
|
||||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
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.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 m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
|
||||||
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
|
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
|
||||||
@@ -1791,6 +2107,79 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
|||||||
assert next(m.mb_timer.exp_count) == 3
|
assert next(m.mb_timer.exp_count) == 3
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_client_mode_scan(config_tsun_scan_dcu, str_test_ip, dcu_modbus_rsp):
|
||||||
|
_ = config_tsun_scan_dcu
|
||||||
|
assert asyncio.get_running_loop()
|
||||||
|
m = MemoryStream(dcu_modbus_rsp, (131,0,))
|
||||||
|
m.append_msg(dcu_modbus_rsp)
|
||||||
|
assert m.state == State.init
|
||||||
|
assert m.no_forwarding == False
|
||||||
|
assert m.mb_timer.tim == None
|
||||||
|
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||||
|
await m.send_start_cmd(get_dcu_sn_int(), str_test_ip, False, m.mb_first_timeout)
|
||||||
|
assert m.mb_start_reg == 0x0000
|
||||||
|
assert m.mb_step == 0x100
|
||||||
|
assert m.mb_bytes == 0x2d
|
||||||
|
|
||||||
|
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x95\x15')
|
||||||
|
assert m.mb_scan == True
|
||||||
|
m.mb_step = 0
|
||||||
|
assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
|
||||||
|
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
|
||||||
|
assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120
|
||||||
|
|
||||||
|
assert m.state == State.up
|
||||||
|
assert m.no_forwarding == True
|
||||||
|
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
assert isclose(m.mb_timeout, 0.5)
|
||||||
|
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.msg_recvd[0]['control']==0x1510
|
||||||
|
assert m.msg_recvd[0]['seq']=='03:03'
|
||||||
|
assert m.msg_recvd[0]['data_len']==109
|
||||||
|
assert m.mb.last_addr == 1
|
||||||
|
assert m.mb.last_fcode == 3
|
||||||
|
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
|
||||||
|
assert m.mb.last_len == 45
|
||||||
|
assert m.mb.err == 0
|
||||||
|
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225)
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604)
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0)
|
||||||
|
assert m.new_data['batterie'] == True
|
||||||
|
m.new_data['batterie'] = False
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x04\x03 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x9b\x15')
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.msg_recvd[1]['control']==0x1510
|
||||||
|
assert m.msg_recvd[1]['seq']=='03:03'
|
||||||
|
assert m.msg_recvd[1]['data_len']==109
|
||||||
|
assert m.mb.last_addr == 1
|
||||||
|
assert m.mb.last_fcode == 3
|
||||||
|
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
|
||||||
|
assert m.mb.last_len == 45
|
||||||
|
assert m.mb.err == 0
|
||||||
|
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225)
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604)
|
||||||
|
assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0)
|
||||||
|
assert m.new_data['batterie'] == False
|
||||||
|
|
||||||
|
assert next(m.mb_timer.exp_count) == 1
|
||||||
|
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_timeout(config_tsun_inv1):
|
def test_timeout(config_tsun_inv1):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
m = MemoryStream(b'')
|
m = MemoryStream(b'')
|
||||||
@@ -1803,3 +2192,30 @@ def test_timeout(config_tsun_inv1):
|
|||||||
assert SolarmanV5.MAX_DEF_IDLE_TIME == m._timeout()
|
assert SolarmanV5.MAX_DEF_IDLE_TIME == m._timeout()
|
||||||
m.state = State.closed
|
m.state = State.closed
|
||||||
m.close()
|
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
@@ -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,13 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest, logging, asyncio
|
import pytest, logging, asyncio
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
from async_stream import AsyncIfcImpl, StreamPtr
|
||||||
from app.src.gen3.talent import Talent, Control
|
from gen3.talent import Talent, Control
|
||||||
from app.src.config import Config
|
from cnf.config import Config
|
||||||
from app.src.infos import Infos, Register
|
from infos import Infos, Register
|
||||||
from app.src.modbus import Modbus
|
from modbus import Modbus
|
||||||
from app.src.messages import State
|
from messages import State
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
@@ -328,6 +329,90 @@ def msg_inverter_ind_new(): # Data indication from DSP V5.0.17
|
|||||||
msg += b'\x00\x00\x00\x00'
|
msg += b'\x00\x00\x00\x00'
|
||||||
return msg
|
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
|
@pytest.fixture
|
||||||
def msg_inverter_ind_0w(): # Data indication with 0.5W grid output
|
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'
|
msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||||
@@ -384,15 +469,15 @@ def config_tsun_allow_all():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_no_tsun_inv1():
|
def config_no_tsun_inv1():
|
||||||
Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
|
Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': True, 'suggested_area':'roof'}}}
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_tsun_inv1():
|
def config_tsun_inv1():
|
||||||
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
|
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0x01900001, 'modbus_polling': True, 'suggested_area':'roof'}}}
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config_no_modbus_poll():
|
def config_no_modbus_poll():
|
||||||
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}}
|
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': False, 'suggested_area':'roof'}}}
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_ota_req(): # Over the air update request from tsun cloud
|
def msg_ota_req(): # Over the air update request from tsun cloud
|
||||||
@@ -733,6 +818,236 @@ def multiple_recv_buf(): # There are three message in the buffer, but the second
|
|||||||
msg += b'\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' # | 0...<T.A,B,C'
|
msg += b'\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' # | 0...<T.A,B,C'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_controller_ms3000_ind(): # Data indication from the controller
|
||||||
|
msg = b'\x00\x00\x04\xdf\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
|
||||||
|
msg += b'\x01\x00\x00\x01'
|
||||||
|
msg += b'\x95\x18\x5e\x19\x98\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' # | ..^.....9..+.T.R
|
||||||
|
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x32\x2e\x30\x31\x2e\x31\x33\x00' # | SW_400_V2.01.13.
|
||||||
|
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' # | .'.T.Raymon../.T
|
||||||
|
msg += b'\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88' # | .RSW-1-10001..Z.
|
||||||
|
msg += b'\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f' # | T.t.raymoniot.co
|
||||||
|
msg += b'\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61' # | m..Z.T.logger.ta
|
||||||
|
msg += b'\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e' # | lent-monitoring.
|
||||||
|
msg += b'\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | com../.T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32\xe8\x54\x10\xff' # | ...........2.T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .6.T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff\xff\xff\xff\xff' # | .......:.T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3e\xa0\x54' # | .............>.T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J
|
||||||
|
msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i
|
||||||
|
msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T
|
||||||
|
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T..........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T....
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................
|
||||||
|
msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
|
||||||
|
msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T..
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
|
||||||
|
msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT......
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
|
||||||
|
msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I..
|
||||||
|
msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I
|
||||||
|
msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I.......
|
||||||
|
msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I.....
|
||||||
|
msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI...
|
||||||
|
msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I.
|
||||||
|
msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........
|
||||||
|
msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I......
|
||||||
|
msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I....
|
||||||
|
msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I..
|
||||||
|
msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L.........
|
||||||
|
msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI...
|
||||||
|
msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I.
|
||||||
|
msg += b'\x00\x00\xff'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_inverter_ms3000_ind(): # Data indication from the controller
|
||||||
|
msg = b'\x00\x00\x08\xff\x10R170000000000001\x91\x04\x01\x90\x00\x00\x10R170000000000001'
|
||||||
|
msg += b'\x01\x00\x00\x01'
|
||||||
|
msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
|
||||||
|
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
|
||||||
|
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
|
||||||
|
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
|
||||||
|
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
|
||||||
|
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
|
||||||
|
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
|
||||||
|
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
|
||||||
|
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
|
||||||
|
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
|
||||||
|
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
|
||||||
|
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
|
||||||
|
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
|
||||||
|
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
|
||||||
|
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
|
||||||
|
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
|
||||||
|
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
|
||||||
|
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
|
||||||
|
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
|
||||||
|
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
|
||||||
|
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
|
||||||
|
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
|
||||||
|
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
|
||||||
|
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
|
||||||
|
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
|
||||||
|
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
|
||||||
|
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
|
||||||
|
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
|
||||||
|
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
|
||||||
|
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
|
||||||
|
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
|
||||||
|
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
|
||||||
|
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
|
||||||
|
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
|
||||||
|
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
|
||||||
|
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
|
||||||
|
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
|
||||||
|
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
|
||||||
|
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
|
||||||
|
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
|
||||||
|
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
|
||||||
|
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
|
||||||
|
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
|
||||||
|
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
|
||||||
|
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
|
||||||
|
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
|
||||||
|
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
|
||||||
|
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
|
||||||
|
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
|
||||||
|
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
|
||||||
|
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
|
||||||
|
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
|
||||||
|
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
|
||||||
|
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
|
||||||
|
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
|
||||||
|
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
|
||||||
|
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
|
||||||
|
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
|
||||||
|
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
|
||||||
|
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
|
||||||
|
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
|
||||||
|
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
|
||||||
|
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
|
||||||
|
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
|
||||||
|
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
|
||||||
|
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
|
||||||
|
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
|
||||||
|
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
|
||||||
|
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
|
||||||
|
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
|
||||||
|
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
|
||||||
|
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
|
||||||
|
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
|
||||||
|
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
|
||||||
|
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
|
||||||
|
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
|
||||||
|
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
|
||||||
|
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
|
||||||
|
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
|
||||||
|
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
|
||||||
|
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
|
||||||
|
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
|
||||||
|
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
|
||||||
|
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
|
||||||
|
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
|
||||||
|
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
|
||||||
|
msg += b'\x53\x00\x66' # | S.f'
|
||||||
|
return msg
|
||||||
|
|
||||||
def test_read_message(msg_contact_info):
|
def test_read_message(msg_contact_info):
|
||||||
Config.act_config = {'tsun':{'enabled': True}}
|
Config.act_config = {'tsun':{'enabled': True}}
|
||||||
m = MemoryStream(msg_contact_info, (0,))
|
m = MemoryStream(msg_contact_info, (0,))
|
||||||
@@ -1486,6 +1801,64 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
|
|||||||
m.close()
|
m.close()
|
||||||
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
||||||
|
|
||||||
|
def test_msg_inv_ind4(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack):
|
||||||
|
'''Check sonar_lists of MS-3000 inverter'''
|
||||||
|
_ = config_tsun_inv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
with patch.object(logging, 'warning') as spy:
|
||||||
|
m = MemoryStream(msg_inverter_ms3000_ind, (0,))
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Invalid_Data_Type'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
spy.assert_not_called()
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==145
|
||||||
|
assert m.msg_id==4
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.data_len==2284
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m.ifc.fwd_fifo.peek())
|
||||||
|
assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind
|
||||||
|
assert m.ifc.tx_fifo.get()==msg_inverter_ack
|
||||||
|
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
||||||
|
assert m.db.get_db_value(Register.TS_GRID) == 1739866976
|
||||||
|
m.db.db['grid'] = {'Output_Power': 100}
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_inv_ind5(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack):
|
||||||
|
'''Check that unexpected sonar_lists will log a warning'''
|
||||||
|
_ = config_tsun_inv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
with patch.object(logging, 'warning') as spy:
|
||||||
|
m = MemoryStream(msg_inverter_ms3000_ind, (0,))
|
||||||
|
m.sensor_list = 0x01900002 # change the expected sensor_list
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Invalid_Data_Type'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
spy.assert_called()
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==145
|
||||||
|
assert m.msg_id==4
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.data_len==2284
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m.ifc.fwd_fifo.peek())
|
||||||
|
assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind
|
||||||
|
assert m.ifc.tx_fifo.get()==msg_inverter_ack
|
||||||
|
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
||||||
|
assert m.db.get_db_value(Register.TS_GRID) == 1739866976
|
||||||
|
m.db.db['grid'] = {'Output_Power': 100}
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
|
def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
@@ -1530,6 +1903,18 @@ def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid):
|
|||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_3000(config_tsun_allow_all, msg_inverter_ms3000_ind):
|
||||||
|
_ = config_tsun_allow_all
|
||||||
|
m = MemoryStream(msg_inverter_ms3000_ind, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||||
|
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 3000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 0 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MS3000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
|
def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
|
||||||
_ = config_tsun_inv1
|
_ = config_tsun_inv1
|
||||||
m = MemoryStream(msg_ota_req, (0,), False)
|
m = MemoryStream(msg_ota_req, (0,), False)
|
||||||
@@ -2100,6 +2485,56 @@ async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
|
|||||||
assert next(m.mb_timer.exp_count) == 4
|
assert next(m.mb_timer.exp_count) == 4
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modbus_scaning(config_tsun_inv1, msg_inverter_ind, msg_modbus_rsp21):
|
||||||
|
_ = config_tsun_inv1
|
||||||
|
assert asyncio.get_running_loop()
|
||||||
|
|
||||||
|
m = MemoryStream(msg_inverter_ind, (0x8f,0))
|
||||||
|
m.append_msg(msg_modbus_rsp21)
|
||||||
|
m.mb_scan = True
|
||||||
|
m.mb_start_reg = 0x4560
|
||||||
|
m.mb_bytes = 0x14
|
||||||
|
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
assert m.mb_timer.tim == None
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert m.msg_recvd[0]['ctrl']==145
|
||||||
|
assert m.msg_recvd[0]['msg_id']==4
|
||||||
|
assert m.msg_recvd[0]['header_len']==23
|
||||||
|
assert m.msg_recvd[0]['data_len']==120
|
||||||
|
assert m.ifc.fwd_fifo.get()==msg_inverter_ind
|
||||||
|
assert m.ifc.tx_fifo.get()==b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
|
||||||
|
m.ifc.tx_clear() # clear send buffer for next test
|
||||||
|
assert isclose(m.mb_timeout, 0.5)
|
||||||
|
assert next(m.mb_timer.exp_count) == 0
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
assert m.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x03\x45\x60\x00\x14\x50\xd7'
|
||||||
|
assert m.ifc.tx_fifo.get()==b''
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.msg_recvd[1]['ctrl']==145
|
||||||
|
assert m.msg_recvd[1]['msg_id']==119
|
||||||
|
assert m.msg_recvd[1]['header_len']==23
|
||||||
|
assert m.msg_recvd[1]['data_len']==50
|
||||||
|
assert m.mb.last_addr == 1
|
||||||
|
assert m.mb.last_fcode == 3
|
||||||
|
assert m.mb.last_reg == 0x4560
|
||||||
|
assert m.mb.last_len == 20
|
||||||
|
assert m.mb.err == 0
|
||||||
|
|
||||||
|
assert next(m.mb_timer.exp_count) == 2
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf):
|
def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf):
|
||||||
_ = config_tsun_allow_all
|
_ = config_tsun_allow_all
|
||||||
m = MemoryStream(broken_recv_buf, (0,))
|
m = MemoryStream(broken_recv_buf, (0,))
|
||||||
@@ -2151,3 +2586,34 @@ def test_timeout(config_tsun_inv1):
|
|||||||
m.modbus_polling = False
|
m.modbus_polling = False
|
||||||
assert Talent.MAX_DEF_IDLE_TIME == m._timeout()
|
assert Talent.MAX_DEF_IDLE_TIME == m._timeout()
|
||||||
m.close()
|
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
|
||||||
|
|||||||
3
ha_addons/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.data.json
|
||||||
|
config.yaml
|
||||||
|
apparmor.txt
|
||||||
181
ha_addons/Makefile
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#!make
|
||||||
|
include ../.env
|
||||||
|
|
||||||
|
.PHONY: debug dev build clean rootfs repro rc rel
|
||||||
|
|
||||||
|
SHELL = /bin/sh
|
||||||
|
JINJA = jinja2
|
||||||
|
IMAGE = tsun-gen3-addon
|
||||||
|
|
||||||
|
|
||||||
|
# Source folders for building the local add-on
|
||||||
|
SRC=../app
|
||||||
|
SRC_PROXY=$(SRC)/src
|
||||||
|
CNF_PROXY=$(SRC)/config
|
||||||
|
|
||||||
|
# Target folders for building the local add-on and the docker container
|
||||||
|
ADDON_PATH = ha_addon
|
||||||
|
DST=$(ADDON_PATH)/rootfs
|
||||||
|
DST_PROXY=$(DST)/home/proxy
|
||||||
|
|
||||||
|
# base director of the add-on repro for installing the add-on git repros
|
||||||
|
INST_BASE=../../ha-addons
|
||||||
|
|
||||||
|
# Template folder for build the config.yaml variants
|
||||||
|
TEMPL=templates
|
||||||
|
|
||||||
|
# help variable STAGE determine the target to build
|
||||||
|
STAGE=dev
|
||||||
|
debug : STAGE=debug
|
||||||
|
rc : STAGE=rc
|
||||||
|
rel : STAGE=rel
|
||||||
|
|
||||||
|
|
||||||
|
export BUILD_DATE := ${shell date -Iminutes}
|
||||||
|
BUILD_ID := ${shell date +'%y%m%d%H%M'}
|
||||||
|
VERSION := $(shell cat $(SRC)/.version)
|
||||||
|
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||||
|
|
||||||
|
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||||
|
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||||
|
|
||||||
|
build: local_add_on
|
||||||
|
|
||||||
|
dev debug: local_add_on
|
||||||
|
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||||
|
export VERSION=$(VERSION)-$@-$(BUILD_ID) && \
|
||||||
|
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||||
|
docker buildx bake -f docker-bake.hcl $@
|
||||||
|
|
||||||
|
rc rel: local_add_on
|
||||||
|
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||||
|
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||||
|
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||||
|
export VERSION=$(VERSION)-$@ && \
|
||||||
|
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||||
|
docker buildx bake -f docker-bake.hcl $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -r -f $(DST_PROXY)
|
||||||
|
rm -f $(DST)/requirements.txt
|
||||||
|
rm -f $(ADDON_PATH)/config.yaml
|
||||||
|
rm -f $(TEMPL)/.data.json
|
||||||
|
docker logout ghcr.io
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Build the local add-on with a rootfs and config.yaml
|
||||||
|
# The rootfs is needed to build the add-on Docker container
|
||||||
|
#
|
||||||
|
local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt
|
||||||
|
|
||||||
|
# collect source files
|
||||||
|
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||||
|
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||||
|
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||||
|
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
|
||||||
|
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||||
|
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||||
|
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||||
|
|
||||||
|
# determine destination files
|
||||||
|
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||||
|
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||||
|
|
||||||
|
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
|
||||||
|
|
||||||
|
$(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) -D BuildID=$(BUILD_ID) --format=json $^ -o $@
|
||||||
|
|
||||||
|
$(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json
|
||||||
|
$(JINJA) --strict --format=json $^ -o $@
|
||||||
|
|
||||||
|
# build a common data.json file from STAGE depending source files
|
||||||
|
# don't touch the destination if the checksum of src and dst is equal
|
||||||
|
$(TEMPL)/.data.json: FORCE
|
||||||
|
rsync --checksum $(TEMPL)/$(STAGE)_data.json $@
|
||||||
|
|
||||||
|
FORCE : ;
|
||||||
|
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Build repository for Home Assistant Add-Onx
|
||||||
|
#
|
||||||
|
|
||||||
|
repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml rootfs/run.sh
|
||||||
|
repro_root = CHANGELOG.md LICENSE.md
|
||||||
|
repro_templates = config.yaml
|
||||||
|
repro_apparmor = apparmor.txt
|
||||||
|
repro_subdirs = translations rootfs
|
||||||
|
repro_vers = debug dev rc rel
|
||||||
|
|
||||||
|
repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||||
|
repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||||
|
repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||||
|
repro_all_apparmor := $(foreach dir,$(repro_vers), $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||||
|
repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||||
|
|
||||||
|
debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \
|
||||||
|
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \
|
||||||
|
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_debug/$(file)) \
|
||||||
|
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_debug/$(file)) \
|
||||||
|
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_debug/$(file))
|
||||||
|
|
||||||
|
dev: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_dev/$(file)) \
|
||||||
|
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_dev/$(file)) \
|
||||||
|
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_dev/$(file)) \
|
||||||
|
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_dev/$(file)) \
|
||||||
|
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_dev/$(file))
|
||||||
|
|
||||||
|
rc: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rc/$(file)) \
|
||||||
|
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rc/$(file)) \
|
||||||
|
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rc/$(file)) \
|
||||||
|
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rc/$(file)) \
|
||||||
|
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rc/$(file))
|
||||||
|
|
||||||
|
rel: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rel/$(file)) \
|
||||||
|
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rel/$(file)) \
|
||||||
|
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rel/$(file)) \
|
||||||
|
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rel/$(file)) \
|
||||||
|
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rel/$(file))
|
||||||
|
|
||||||
|
$(repro_all_subdirs) :
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version FORCE
|
||||||
|
$(JINJA) --strict -D AppVersion=$(VERSION)-$* -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@
|
||||||
|
|
||||||
|
$(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json
|
||||||
|
$(JINJA) --strict $< $(filter %.json,$^) -o $@
|
||||||
|
|
||||||
|
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../%
|
||||||
|
cp $< $@
|
||||||
|
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../%
|
||||||
|
cp $< $@
|
||||||
|
$(filter $(INST_BASE)/ha_addon_rc/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rc/% : ../%
|
||||||
|
cp $< $@
|
||||||
|
$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rel/% : ../%
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
|
||||||
|
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_debug/% : ha_addon/%
|
||||||
|
cp $< $@
|
||||||
|
$(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
@@ -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", "${IMAGE}:${VERSION}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "dev" {
|
||||||
|
inherits = ["_common"]
|
||||||
|
tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
177
ha_addons/ha_addon/DOCS.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 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: inv_1
|
||||||
|
suggested_area: Roof
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Example add-on configuration for GEN3PLUS energie storages:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
batteries:
|
||||||
|
- serial: 4100000000000000
|
||||||
|
monitor_sn: 2300000000
|
||||||
|
node_id: bat_1
|
||||||
|
suggested_area: Garage
|
||||||
|
modbus_polling: false
|
||||||
|
pv1.manufacturer: Shinefar
|
||||||
|
pv1.type: SF-M18/144550
|
||||||
|
pv2.manufacturer: Shinefar
|
||||||
|
pv2.type: SF-M18/144550
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: _This is just an example, you need to replace the values with your own!_
|
||||||
|
|
||||||
|
more information about the configuration can be found in the [configuration details page][configdetails].
|
||||||
|
|
||||||
|
## 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
|
||||||
92
ha_addons/ha_addon/Dockerfile
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
|
||||||
|
############################################################################
|
||||||
|
#
|
||||||
|
# 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.2.2"
|
||||||
|
# hadolint ignore=DL3006
|
||||||
|
FROM $BUILD_FROM AS base
|
||||||
|
|
||||||
|
# Installiere Python, pip und virtuelle Umgebungstools
|
||||||
|
RUN apk add --no-cache python3=3.12.9-r0 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
|
||||||
|
ARG LOG_LVL=INFO
|
||||||
|
ENV LOG_LVL=$LOG_LVL
|
||||||
|
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
|
After Width: | Height: | Size: 113 KiB |
BIN
ha_addons/ha_addon/logo.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
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
|
||||||
108
ha_addons/ha_addon/translations/de.yaml
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
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` oder `R47` und die der GEN3PLUS
|
||||||
|
Wechselrichter mit `Y17`oder `Y47`!
|
||||||
|
|
||||||
|
Siehe Beispielkonfiguration im Dokumentations-Tab
|
||||||
|
batteries:
|
||||||
|
name: Batterien
|
||||||
|
description: >+
|
||||||
|
Für jeden Energiespeicher muss die Seriennummer des Speichers einer MQTT
|
||||||
|
Definition zugeordnet werden. Dazu wird der entsprechende Konfigurationsblock mit der
|
||||||
|
16-stellige Seriennummer gestartet, so dass alle nachfolgenden Parameter diesem
|
||||||
|
Speicher zugeordnet sind.
|
||||||
|
Weitere speicherspezifische Parameter (z.B. Polling Mode) können im
|
||||||
|
Konfigurationsblock gesetzt werden.
|
||||||
|
|
||||||
|
Die Seriennummer der GEN3PLUS Batteriespeicher beginnen mit `410`!
|
||||||
|
|
||||||
|
Siehe Beispielkonfiguration im Dokumentations-Tab
|
||||||
|
|
||||||
|
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/TSUN Cloud - nur für GEN3PLUS Wechselrichter
|
||||||
|
description: >+
|
||||||
|
Schaltet die Verbindung zur Solarman oder TSUN Cloud ein/aus.
|
||||||
|
Diese Verbindung ist erforderlich, wenn Sie Daten an die Cloud senden möchten,
|
||||||
|
z.B. um die Solarman App oder TSUN Smart App zu nutzen oder Firmware-Updates zu erhalten.
|
||||||
|
|
||||||
|
ein => normaler Proxy-Betrieb.
|
||||||
|
aus => Die GEN3PLUS Geräte werden 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
|
||||||
109
ha_addons/ha_addon/translations/en.yaml
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
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` or `R47` and that of the GEN3PLUS
|
||||||
|
inverters with ‘Y17’ or ‘Y47’!
|
||||||
|
|
||||||
|
For reference see example configuration in Documentation Tab
|
||||||
|
|
||||||
|
batteries:
|
||||||
|
name: Energy Storages
|
||||||
|
description: >+
|
||||||
|
For each energy storage device, the serial number of the storage device must be
|
||||||
|
assigned to an MQTT definition. To do this, the corresponding configuration block
|
||||||
|
is started with the 16-digit serial number so that all subsequent parameters are
|
||||||
|
assigned to this energy storage. Further inverter-specific parameters (e.g. polling
|
||||||
|
mode) can be set in the configuration block.
|
||||||
|
|
||||||
|
The serial numbers of all GEN3PLUS energy storages start with ‘410’!
|
||||||
|
|
||||||
|
For reference see example configuration in Documentation Tab
|
||||||
|
|
||||||
|
|
||||||
|
tsun.enabled:
|
||||||
|
name: Connection to TSUN Cloud - for GEN3 inverter only
|
||||||
|
description: >+
|
||||||
|
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/TSUN Cloud - for GEN3PLUS inverter only
|
||||||
|
description: >+
|
||||||
|
switch on/off connection to the Solarman or TSUN cloud.
|
||||||
|
This connection is only required if you want send data to the cloud
|
||||||
|
eg. to use the Solarman APP, the TSUN Smart APP or receive firmware updates.
|
||||||
|
|
||||||
|
on => normal proxy operation.
|
||||||
|
off => The GEN3PLUS devices 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
@@ -0,0 +1,3 @@
|
|||||||
|
name: TSUN-Proxy
|
||||||
|
url: https://github.com/s-allius/tsun-gen3-proxy/ha_addons
|
||||||
|
maintainer: Stefan Allius
|
||||||
52
ha_addons/templates/apparmor.jinja
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile {{slug}} flags=(attach_disconnected,mediate_deleted) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
file,
|
||||||
|
signal (send) set=(kill,term,int,hup,cont),
|
||||||
|
|
||||||
|
# S6-Overlay
|
||||||
|
/init ix,
|
||||||
|
/bin/** ix,
|
||||||
|
/usr/bin/** ix,
|
||||||
|
/run/{s6,s6-rc*,service}/** ix,
|
||||||
|
/package/** ix,
|
||||||
|
/command/** ix,
|
||||||
|
/etc/services.d/** rwix,
|
||||||
|
/etc/cont-init.d/** rwix,
|
||||||
|
/etc/cont-finish.d/** rwix,
|
||||||
|
/run/{,**} rwk,
|
||||||
|
/dev/tty rw,
|
||||||
|
|
||||||
|
# Bashio
|
||||||
|
/usr/lib/bashio/** ix,
|
||||||
|
/tmp/** rwk,
|
||||||
|
|
||||||
|
# Access to options.json and other files within your addon
|
||||||
|
/data/** rw,
|
||||||
|
|
||||||
|
# Start new profile for service
|
||||||
|
/usr/bin/myprogram cx -> myprogram,
|
||||||
|
|
||||||
|
profile myprogram flags=(attach_disconnected,mediate_deleted) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
|
||||||
|
# Receive signals from S6-Overlay
|
||||||
|
signal (receive) peer=*_{{slug}},
|
||||||
|
|
||||||
|
# Access to options.json and other files within your addon
|
||||||
|
/data/** rw,
|
||||||
|
|
||||||
|
# Access to mapped volumes specified in config.json
|
||||||
|
/share/** rw,
|
||||||
|
|
||||||
|
# Access required for service functionality
|
||||||
|
/usr/bin/myprogram r,
|
||||||
|
/bin/bash rix,
|
||||||
|
/bin/echo ix,
|
||||||
|
/etc/passwd r,
|
||||||
|
/dev/tty rw,
|
||||||
|
}
|
||||||
|
}
|
||||||
125
ha_addons/templates/config.jinja
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
name: {{name}}
|
||||||
|
description: {{description}}
|
||||||
|
version: {% if version is defined and version|length %} {{version}} {% elif BuildID is defined and BuildID|length %} {{AppVersion}}-{{BuildID}} {% else %} {{AppVersion}} {% endif %}
|
||||||
|
image: {{image}}
|
||||||
|
url: https://github.com/s-allius/tsun-gen3-proxy
|
||||||
|
slug: {{slug}}
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
schema:
|
||||||
|
inverters:
|
||||||
|
- serial: match(^(R17|R47|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?
|
||||||
|
modbus_scanning.start: int(0,65535)?
|
||||||
|
modbus_scanning.step: int(0,65535)?
|
||||||
|
modbus_scanning.bytes: int(1,80)?
|
||||||
|
pv1.manufacturer: str?
|
||||||
|
pv1.type: str?
|
||||||
|
pv2.manufacturer: str?
|
||||||
|
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
|
||||||
|
batteries:
|
||||||
|
- serial: match(^(410).{13}$)
|
||||||
|
monitor_sn: int
|
||||||
|
node_id: str
|
||||||
|
suggested_area: str
|
||||||
|
modbus_polling: bool
|
||||||
|
client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)?
|
||||||
|
client_mode.port: port?
|
||||||
|
client_mode.forward: bool?
|
||||||
|
pv1.manufacturer: str?
|
||||||
|
pv1.type: str?
|
||||||
|
pv2.manufacturer: str?
|
||||||
|
pv2.type: str?
|
||||||
|
|
||||||
|
# optionale parameter
|
||||||
|
|
||||||
|
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: R17E000000000000
|
||||||
|
monitor_sn: 0
|
||||||
|
node_id: inv_1
|
||||||
|
suggested_area: Roof
|
||||||
|
modbus_polling: false
|
||||||
|
pv1.manufacturer: Shinefar
|
||||||
|
pv1.type: SF-M18/144550
|
||||||
|
pv2.manufacturer: Shinefar
|
||||||
|
pv2.type: SF-M18/144550
|
||||||
|
batteries:
|
||||||
|
- serial: 4100000000000000
|
||||||
|
monitor_sn: 0
|
||||||
|
node_id: bat_1
|
||||||
|
suggested_area: Garage
|
||||||
|
modbus_polling: false
|
||||||
|
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+"]
|
||||||
9
ha_addons/templates/debug_data.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"name": "TSUN-Proxy (Debug)",
|
||||||
|
"description": "MQTT Proxy for TSUN Photovoltaic Inverters with Debug Logging",
|
||||||
|
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||||
|
"slug": "tsun-proxy-debug",
|
||||||
|
"advanced": true,
|
||||||
|
"stage": "experimental"
|
||||||
|
}
|
||||||
9
ha_addons/templates/dev_data.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"name": "TSUN-Proxy (Dev)",
|
||||||
|
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||||
|
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||||
|
"slug": "tsun-proxy-dev",
|
||||||
|
"advanced": false,
|
||||||
|
"stage": "experimental"
|
||||||
|
}
|
||||||
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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -15,6 +15,10 @@ sonar.sources=app/src/
|
|||||||
sonar.python.version=3.12
|
sonar.python.version=3.12
|
||||||
sonar.tests=system_tests/,app/tests/
|
sonar.tests=system_tests/,app/tests/
|
||||||
sonar.exclusions=**/.vscode/**/*
|
sonar.exclusions=**/.vscode/**/*
|
||||||
|
|
||||||
|
# disable code dupication check for config grammar
|
||||||
|
sonar.cpd.exclusions=app/src/cnf/config.py
|
||||||
|
|
||||||
# Name your criteria
|
# Name your criteria
|
||||||
sonar.issue.ignore.multicriteria=e1,e2
|
sonar.issue.ignore.multicriteria=e1,e2
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,155 @@ def msg_inverter_ind(): # Data indication from the inverter
|
|||||||
msg += b'\x53\x00\x00'
|
msg += b'\x53\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def msg_inverter_ind2(): # Data indication from the inverter
|
||||||
|
msg = b'\x00\x00\x08\xff\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x00\x10'+get_inv_no()
|
||||||
|
msg += b'\x01\x00\x00\x01'
|
||||||
|
msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
|
||||||
|
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
|
||||||
|
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
|
||||||
|
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
|
||||||
|
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
|
||||||
|
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
|
||||||
|
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
|
||||||
|
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
|
||||||
|
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
|
||||||
|
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
|
||||||
|
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
|
||||||
|
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
|
||||||
|
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
|
||||||
|
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
|
||||||
|
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
|
||||||
|
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
|
||||||
|
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
|
||||||
|
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
|
||||||
|
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
|
||||||
|
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
|
||||||
|
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
|
||||||
|
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
|
||||||
|
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
|
||||||
|
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
|
||||||
|
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
|
||||||
|
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
|
||||||
|
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
|
||||||
|
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
|
||||||
|
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
|
||||||
|
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
|
||||||
|
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
|
||||||
|
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
|
||||||
|
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
|
||||||
|
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
|
||||||
|
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
|
||||||
|
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
|
||||||
|
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
|
||||||
|
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
|
||||||
|
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
|
||||||
|
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
|
||||||
|
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
|
||||||
|
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
|
||||||
|
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
|
||||||
|
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
|
||||||
|
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
|
||||||
|
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
|
||||||
|
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
|
||||||
|
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
|
||||||
|
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
|
||||||
|
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
|
||||||
|
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
|
||||||
|
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
|
||||||
|
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
|
||||||
|
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
|
||||||
|
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
|
||||||
|
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
|
||||||
|
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
|
||||||
|
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
|
||||||
|
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
|
||||||
|
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
|
||||||
|
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
|
||||||
|
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
|
||||||
|
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
|
||||||
|
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
|
||||||
|
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
|
||||||
|
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
|
||||||
|
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
|
||||||
|
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
|
||||||
|
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
|
||||||
|
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
|
||||||
|
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
|
||||||
|
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
|
||||||
|
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
|
||||||
|
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
|
||||||
|
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
|
||||||
|
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
|
||||||
|
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
|
||||||
|
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
|
||||||
|
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
|
||||||
|
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
|
||||||
|
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
|
||||||
|
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
|
||||||
|
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
|
||||||
|
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
|
||||||
|
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
|
||||||
|
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
|
||||||
|
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
|
||||||
|
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
|
||||||
|
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
|
||||||
|
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
|
||||||
|
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
|
||||||
|
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
|
||||||
|
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
|
||||||
|
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
|
||||||
|
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
|
||||||
|
msg += b'\x53\x00\x66' # | S.f'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_ota_update_req(): # Over the air update request from talent cloud
|
def msg_ota_update_req(): # Over the air update request from talent cloud
|
||||||
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
|
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
|
||||||
@@ -234,3 +383,18 @@ def test_ota_req(client_connection, msg_ota_update_req):
|
|||||||
_ = s.recv(1024)
|
_ = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_send_inv_data2(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inverter_ind2):
|
||||||
|
s = client_connection
|
||||||
|
try:
|
||||||
|
s.sendall(msg_timestamp_req)
|
||||||
|
_ = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(32.5)
|
||||||
|
# assert data == msg_timestamp_resp
|
||||||
|
try:
|
||||||
|
s.sendall(msg_inverter_ind2)
|
||||||
|
_ = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
|||||||
def get_sn() -> bytes:
|
def get_sn() -> bytes:
|
||||||
return bytes.fromhex(SOLARMAN_SNR)
|
return bytes.fromhex(SOLARMAN_SNR)
|
||||||
|
|
||||||
|
def get_dcu_sn() -> bytes:
|
||||||
|
return b'\x20\x43\x65\x7b'
|
||||||
|
|
||||||
|
def get_dcu_no() -> bytes:
|
||||||
|
return b'4100000000000001'
|
||||||
|
|
||||||
def get_inv_no() -> bytes:
|
def get_inv_no() -> bytes:
|
||||||
return b'T170000000000001'
|
return b'T170000000000001'
|
||||||
|
|
||||||
@@ -105,6 +111,62 @@ def MsgInvalidInfo(): # Contact Info message wrong start byte
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_dev_ind_msg(): # 0x4110
|
||||||
|
msg = b'\xa5\x3a\x01\x10\x41\x00\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32'
|
||||||
|
msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35'
|
||||||
|
msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26'
|
||||||
|
msg += b'\x30\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x7a\x75\x68\x61\x75\x73\x65\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x08\x01\x01\x01\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_dev_rsp_msg(): # 0x1110
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01\x4a\xf6\xa6'
|
||||||
|
msg += b'\x67\x3c\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_data_ind_msg(): # 0x4210
|
||||||
|
msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde'
|
||||||
|
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
|
||||||
|
msg += get_dcu_no()
|
||||||
|
msg += b'\x0d\x3a\x00\x0a\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
|
||||||
|
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
|
||||||
|
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dcu_data_rsp_msg(): # 0x1210
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01\xd1\x96\x04'
|
||||||
|
msg += b'\x66\x3c\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def ClientConnection():
|
def ClientConnection():
|
||||||
@@ -181,4 +243,24 @@ def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactR
|
|||||||
# time.sleep(2.5)
|
# time.sleep(2.5)
|
||||||
checkResponse(data, MsgContactResp)
|
checkResponse(data, MsgContactResp)
|
||||||
|
|
||||||
|
def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
|
||||||
|
s = ClientConnection
|
||||||
|
try:
|
||||||
|
s.sendall(dcu_dev_ind_msg)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
checkResponse(data, dcu_dev_rsp_msg)
|
||||||
|
|
||||||
|
def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg):
|
||||||
|
s = ClientConnection
|
||||||
|
try:
|
||||||
|
s.sendall(dcu_data_ind_msg)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
checkResponse(data, dcu_data_rsp_msg)
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../wiki"
|
"path": "../tsun-gen3-proxy.wiki"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ha-addons",
|
||||||
|
"path": "../ha-addons"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {}
|
||||||
|
|||||||