Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42ba8a8c6 | ||
|
|
f3e69ff217 | ||
|
|
a3c054d2b1 | ||
|
|
c34b33ed5f | ||
|
|
0a18918326 | ||
|
|
aa3bb4a1fa | ||
|
|
a62864218d | ||
|
|
0b2631c162 | ||
|
|
c59bd16664 | ||
|
|
039a021cda | ||
|
|
49e2dfbd86 | ||
|
|
e6ecf5911b | ||
|
|
6e1ed5d1e7 | ||
|
|
ad885e9644 | ||
|
|
8f81ceda98 | ||
|
|
8204cae2b1 | ||
|
|
8baa68e615 | ||
|
|
56f36e9f3f | ||
|
|
5b60d5dae1 | ||
|
|
c1c38ab5c7 | ||
|
|
ec4261ae84 | ||
|
|
be57d11214 | ||
|
|
685c2dc07b | ||
|
|
d27fe09006 | ||
|
|
e850a8c534 | ||
|
|
33f215def2 | ||
|
|
4be726166e | ||
|
|
20f4fd647c | ||
|
|
407c1ceb2b | ||
|
|
c6eecb4791 | ||
|
|
87d59d046f | ||
|
|
063850c7fb | ||
|
|
17c33601a0 | ||
|
|
3980ac013b | ||
|
|
66657888dd | ||
|
|
ab9e798152 | ||
|
|
fdf3475909 | ||
|
|
edc2c12b5b | ||
|
|
5c6f9e7414 | ||
|
|
0fc74b0d19 | ||
|
|
87cc3fb205 | ||
|
|
8fc5eb3670 | ||
|
|
55fc834a1e | ||
|
|
da2388941e | ||
|
|
9e38cb93ea | ||
|
|
de1c48fa62 | ||
|
|
e432441134 | ||
|
|
98ef252bb0 | ||
|
|
25e3db36c4 | ||
|
|
3ac48dad1f | ||
|
|
eff3e7558b | ||
|
|
6ef6f4cd34 | ||
|
|
177706c3e6 | ||
|
|
9ac1f6f46d | ||
|
|
3cc5f3ec53 | ||
|
|
23ff2bb05c | ||
|
|
c761446c11 | ||
|
|
f30aa07431 | ||
|
|
476c5f0006 | ||
|
|
282a459ef0 | ||
|
|
d25173e591 | ||
|
|
9c39ea27f7 | ||
|
|
766774224b | ||
|
|
f4da16987f | ||
|
|
841877305d | ||
|
|
fb5c6a74cf | ||
|
|
14425da5fa | ||
|
|
6877465915 | ||
|
|
2e214b1e71 | ||
|
|
036af8e127 | ||
|
|
92469456b7 | ||
|
|
1658036a26 | ||
|
|
1ae7784bee | ||
|
|
e43a02c508 | ||
|
|
4ea70dee64 | ||
|
|
6fcf4f47c2 | ||
|
|
73baffe9e0 | ||
|
|
3fda08bd25 | ||
|
|
0e7fbc7820 | ||
|
|
26f108cc51 | ||
|
|
dd438bf201 | ||
|
|
f48596a512 | ||
|
|
6a64484174 | ||
|
|
def5702415 | ||
|
|
b3f0fc97d7 | ||
|
|
65973b2835 | ||
|
|
b240b74994 | ||
|
|
93e82a2284 | ||
|
|
537d81fa19 | ||
|
|
5fe455e42f | ||
|
|
5a0456650f | ||
|
|
41d9a2a1ef | ||
|
|
a869ead89a | ||
|
|
91873d0c34 | ||
|
|
c4b3e1a817 | ||
|
|
0ac4b1f571 | ||
|
|
2ec0a59cd3 | ||
|
|
2d176894d3 | ||
|
|
0ae6dffc6b | ||
|
|
5fc1b16627 | ||
|
|
eab109ddab | ||
|
|
1b6bee12de | ||
|
|
2301511242 | ||
|
|
3fd528bdbe | ||
|
|
e15387b1ff | ||
|
|
02d9f01947 | ||
|
|
39beb0cb44 | ||
|
|
d5010fe053 | ||
|
|
54d2bf4439 | ||
|
|
f804b755a4 | ||
|
|
bf0f152d5a | ||
|
|
29ee540a19 | ||
|
|
5822f5de50 | ||
|
|
283ae31af2 | ||
|
|
808bf2fe87 | ||
|
|
fa2626ec7a | ||
|
|
eda8ef1db6 | ||
|
|
3dbcee63f6 | ||
|
|
f2c4230a49 | ||
|
|
763af8b4cf | ||
|
|
a2f67e7d3e | ||
|
|
f78d4ac310 | ||
|
|
fdedfcbf8e | ||
|
|
494c30e489 | ||
|
|
30dc802fb2 | ||
|
|
5fdad484f4 | ||
|
|
dba3b458ba | ||
|
|
1d9cbf314e | ||
|
|
58c3333fcc | ||
|
|
530687039d | ||
|
|
5d0c95d6e6 | ||
|
|
e603bb9baa | ||
|
|
e8902f7923 | ||
|
|
b1e577d357 | ||
|
|
4e8fd8e2a2 | ||
|
|
d34862260e | ||
|
|
c061d263eb | ||
|
|
ccc7e7959e | ||
|
|
7b4ed406a1 | ||
|
|
549fca8ae5 | ||
|
|
f73376b330 | ||
|
|
220f2cce18 | ||
|
|
e2a5c7e640 | ||
|
|
2e64ae5884 | ||
|
|
95ebb92f05 | ||
|
|
59dabbfa4a | ||
|
|
aa0d432149 | ||
|
|
6dbf259e44 |
8
.github/workflows/python-app.yml
vendored
8
.github/workflows/python-app.yml
vendored
@@ -5,7 +5,7 @@ name: Python application
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main", "dev-*" ]
|
branches: [ "main", "dev-*", "*/issue*" ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md' # Do no build on *.md changes
|
- '**.md' # Do no build on *.md changes
|
||||||
- '**.yml' # Do no build on *.yml changes
|
- '**.yml' # Do no build on *.yml changes
|
||||||
@@ -29,15 +29,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install flake8 pytest
|
pip install flake8 pytest pytest-asyncio
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: Aktuelle Datei",
|
"name": "Python: Aktuelle Datei",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -5,7 +5,65 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
## [0.9.0] - 2024-07-01
|
||||||
|
|
||||||
|
- fix exception in MODBUS timeout callback
|
||||||
|
|
||||||
|
## [0.9.0-RC1] - 2024-06-29
|
||||||
|
|
||||||
|
- add asyncio log and debug mode
|
||||||
|
- stop the HTTP server on shutdown gracefully
|
||||||
|
- Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to
|
||||||
|
unexpected packets. [#111](https://github.com/s-allius/tsun-gen3-proxy/issues/111)
|
||||||
|
- GEN3: avoid sending MODBUS commands to the inverter during the inverter's reporting phase
|
||||||
|
- GEN3: determine the connection timeout based on the connection state
|
||||||
|
- GEN3: support more data encodings for DSP version V5.0.17 [#108](https://github.com/s-allius/tsun-gen3-proxy/issues/108)
|
||||||
|
- detect dead connections [#100](https://github.com/s-allius/tsun-gen3-proxy/issues/100)
|
||||||
|
- improve connection logging wirt a unique connection id
|
||||||
|
- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91)
|
||||||
|
- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93)
|
||||||
|
- add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94)
|
||||||
|
- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)
|
||||||
|
- GEN3PLUS: dump invalid packages with wrong start or stop byte
|
||||||
|
- label debug imagages als `debug`
|
||||||
|
- print imgae build time during proxy start
|
||||||
|
- add type annotations
|
||||||
|
- improve async unit test and fix pytest warnings
|
||||||
|
- run github tests even for pulls on issue branches
|
||||||
|
|
||||||
|
## [0.8.1] - 2024-06-21
|
||||||
|
|
||||||
|
- Fix MODBUS responses are dropped and not forwarded to the TSUN cloud [#104](https://github.com/s-allius/tsun-gen3-proxy/issues/104)
|
||||||
|
- GEN3: Fix connections losts due MODBUS requests [#102](https://github.com/s-allius/tsun-gen3-proxy/issues/102)
|
||||||
|
|
||||||
|
## [0.8.0] - 2024-06-07
|
||||||
|
|
||||||
|
- improve logging: add protocol or node_id to connection logs
|
||||||
|
- improve logging: log ignored AT+ or MODBUS commands
|
||||||
|
- improve tracelog: log level depends on message type and source
|
||||||
|
- fix typo in docker-compose.yaml and remove the external network definition
|
||||||
|
- trace heartbeat and regular modbus pakets witl log level DEBUG
|
||||||
|
- GEN3PLUS: don't forward ack paket from tsun to the inverter
|
||||||
|
- GEN3PLUS: add allow and block filter for AT+ commands
|
||||||
|
- catch all OSError errors in the read loop
|
||||||
|
- log Modbus traces with different log levels
|
||||||
|
- add Modbus fifo and timeout handler
|
||||||
|
- build version string in the same format as TSUN for GEN3 inverters
|
||||||
|
- add graceful shutdown
|
||||||
|
- parse Modbus values and store them in the database
|
||||||
|
- add cron task to request the output power every minute
|
||||||
|
- GEN3PLUS: add MQTT topics to send AT commands to the inverter
|
||||||
|
- add MQTT topics to send Modbus commands to the inverter
|
||||||
|
- convert data collect interval to minutes
|
||||||
|
- add postfix for rc and dev versions to the version number
|
||||||
|
- change logging level to DEBUG for some logs
|
||||||
|
- remove experimental value Register.VALUE_1
|
||||||
|
- format Register.POWER_ON_TIME as integer
|
||||||
|
- ignore catch-up values from the inverters for now
|
||||||
|
|
||||||
|
## [0.7.0] - 2024-04-20
|
||||||
|
|
||||||
- GEN3PLUS: fix temperature values
|
- GEN3PLUS: fix temperature values
|
||||||
- GEN3PLUS: read corect firmware and logger version
|
- GEN3PLUS: read corect firmware and logger version
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -39,12 +39,17 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600
|
- Supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600
|
||||||
- supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300
|
- Supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300
|
||||||
- `MQTT` support
|
- `MQTT` support
|
||||||
- `Home-Assistant` auto-discovery support
|
- `Home-Assistant` auto-discovery support
|
||||||
|
- `MODBUS` support via MQTT topics
|
||||||
|
- `AT-Command` support via MQTT topics (GEN3PLUS only)
|
||||||
|
- Faster DataUp interval sends measurement data to the MQTT broker every minute
|
||||||
- Self-sufficient island operation without internet
|
- Self-sufficient island operation without internet
|
||||||
- runs in a non-root Docker Container
|
- Security-Features:
|
||||||
|
- control access via `AT-commands`
|
||||||
|
- Runs in a non-root Docker Container
|
||||||
|
|
||||||
## Home Assistant Screenshots
|
## Home Assistant Screenshots
|
||||||
|
|
||||||
@@ -156,7 +161,7 @@ suggested_area = 'balcony' # Optional, suggested installation area for home-a
|
|||||||
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
[inverters."Y17xxxxxxxxxxxx1"]
|
[inverters."Y17xxxxxxxxxxxx1"] # This block is also for inverters with a Y47 serial no
|
||||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||||
suggested_area = 'garage' # suggested installation place for home-assistant
|
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||||
@@ -165,6 +170,12 @@ pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de
|
|||||||
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
|
[gen3plus.at_acl]
|
||||||
|
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access
|
||||||
|
tsun.block = []
|
||||||
|
mqtt.allow = ['AT+'] # allow all via mqtt
|
||||||
|
mqtt.block = []
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inverter Configuration
|
## Inverter Configuration
|
||||||
@@ -215,7 +226,7 @@ In the following table you will find an overview of which inverter model has bee
|
|||||||
A combination with a red question mark should work, but I have not checked it in detail.
|
A combination with a red question mark should work, but I have not checked it in detail.
|
||||||
|
|
||||||
<table align="center">
|
<table align="center">
|
||||||
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 1.1.00.0B</th></tr>
|
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 4.0.10</th></tr>
|
||||||
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">➖</td></tr>
|
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">➖</td></tr>
|
||||||
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">➖</td></tr>
|
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">➖</td></tr>
|
||||||
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center">➖</td><td align="center">➖</td><td align="center">➖</td><td align="center">✔️</td></tr>
|
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center">➖</td><td align="center">➖</td><td align="center">➖</td><td align="center">✔️</td></tr>
|
||||||
@@ -230,7 +241,7 @@ Legend
|
|||||||
🚧: Proxy support in preparation
|
🚧: Proxy support in preparation
|
||||||
```
|
```
|
||||||
|
|
||||||
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E`
|
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` or `Y47E` instead of `R17E`
|
||||||
|
|
||||||
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RUN apk upgrade --no-cache && \
|
|||||||
|
|
||||||
#
|
#
|
||||||
# second stage for building wheels packages
|
# second stage for building wheels packages
|
||||||
FROM base as builder
|
FROM base AS builder
|
||||||
|
|
||||||
# copy the dependencies file to the root dir and install requirements
|
# copy the dependencies file to the root dir and install requirements
|
||||||
COPY ./requirements.txt /root/
|
COPY ./requirements.txt /root/
|
||||||
@@ -26,7 +26,7 @@ RUN apk add --no-cache build-base && \
|
|||||||
|
|
||||||
#
|
#
|
||||||
# third stage for our runtime image
|
# third stage for our runtime image
|
||||||
FROM base as runtime
|
FROM base AS runtime
|
||||||
ARG SERVICE_NAME
|
ARG SERVICE_NAME
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG UID
|
ARG UID
|
||||||
@@ -63,8 +63,8 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
|||||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||||
COPY config .
|
COPY config .
|
||||||
COPY src .
|
COPY src .
|
||||||
|
RUN date > /build-date.txt
|
||||||
EXPOSE 5005
|
EXPOSE 5005 8127 10000
|
||||||
|
|
||||||
# command to run on container start
|
# command to run on container start
|
||||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||||
@@ -73,7 +73,7 @@ CMD [ "python3", "./server.py" ]
|
|||||||
|
|
||||||
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
|
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
|
||||||
LABEL org.opencontainers.image.authors="Stefan Allius"
|
LABEL org.opencontainers.image.authors="Stefan Allius"
|
||||||
LABEL org.opencontainers.image.source https://github.com/s-allius/tsun-gen3-proxy
|
LABEL org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy
|
||||||
LABEL org.opencontainers.image.description 'This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
LABEL org.opencontainers.image.description='This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
||||||
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
|
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
|
||||||
LABEL org.opencontainers.image.vendor="Stefan Allius"
|
LABEL org.opencontainers.image.vendor="Stefan Allius"
|
||||||
|
|||||||
35
app/build.sh
35
app/build.sh
@@ -4,7 +4,7 @@
|
|||||||
# rc: release candidate build
|
# rc: release candidate build
|
||||||
# rel: release build and push to ghcr.io
|
# rel: release build and push to ghcr.io
|
||||||
# Note: for release build, you need to set GHCR_TOKEN
|
# Note: for release build, you need to set GHCR_TOKEN
|
||||||
# export GHCR_TOKEN=<YOUR_GITHUB_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
|
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||||
|
|
||||||
|
|
||||||
@@ -18,27 +18,38 @@ arr=(${VERSION//./ })
|
|||||||
MAJOR=${arr[0]}
|
MAJOR=${arr[0]}
|
||||||
IMAGE=tsun-gen3-proxy
|
IMAGE=tsun-gen3-proxy
|
||||||
|
|
||||||
if [[ $1 == dev ]] || [[ $1 == rc ]] ;then
|
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
||||||
IMAGE=docker.io/sallius/${IMAGE}
|
IMAGE=docker.io/sallius/${IMAGE}
|
||||||
VERSION=${VERSION}
|
VERSION=${VERSION}-$1
|
||||||
elif [[ $1 == rel ]];then
|
elif [[ $1 == rc ]] || [[ $1 == rel ]];then
|
||||||
IMAGE=ghcr.io/s-allius/${IMAGE}
|
IMAGE=ghcr.io/s-allius/${IMAGE}
|
||||||
else
|
else
|
||||||
echo argument missing!
|
echo argument missing!
|
||||||
echo try: $0 '[dev|rc|rel]'
|
echo try: $0 '[debug|dev|rc|rel]'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||||
if [[ $1 == dev ]];then
|
if [[ $1 == debug ]];then
|
||||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest app
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:debug app
|
||||||
|
elif [[ $1 == dev ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||||
|
|
||||||
elif [[ $1 == rc ]];then
|
elif [[ $1 == rc ]];then
|
||||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest app
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
||||||
|
echo 'login to ghcr.io'
|
||||||
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:rc
|
||||||
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||||
|
|
||||||
elif [[ $1 == rel ]];then
|
elif [[ $1 == rel ]];then
|
||||||
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
||||||
echo 'login to ghcr.io'
|
echo 'login to ghcr.io'
|
||||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:latest
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
|
||||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo 'check docker-compose.yaml file'
|
||||||
|
docker-compose config -q
|
||||||
@@ -49,3 +49,8 @@ monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker e
|
|||||||
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
|
[gen3plus.at_acl]
|
||||||
|
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
|
||||||
|
tsun.block = []
|
||||||
|
mqtt.allow = ['AT+']
|
||||||
|
mqtt.block = []
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ user="$(id -u)"
|
|||||||
echo "######################################################"
|
echo "######################################################"
|
||||||
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
||||||
echo "# for running with UserID:$UID, GroupID:$GID"
|
echo "# for running with UserID:$UID, GroupID:$GID"
|
||||||
|
echo "# Image built: $(cat /build-date.txt) "
|
||||||
echo "#"
|
echo "#"
|
||||||
|
|
||||||
if [ "$user" = '0' ]; then
|
if [ "$user" = '0' ]; then
|
||||||
|
|||||||
@@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \
|
|||||||
-name od -o \
|
-name od -o \
|
||||||
-name strings -o \
|
-name strings -o \
|
||||||
-name su -o \
|
-name su -o \
|
||||||
-name wget -o \
|
|
||||||
\) -delete \
|
\) -delete \
|
||||||
; fi
|
; fi
|
||||||
|
|||||||
607
app/proxy.svg
607
app/proxy.svg
@@ -4,340 +4,381 @@
|
|||||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||||
-->
|
-->
|
||||||
<!-- Title: G Pages: 1 -->
|
<!-- Title: G Pages: 1 -->
|
||||||
<svg width="511pt" height="1204pt"
|
<svg width="691pt" height="1312pt"
|
||||||
viewBox="0.00 0.00 511.39 1204.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
viewBox="0.00 0.00 691.35 1312.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 1200)">
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1308)">
|
||||||
<title>G</title>
|
<title>G</title>
|
||||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1200 507.3928,-1200 507.3928,4 -4,4"/>
|
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1308 687.348,-1308 687.348,4 -4,4"/>
|
||||||
<!-- A0 -->
|
<!-- A0 -->
|
||||||
<g id="node1" class="node">
|
<g id="node1" class="node">
|
||||||
<title>A0</title>
|
<title>A0</title>
|
||||||
<polygon fill="#fff8dc" stroke="#000000" points="148.1964,-1100 39.8036,-1100 39.8036,-1064 154.1964,-1064 154.1964,-1094 148.1964,-1100"/>
|
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1208 .1516,-1208 .1516,-1172 114.5444,-1172 114.5444,-1202 108.5444,-1208"/>
|
||||||
<polyline fill="none" stroke="#000000" points="148.1964,-1100 148.1964,-1094 "/>
|
<polyline fill="none" stroke="#000000" points="108.5444,-1208 108.5444,-1202 "/>
|
||||||
<polyline fill="none" stroke="#000000" points="154.1964,-1094 148.1964,-1094 "/>
|
<polyline fill="none" stroke="#000000" points="114.5444,-1202 108.5444,-1202 "/>
|
||||||
<text text-anchor="middle" x="97" y="-1085" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
<text text-anchor="middle" x="57.348" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||||
<text text-anchor="middle" x="97" y="-1073" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
<text text-anchor="middle" x="57.348" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A1 -->
|
<!-- A1 -->
|
||||||
<g id="node2" class="node">
|
<g id="node2" class="node">
|
||||||
<title>A1</title>
|
<title>A1</title>
|
||||||
<polygon fill="none" stroke="#000000" points="95.6817,-804 26.3183,-804 26.3183,-768 95.6817,-768 95.6817,-804"/>
|
<polygon fill="none" stroke="#000000" points="657.0297,-906 587.6663,-906 587.6663,-870 657.0297,-870 657.0297,-906"/>
|
||||||
<text text-anchor="middle" x="61" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
<text text-anchor="middle" x="622.348" y="-885" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A2 -->
|
<!-- A2 -->
|
||||||
<g id="node3" class="node">
|
<g id="node3" class="node">
|
||||||
<title>A2</title>
|
<title>A2</title>
|
||||||
<polygon fill="none" stroke="#000000" points="0,-518 0,-550 122,-550 122,-518 0,-518"/>
|
<polygon fill="none" stroke="#000000" points="561.348,-608 561.348,-640 683.348,-640 683.348,-608 561.348,-608"/>
|
||||||
<text text-anchor="start" x="51.277" y="-531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
<text text-anchor="start" x="612.625" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||||
<polygon fill="none" stroke="#000000" points="0,-462 0,-518 122,-518 122,-462 0,-462"/>
|
<polygon fill="none" stroke="#000000" points="561.348,-552 561.348,-608 683.348,-608 683.348,-552 561.348,-552"/>
|
||||||
<text text-anchor="start" x="18.4875" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
<text text-anchor="start" x="579.8355" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||||
<text text-anchor="start" x="26.2665" y="-487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
<text text-anchor="start" x="587.6145" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||||
<text text-anchor="start" x="9.8735" y="-475" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
<text text-anchor="start" x="571.2215" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||||
<polygon fill="none" stroke="#000000" points="0,-418 0,-462 122,-462 122,-418 0,-418"/>
|
<polygon fill="none" stroke="#000000" points="561.348,-508 561.348,-552 683.348,-552 683.348,-508 561.348,-508"/>
|
||||||
<text text-anchor="start" x="22.936" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
<text text-anchor="start" x="584.284" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||||
<text text-anchor="start" x="27.1045" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
<text text-anchor="start" x="588.4525" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A1->A2 -->
|
<!-- A1->A2 -->
|
||||||
<g id="edge1" class="edge">
|
<g id="edge1" class="edge">
|
||||||
<title>A1->A2</title>
|
<title>A1->A2</title>
|
||||||
<path fill="none" stroke="#000000" d="M61,-757.4632C61,-710.3291 61,-615.0013 61,-550.3153"/>
|
<path fill="none" stroke="#000000" d="M622.348,-859.5395C622.348,-810.311 622.348,-708.0351 622.348,-640.2069"/>
|
||||||
<polygon fill="none" stroke="#000000" points="57.5001,-757.5631 61,-767.5632 64.5001,-757.5632 57.5001,-757.5631"/>
|
<polygon fill="none" stroke="#000000" points="618.8481,-859.7608 622.348,-869.7608 625.8481,-859.7608 618.8481,-859.7608"/>
|
||||||
</g>
|
|
||||||
<!-- A10 -->
|
|
||||||
<g id="node11" class="node">
|
|
||||||
<title>A10</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="7,-282 7,-314 115,-314 115,-282 7,-282"/>
|
|
||||||
<text text-anchor="start" x="44.0535" y="-295" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="7,-190 7,-282 115,-282 115,-190 7,-190"/>
|
|
||||||
<text text-anchor="start" x="37.104" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
|
|
||||||
<text text-anchor="start" x="30.4405" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
|
|
||||||
<text text-anchor="start" x="21.2755" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
|
||||||
<text text-anchor="start" x="20.7115" y="-227" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
|
||||||
<text text-anchor="start" x="16.8225" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
|
||||||
<text text-anchor="start" x="32.6655" y="-203" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="7,-170 7,-190 115,-190 115,-170 7,-170"/>
|
|
||||||
</g>
|
|
||||||
<!-- A2->A10 -->
|
|
||||||
<g id="edge11" class="edge">
|
|
||||||
<title>A2->A10</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M61,-417.8724C61,-385.8251 61,-347.2624 61,-314.4235"/>
|
|
||||||
</g>
|
|
||||||
<!-- A3 -->
|
|
||||||
<g id="node4" class="node">
|
|
||||||
<title>A3</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="173,-1092 173,-1124 244,-1124 244,-1092 173,-1092"/>
|
|
||||||
<text text-anchor="start" x="182.945" y="-1105" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="173,-1072 173,-1092 244,-1092 244,-1072 173,-1072"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="173,-1040 173,-1072 244,-1072 244,-1040 173,-1040"/>
|
|
||||||
<text text-anchor="start" x="190.439" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
|
||||||
</g>
|
|
||||||
<!-- A4 -->
|
|
||||||
<g id="node5" class="node">
|
|
||||||
<title>A4</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="141,-886 141,-918 275,-918 275,-886 141,-886"/>
|
|
||||||
<text text-anchor="start" x="187.7175" y="-899" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="141,-722 141,-886 275,-886 275,-722 141,-722"/>
|
|
||||||
<text text-anchor="start" x="171.3265" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
|
||||||
<text text-anchor="start" x="168.543" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
|
||||||
<text text-anchor="start" x="161.314" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
|
||||||
<text text-anchor="start" x="167.148" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
|
||||||
<text text-anchor="start" x="186.3245" y="-819" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
|
||||||
<text text-anchor="start" x="190.2135" y="-807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
|
||||||
<text text-anchor="start" x="187.1585" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
|
||||||
<text text-anchor="start" x="157.989" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
|
||||||
<text text-anchor="start" x="156.5945" y="-771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
|
||||||
<text text-anchor="start" x="150.7665" y="-759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
|
||||||
<text text-anchor="start" x="190.2135" y="-747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
|
||||||
<text text-anchor="start" x="178.826" y="-735" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="141,-654 141,-722 275,-722 275,-654 141,-654"/>
|
|
||||||
<text text-anchor="start" x="157.7095" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
|
||||||
<text text-anchor="start" x="182.4445" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
|
||||||
<text text-anchor="start" x="168.2725" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
|
||||||
<text text-anchor="start" x="166.6025" y="-667" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
|
||||||
</g>
|
|
||||||
<!-- A3->A4 -->
|
|
||||||
<g id="edge2" class="edge">
|
|
||||||
<title>A3->A4</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M208,-1029.7414C208,-998.6043 208,-957.5621 208,-918.0536"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="204.5001,-1029.9047 208,-1039.9048 211.5001,-1029.9048 204.5001,-1029.9047"/>
|
|
||||||
</g>
|
|
||||||
<!-- A5 -->
|
|
||||||
<g id="node6" class="node">
|
|
||||||
<title>A5</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145,-566 145,-598 259,-598 259,-566 145,-566"/>
|
|
||||||
<text text-anchor="start" x="188.108" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145,-486 145,-566 259,-566 259,-486 145,-486"/>
|
|
||||||
<text text-anchor="start" x="154.763" y="-547" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
|
||||||
<text text-anchor="start" x="189.7775" y="-535" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
|
||||||
<text text-anchor="start" x="170.6" y="-523" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
|
||||||
<text text-anchor="start" x="173.94" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
|
||||||
<text text-anchor="start" x="188.112" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="145,-370 145,-486 259,-486 259,-370 145,-370"/>
|
|
||||||
<text text-anchor="start" x="159.4925" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
|
||||||
<text text-anchor="start" x="161.4325" y="-455" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
|
||||||
<text text-anchor="start" x="167.2765" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
|
||||||
<text text-anchor="start" x="155.3285" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
|
||||||
<text text-anchor="start" x="157.2735" y="-419" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
|
||||||
<text text-anchor="start" x="166.4405" y="-407" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
|
||||||
<text text-anchor="start" x="187.0025" y="-383" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A4->A5 -->
|
|
||||||
<g id="edge3" class="edge">
|
|
||||||
<title>A4->A5</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M205.1775,-643.9363C204.8732,-628.6188 204.5665,-613.1783 204.2698,-598.2481"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="201.679,-644.0493 205.377,-653.9777 208.6776,-643.9102 201.679,-644.0493"/>
|
|
||||||
</g>
|
|
||||||
<!-- A6 -->
|
|
||||||
<g id="node7" class="node">
|
|
||||||
<title>A6</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="412,-530 412,-562 503,-562 503,-530 412,-530"/>
|
|
||||||
<text text-anchor="start" x="429.995" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="412,-462 412,-530 503,-530 503,-462 412,-462"/>
|
|
||||||
<text text-anchor="start" x="442.498" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
|
||||||
<text text-anchor="start" x="445.5575" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
|
||||||
<text text-anchor="start" x="450.556" y="-487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
|
||||||
<text text-anchor="start" x="443.612" y="-475" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="412,-406 412,-462 503,-462 503,-406 412,-406"/>
|
|
||||||
<text text-anchor="start" x="421.9405" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
|
||||||
<text text-anchor="start" x="442.5025" y="-419" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A4->A6 -->
|
|
||||||
<g id="edge4" class="edge">
|
|
||||||
<title>A4->A6</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M281.898,-681.5056C290.821,-671.6784 300.2479,-662.304 310,-654 345.4324,-623.8293 370.2318,-638.0075 402,-604 413.2639,-591.9422 422.5424,-577.1747 430.0614,-562.1755"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="279.184,-679.2912 275.1758,-689.0986 284.4251,-683.9313 279.184,-679.2912"/>
|
|
||||||
</g>
|
|
||||||
<!-- A7 -->
|
|
||||||
<g id="node8" class="node">
|
|
||||||
<title>A7</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="133,-258 133,-290 283,-290 283,-258 133,-258"/>
|
|
||||||
<text text-anchor="start" x="176.0455" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="133,-226 133,-258 283,-258 283,-226 133,-226"/>
|
|
||||||
<text text-anchor="start" x="142.987" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="133,-194 133,-226 283,-226 283,-194 133,-194"/>
|
|
||||||
<text text-anchor="start" x="193.0025" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A5->A7 -->
|
|
||||||
<g id="edge5" class="edge">
|
|
||||||
<title>A5->A7</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M205.0858,-359.5407C205.6971,-334.8843 206.3036,-310.4196 206.8038,-290.2462"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="201.5821,-359.6485 204.8331,-369.7323 208.58,-359.8221 201.5821,-359.6485"/>
|
|
||||||
</g>
|
|
||||||
<!-- A8 -->
|
|
||||||
<g id="node9" class="node">
|
|
||||||
<title>A8</title>
|
|
||||||
<polygon fill="none" stroke="#000000" points="319,-258 319,-290 475,-290 475,-258 319,-258"/>
|
|
||||||
<text text-anchor="start" x="361.711" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="319,-226 319,-258 475,-258 475,-226 319,-226"/>
|
|
||||||
<text text-anchor="start" x="328.6525" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="319,-194 319,-226 475,-226 475,-194 319,-194"/>
|
|
||||||
<text text-anchor="start" x="382.0025" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
|
||||||
<!-- A6->A8 -->
|
|
||||||
<g id="edge6" class="edge">
|
|
||||||
<title>A6->A8</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M435.1335,-395.8051C426.2731,-360.0682 416.3888,-320.2015 408.9039,-290.0125"/>
|
|
||||||
<polygon fill="none" stroke="#000000" points="431.7989,-396.8999 437.6026,-405.7637 438.5932,-395.2153 431.7989,-396.8999"/>
|
|
||||||
</g>
|
|
||||||
<!-- A7->A7 -->
|
|
||||||
<g id="edge13" class="edge">
|
|
||||||
<title>A7->A7</title>
|
|
||||||
<path fill="none" stroke="#000000" d="M283.1684,-272.6238C293.8394,-267.6708 301,-257.4629 301,-242 301,-231.1277 297.4599,-222.8533 291.6486,-217.1769"/>
|
|
||||||
<polygon fill="#000000" stroke="#000000" points="283.1684,-211.3762 293.9628,-213.3079 287.2953,-214.1991 291.4222,-217.0221 291.4222,-217.0221 291.4222,-217.0221 287.2953,-214.1991 288.8816,-220.7363 283.1684,-211.3762 283.1684,-211.3762"/>
|
|
||||||
<text text-anchor="middle" x="302.9014" y="-211.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
|
||||||
<text text-anchor="middle" x="295.2075" y="-253.6532" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
|
||||||
</g>
|
</g>
|
||||||
<!-- A11 -->
|
<!-- A11 -->
|
||||||
<g id="node12" class="node">
|
<g id="node12" class="node">
|
||||||
<title>A11</title>
|
<title>A11</title>
|
||||||
<polygon fill="none" stroke="#000000" points="73,-88 73,-120 195,-120 195,-88 73,-88"/>
|
<polygon fill="none" stroke="#000000" points="568.348,-324 568.348,-356 676.348,-356 676.348,-324 568.348,-324"/>
|
||||||
<text text-anchor="start" x="110.3845" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
<text text-anchor="start" x="605.4015" y="-337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
||||||
<polygon fill="none" stroke="#000000" points="73,-56 73,-88 195,-88 195,-56 73,-56"/>
|
<polygon fill="none" stroke="#000000" points="568.348,-232 568.348,-324 676.348,-324 676.348,-232 568.348,-232"/>
|
||||||
<text text-anchor="start" x="103.4355" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
<text text-anchor="start" x="598.452" y="-305" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
|
||||||
<polygon fill="none" stroke="#000000" points="73,0 73,-56 195,-56 195,0 73,0"/>
|
<text text-anchor="start" x="591.7885" y="-293" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
|
||||||
<text text-anchor="start" x="82.6035" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
<text text-anchor="start" x="582.6235" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
||||||
<text text-anchor="start" x="119.0025" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
<text text-anchor="start" x="582.0595" y="-269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
||||||
|
<text text-anchor="start" x="578.1705" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
||||||
|
<text text-anchor="start" x="594.0135" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="568.348,-212 568.348,-232 676.348,-232 676.348,-212 568.348,-212"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- A7->A11 -->
|
<!-- A2->A11 -->
|
||||||
<g id="edge12" class="edge">
|
<g id="edge13" class="edge">
|
||||||
<title>A7->A11</title>
|
<title>A2->A11</title>
|
||||||
<path fill="none" stroke="#000000" d="M184.5103,-184.2281C176.2169,-163.8307 166.8737,-140.8515 158.4988,-120.2539"/>
|
<path fill="none" stroke="#000000" d="M622.348,-507.8316C622.348,-462.6124 622.348,-402.6972 622.348,-356.2361"/>
|
||||||
<polygon fill="none" stroke="#000000" points="181.3548,-185.7599 188.3636,-193.7052 187.8393,-183.1233 181.3548,-185.7599"/>
|
|
||||||
</g>
|
</g>
|
||||||
<!-- A8->A8 -->
|
<!-- A3 -->
|
||||||
<g id="edge15" class="edge">
|
<g id="node4" class="node">
|
||||||
<title>A8->A8</title>
|
<title>A3</title>
|
||||||
<path fill="none" stroke="#000000" d="M475.3471,-272.2739C485.9443,-267.1987 493,-257.1074 493,-242 493,-231.3776 489.5118,-223.2351 483.7569,-217.5725"/>
|
<polygon fill="none" stroke="#000000" points="257.348,-366 257.348,-398 364.348,-398 364.348,-366 257.348,-366"/>
|
||||||
<polygon fill="#000000" stroke="#000000" points="475.3471,-211.7261 486.1266,-213.7393 479.4525,-214.5802 483.5579,-217.4342 483.5579,-217.4342 483.5579,-217.4342 479.4525,-214.5802 480.9893,-221.1291 475.3471,-211.7261 475.3471,-211.7261"/>
|
<text text-anchor="start" x="293.0655" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||||
<text text-anchor="middle" x="495.0548" y="-212.1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
<polygon fill="none" stroke="#000000" points="257.348,-226 257.348,-366 364.348,-366 364.348,-226 257.348,-226"/>
|
||||||
<text text-anchor="middle" x="487.2174" y="-253.1774" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
<text text-anchor="start" x="302.5095" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||||
|
<text text-anchor="start" x="283.338" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||||
|
<text text-anchor="start" x="284.453" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||||
|
<text text-anchor="start" x="266.9565" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout:max_retires</text>
|
||||||
|
<text text-anchor="start" x="292.79" y="-287" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||||
|
<text text-anchor="start" x="304.7395" y="-275" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||||
|
<text text-anchor="start" x="291.4015" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||||
|
<text text-anchor="start" x="289.727" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||||
|
<text text-anchor="start" x="304.1845" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="257.348,-170 257.348,-226 364.348,-226 364.348,-170 257.348,-170"/>
|
||||||
|
<text text-anchor="start" x="284.738" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||||
|
<text text-anchor="start" x="288.072" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||||
|
<text text-anchor="start" x="285.572" y="-183" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A12 -->
|
<!-- A4 -->
|
||||||
<g id="node13" class="node">
|
<g id="node5" class="node">
|
||||||
<title>A12</title>
|
<title>A4</title>
|
||||||
<polygon fill="none" stroke="#000000" points="274,-88 274,-120 396,-120 396,-88 274,-88"/>
|
<polygon fill="none" stroke="#000000" points="263.348,-1200 263.348,-1232 334.348,-1232 334.348,-1200 263.348,-1200"/>
|
||||||
<text text-anchor="start" x="308.05" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
<text text-anchor="start" x="273.293" y="-1213" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
|
||||||
<polygon fill="none" stroke="#000000" points="274,-56 274,-88 396,-88 396,-56 274,-56"/>
|
<polygon fill="none" stroke="#000000" points="263.348,-1180 263.348,-1200 334.348,-1200 334.348,-1180 263.348,-1180"/>
|
||||||
<text text-anchor="start" x="304.4355" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
<polygon fill="none" stroke="#000000" points="263.348,-1148 263.348,-1180 334.348,-1180 334.348,-1148 263.348,-1148"/>
|
||||||
<polygon fill="none" stroke="#000000" points="274,0 274,-56 396,-56 396,0 274,0"/>
|
<text text-anchor="start" x="280.787" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
||||||
<text text-anchor="start" x="283.6035" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
|
||||||
<text text-anchor="start" x="320.0025" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
</g>
|
</g>
|
||||||
<!-- A8->A12 -->
|
<!-- A5 -->
|
||||||
<g id="edge14" class="edge">
|
<g id="node6" class="node">
|
||||||
<title>A8->A12</title>
|
<title>A5</title>
|
||||||
<path fill="none" stroke="#000000" d="M377.3195,-184.2281C370.3709,-163.8307 362.5428,-140.8515 355.526,-120.2539"/>
|
<polygon fill="none" stroke="#000000" points="231.348,-994 231.348,-1026 365.348,-1026 365.348,-994 231.348,-994"/>
|
||||||
<polygon fill="none" stroke="#000000" points="374.0102,-185.368 380.5479,-193.7052 380.6363,-183.1107 374.0102,-185.368"/>
|
<text text-anchor="start" x="278.0655" y="-1007" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="231.348,-818 231.348,-994 365.348,-994 365.348,-818 231.348,-818"/>
|
||||||
|
<text text-anchor="start" x="261.6745" y="-975" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||||
|
<text text-anchor="start" x="258.891" y="-963" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||||
|
<text text-anchor="start" x="251.662" y="-951" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
||||||
|
<text text-anchor="start" x="257.496" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
||||||
|
<text text-anchor="start" x="276.6725" y="-927" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||||
|
<text text-anchor="start" x="280.5615" y="-915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||||
|
<text text-anchor="start" x="277.5065" y="-903" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
||||||
|
<text text-anchor="start" x="248.337" y="-891" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="246.9425" y="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="241.1145" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="280.5615" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
||||||
|
<text text-anchor="start" x="269.174" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
||||||
|
<text text-anchor="start" x="287.51" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="231.348,-750 231.348,-818 365.348,-818 365.348,-750 231.348,-750"/>
|
||||||
|
<text text-anchor="start" x="248.0575" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
||||||
|
<text text-anchor="start" x="272.7925" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
||||||
|
<text text-anchor="start" x="258.6205" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
||||||
|
<text text-anchor="start" x="256.9505" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
||||||
|
</g>
|
||||||
|
<!-- A4->A5 -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>A4->A5</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M298.348,-1137.5879C298.348,-1106.6429 298.348,-1065.8843 298.348,-1026.2983"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="294.8481,-1137.6902 298.348,-1147.6902 301.8481,-1137.6902 294.8481,-1137.6902"/>
|
||||||
|
</g>
|
||||||
|
<!-- A6 -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>A6</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="370.348,-668 370.348,-700 484.348,-700 484.348,-668 370.348,-668"/>
|
||||||
|
<text text-anchor="start" x="413.456" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="370.348,-564 370.348,-668 484.348,-668 484.348,-564 370.348,-564"/>
|
||||||
|
<text text-anchor="start" x="380.111" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||||
|
<text text-anchor="start" x="415.1255" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||||
|
<text text-anchor="start" x="395.948" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||||
|
<text text-anchor="start" x="399.288" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||||
|
<text text-anchor="start" x="402.8925" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||||
|
<text text-anchor="start" x="401.232" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||||
|
<text text-anchor="start" x="413.46" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="370.348,-448 370.348,-564 484.348,-564 484.348,-448 370.348,-448"/>
|
||||||
|
<text text-anchor="start" x="384.8405" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||||
|
<text text-anchor="start" x="386.7805" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||||
|
<text text-anchor="start" x="392.6245" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||||
|
<text text-anchor="start" x="380.6765" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||||
|
<text text-anchor="start" x="382.6215" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||||
|
<text text-anchor="start" x="391.7885" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||||
|
<text text-anchor="start" x="412.3505" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A5->A6 -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>A5->A6</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M358.9294,-740.5383C364.4479,-727.1056 370.0049,-713.5794 375.4378,-700.355"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="355.6797,-739.2382 355.117,-749.8181 362.1546,-741.8983 355.6797,-739.2382"/>
|
||||||
|
</g>
|
||||||
|
<!-- A7 -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>A7</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="127.348,-632 127.348,-664 218.348,-664 218.348,-632 127.348,-632"/>
|
||||||
|
<text text-anchor="start" x="145.343" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="127.348,-540 127.348,-632 218.348,-632 218.348,-540 127.348,-540"/>
|
||||||
|
<text text-anchor="start" x="157.846" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||||
|
<text text-anchor="start" x="160.9055" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||||
|
<text text-anchor="start" x="165.904" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||||
|
<text text-anchor="start" x="145.058" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||||
|
<text text-anchor="start" x="146.732" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||||
|
<text text-anchor="start" x="158.96" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="127.348,-484 127.348,-540 218.348,-540 218.348,-484 127.348,-484"/>
|
||||||
|
<text text-anchor="start" x="137.2885" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||||
|
<text text-anchor="start" x="157.8505" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A5->A7 -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>A5->A7</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M239.076,-740.2903C228.6761,-714.3733 218.1403,-688.1174 208.6075,-664.3611"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="235.9268,-741.8409 242.8992,-749.8181 242.4233,-739.2339 235.9268,-741.8409"/>
|
||||||
|
</g>
|
||||||
|
<!-- A6->A3 -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>A6->A3</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M376.3705,-447.6454C371.0187,-434.3805 365.5816,-420.9039 360.2423,-407.6696"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="356.3743,-398.0824 364.289,-405.6724 358.2451,-402.7192 360.1158,-407.3561 360.1158,-407.3561 360.1158,-407.3561 358.2451,-402.7192 355.9427,-409.0397 356.3743,-398.0824 356.3743,-398.0824"/>
|
||||||
|
<text text-anchor="middle" x="370.9946" y="-408.7296" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||||
|
<text text-anchor="middle" x="361.7502" y="-430.9982" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
|
</g>
|
||||||
|
<!-- A8 -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>A8</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="382.348,-300 382.348,-332 532.348,-332 532.348,-300 382.348,-300"/>
|
||||||
|
<text text-anchor="start" x="425.3935" y="-313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="382.348,-268 382.348,-300 532.348,-300 532.348,-268 382.348,-268"/>
|
||||||
|
<text text-anchor="start" x="392.335" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="382.348,-236 382.348,-268 532.348,-268 532.348,-236 382.348,-236"/>
|
||||||
|
<text text-anchor="start" x="442.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A6->A8 -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>A6->A8</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M441.464,-437.5454C445.3714,-399.7739 449.3591,-361.2265 452.3615,-332.203"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="437.9668,-437.3383 440.4192,-447.6454 444.9297,-438.0587 437.9668,-437.3383"/>
|
||||||
|
</g>
|
||||||
|
<!-- A7->A3 -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>A7->A3</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M210.935,-483.8952C216.3404,-471.7801 221.9084,-459.553 227.348,-448 235.1472,-431.4354 243.6196,-414.0579 252.0717,-397.0641"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="256.7608,-387.6701 256.3209,-398.6272 254.5277,-392.1437 252.2946,-396.6174 252.2946,-396.6174 252.2946,-396.6174 254.5277,-392.1437 248.2683,-394.6076 256.7608,-387.6701 256.7608,-387.6701"/>
|
||||||
|
<text text-anchor="middle" x="256.228" y="-404.663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||||
|
<text text-anchor="middle" x="210.6174" y="-460.8977" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A9 -->
|
<!-- A9 -->
|
||||||
<g id="node10" class="node">
|
<g id="node10" class="node">
|
||||||
<title>A9</title>
|
<title>A9</title>
|
||||||
<polygon fill="none" stroke="#000000" points="277,-572 277,-604 393,-604 393,-572 277,-572"/>
|
<polygon fill="none" stroke="#000000" points="64.348,-300 64.348,-332 220.348,-332 220.348,-300 64.348,-300"/>
|
||||||
<text text-anchor="start" x="305.274" y="-585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
<text text-anchor="start" x="107.059" y="-313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
||||||
<polygon fill="none" stroke="#000000" points="277,-492 277,-572 393,-572 393,-492 277,-492"/>
|
<polygon fill="none" stroke="#000000" points="64.348,-268 64.348,-300 220.348,-300 220.348,-268 64.348,-268"/>
|
||||||
<text text-anchor="start" x="320.553" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
<text text-anchor="start" x="74.0005" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
||||||
<text text-anchor="start" x="322.783" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
<polygon fill="none" stroke="#000000" points="64.348,-236 64.348,-268 220.348,-268 220.348,-236 64.348,-236"/>
|
||||||
<text text-anchor="start" x="324.997" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
<text text-anchor="start" x="127.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
<text text-anchor="start" x="320.553" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
|
||||||
<text text-anchor="start" x="321.108" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
|
||||||
<polygon fill="none" stroke="#000000" points="277,-364 277,-492 393,-492 393,-364 277,-364"/>
|
|
||||||
<text text-anchor="start" x="286.6575" y="-473" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
|
||||||
<text text-anchor="start" x="288.878" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
|
||||||
<text text-anchor="start" x="306.654" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
|
||||||
<text text-anchor="start" x="322.782" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
|
||||||
<text text-anchor="start" x="320.0025" y="-425" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
|
||||||
<text text-anchor="start" x="300.2705" y="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
|
||||||
<text text-anchor="start" x="299.721" y="-389" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
|
||||||
<text text-anchor="start" x="293.607" y="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
|
||||||
</g>
|
</g>
|
||||||
<!-- A9->A7 -->
|
<!-- A7->A9 -->
|
||||||
<g id="edge7" class="edge">
|
<g id="edge7" class="edge">
|
||||||
<title>A9->A7</title>
|
<title>A7->A9</title>
|
||||||
<path fill="none" stroke="#000000" d="M272.2034,-364.3403C258.3698,-337.9803 244.4918,-311.5356 233.1958,-290.0108"/>
|
<path fill="none" stroke="#000000" d="M161.9757,-473.7349C157.0236,-425.8645 151.3255,-370.7828 147.349,-332.3431"/>
|
||||||
<polygon fill="none" stroke="#000000" points="269.1431,-366.041 276.8893,-373.2693 275.3415,-362.7881 269.1431,-366.041"/>
|
<polygon fill="none" stroke="#000000" points="158.5098,-474.2451 163.0203,-483.8319 165.4726,-473.5248 158.5098,-474.2451"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- A9->A8 -->
|
<!-- A8->A8 -->
|
||||||
<g id="edge8" class="edge">
|
<g id="edge15" class="edge">
|
||||||
<title>A9->A8</title>
|
<title>A8->A8</title>
|
||||||
<path fill="none" stroke="#000000" d="M368.3237,-353.9299C374.1595,-331.1516 379.8848,-308.8044 384.6662,-290.1415"/>
|
<path fill="none" stroke="#000000" d="M532.5164,-321.6908C543.1874,-315.5948 550.348,-303.0313 550.348,-284 550.348,-270.3213 546.6488,-259.9838 540.6058,-252.9875"/>
|
||||||
<polygon fill="none" stroke="#000000" points="364.9098,-353.1532 365.8184,-363.709 371.6908,-354.8905 364.9098,-353.1532"/>
|
<polygon fill="#000000" stroke="#000000" points="532.5164,-246.3092 543.0929,-249.2054 536.3722,-249.4924 540.228,-252.6756 540.228,-252.6756 540.228,-252.6756 536.3722,-249.4924 537.3632,-256.1459 532.5164,-246.3092 532.5164,-246.3092"/>
|
||||||
|
<text text-anchor="middle" x="551.8757" y="-248.3308" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||||
|
<text text-anchor="middle" x="543.0584" y="-301.6947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A10->A11 -->
|
<!-- A12 -->
|
||||||
<g id="edge9" class="edge">
|
<g id="node13" class="node">
|
||||||
<title>A10->A11</title>
|
<title>A12</title>
|
||||||
<path fill="none" stroke="#000000" d="M93.7037,-160.4648C99.1515,-146.8826 104.7134,-133.016 109.8967,-120.0931"/>
|
<polygon fill="none" stroke="#000000" points="478.348,-88 478.348,-120 600.348,-120 600.348,-88 478.348,-88"/>
|
||||||
<polygon fill="none" stroke="#000000" points="90.4307,-159.2232 89.9564,-169.8074 96.9276,-161.8291 90.4307,-159.2232"/>
|
<text text-anchor="start" x="515.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="478.348,-56 478.348,-88 600.348,-88 600.348,-56 478.348,-56"/>
|
||||||
|
<text text-anchor="start" x="508.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="478.348,0 478.348,-56 600.348,-56 600.348,0 478.348,0"/>
|
||||||
|
<text text-anchor="start" x="487.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||||
|
<text text-anchor="start" x="524.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A10->A12 -->
|
<!-- A8->A12 -->
|
||||||
<g id="edge10" class="edge">
|
<g id="edge14" class="edge">
|
||||||
<title>A10->A12</title>
|
<title>A8->A12</title>
|
||||||
<path fill="none" stroke="#000000" d="M122.8753,-170.8927C123.2497,-170.5927 123.6246,-170.2951 124,-170 149.1284,-150.2443 220.9786,-114.1777 273.8825,-88.7388"/>
|
<path fill="none" stroke="#000000" d="M478.5265,-226.1465C490.409,-193.6871 505.2165,-153.2373 517.2458,-120.3767"/>
|
||||||
<polygon fill="none" stroke="#000000" points="120.4501,-168.3606 115.1024,-177.5068 124.9865,-173.6918 120.4501,-168.3606"/>
|
<polygon fill="none" stroke="#000000" points="475.09,-225.3526 474.9391,-235.9464 481.6634,-227.759 475.09,-225.3526"/>
|
||||||
|
</g>
|
||||||
|
<!-- A9->A9 -->
|
||||||
|
<g id="edge17" class="edge">
|
||||||
|
<title>A9->A9</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M220.6951,-321.2601C231.2923,-315.0138 238.348,-302.5938 238.348,-284 238.348,-270.6357 234.703,-260.4608 228.7179,-253.4753"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="220.6951,-246.7399 231.2473,-249.7232 224.5245,-249.9548 228.3539,-253.1697 228.3539,-253.1697 228.3539,-253.1697 224.5245,-249.9548 225.4605,-256.6162 220.6951,-246.7399 220.6951,-246.7399"/>
|
||||||
|
<text text-anchor="middle" x="240.0123" y="-248.9211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||||
|
<text text-anchor="middle" x="231.039" y="-301.1428" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A13 -->
|
<!-- A13 -->
|
||||||
<g id="node14" class="node">
|
<g id="node14" class="node">
|
||||||
<title>A13</title>
|
<title>A13</title>
|
||||||
<polygon fill="none" stroke="#000000" points="350,-1164 350,-1196 453,-1196 453,-1164 350,-1164"/>
|
<polygon fill="none" stroke="#000000" points="251.348,-88 251.348,-120 373.348,-120 373.348,-88 251.348,-88"/>
|
||||||
<text text-anchor="start" x="390.662" y="-1177" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
<text text-anchor="start" x="285.398" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||||
<polygon fill="none" stroke="#000000" points="350,-1108 350,-1164 453,-1164 453,-1108 350,-1108"/>
|
<polygon fill="none" stroke="#000000" points="251.348,-56 251.348,-88 373.348,-88 373.348,-56 251.348,-56"/>
|
||||||
<text text-anchor="start" x="393.4415" y="-1145" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
<text text-anchor="start" x="281.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||||
<text text-anchor="start" x="368.986" y="-1133" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
<polygon fill="none" stroke="#000000" points="251.348,0 251.348,-56 373.348,-56 373.348,0 251.348,0"/>
|
||||||
<text text-anchor="start" x="382.6035" y="-1121" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
<text text-anchor="start" x="260.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||||
<polygon fill="none" stroke="#000000" points="350,-968 350,-1108 453,-1108 453,-968 350,-968"/>
|
<text text-anchor="start" x="297.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
<text text-anchor="start" x="377.3355" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
</g>
|
||||||
<text text-anchor="start" x="375.3845" y="-1077" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
<!-- A9->A13 -->
|
||||||
<text text-anchor="start" x="372.3305" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
<g id="edge16" class="edge">
|
||||||
<text text-anchor="start" x="370.6605" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
<title>A9->A13</title>
|
||||||
<text text-anchor="start" x="368.71" y="-1041" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
<path fill="none" stroke="#000000" d="M184.9859,-227.8183C209.8288,-195.0842 241.1576,-153.8039 266.5264,-120.3767"/>
|
||||||
<text text-anchor="start" x="383.713" y="-1029" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
<polygon fill="none" stroke="#000000" points="182.0747,-225.8647 178.8173,-235.9464 187.6507,-230.0965 182.0747,-225.8647"/>
|
||||||
<text text-anchor="start" x="377.8745" y="-1017" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
</g>
|
||||||
<text text-anchor="start" x="362.037" y="-1005" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
<!-- A10 -->
|
||||||
<text text-anchor="start" x="371.4855" y="-993" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
<g id="node11" class="node">
|
||||||
<text text-anchor="start" x="359.8225" y="-981" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
<title>A10</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="236.348,-662 236.348,-694 352.348,-694 352.348,-662 236.348,-662"/>
|
||||||
|
<text text-anchor="start" x="264.622" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="236.348,-582 236.348,-662 352.348,-662 352.348,-582 236.348,-582"/>
|
||||||
|
<text text-anchor="start" x="279.901" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||||
|
<text text-anchor="start" x="282.131" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||||
|
<text text-anchor="start" x="284.345" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||||
|
<text text-anchor="start" x="279.901" y="-607" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||||
|
<text text-anchor="start" x="280.456" y="-595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="236.348,-454 236.348,-582 352.348,-582 352.348,-454 236.348,-454"/>
|
||||||
|
<text text-anchor="start" x="246.0055" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||||
|
<text text-anchor="start" x="248.226" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||||
|
<text text-anchor="start" x="266.002" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||||
|
<text text-anchor="start" x="282.13" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||||
|
<text text-anchor="start" x="279.3505" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
<text text-anchor="start" x="259.6185" y="-491" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||||
|
<text text-anchor="start" x="264.628" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</text>
|
||||||
|
<text text-anchor="start" x="252.955" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A10->A8 -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>A10->A8</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M357.3002,-455.2837C358.6569,-452.8318 360.0073,-450.4016 361.348,-448 383.2991,-408.6787 409.1348,-364.6637 428.4716,-332.1398"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="354.1241,-453.7956 352.3659,-464.2436 360.2557,-457.1724 354.1241,-453.7956"/>
|
||||||
|
</g>
|
||||||
|
<!-- A10->A9 -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>A10->A9</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M231.478,-454.0506C209.0706,-411.2997 185.0929,-365.5527 167.6574,-332.2876"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="228.4903,-455.8898 236.2327,-463.1221 234.6903,-452.6401 228.4903,-455.8898"/>
|
||||||
|
</g>
|
||||||
|
<!-- A11->A12 -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>A11->A12</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M592.1173,-202.4136C582.0634,-175.28 571.0546,-145.5697 561.7056,-120.3387"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="588.8729,-203.7311 595.6294,-211.892 595.4368,-201.2989 588.8729,-203.7311"/>
|
||||||
|
</g>
|
||||||
|
<!-- A11->A13 -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>A11->A13</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M586.2753,-202.8933C578.5712,-190.8884 569.6045,-179.4114 559.348,-170 530.8998,-143.8959 437.024,-105.9199 373.5518,-82.1078"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="583.4606,-204.9994 591.6624,-211.7061 589.4331,-201.3485 583.4606,-204.9994"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- A14 -->
|
<!-- A14 -->
|
||||||
<g id="node15" class="node">
|
<g id="node15" class="node">
|
||||||
<title>A14</title>
|
<title>A14</title>
|
||||||
<polygon fill="none" stroke="#000000" points="319,-802 319,-834 386,-834 386,-802 319,-802"/>
|
<polygon fill="none" stroke="#000000" points="133.348,-1272 133.348,-1304 236.348,-1304 236.348,-1272 133.348,-1272"/>
|
||||||
<text text-anchor="start" x="334.993" y="-815" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
<text text-anchor="start" x="174.01" y="-1285" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||||
<polygon fill="none" stroke="#000000" points="319,-782 319,-802 386,-802 386,-782 319,-782"/>
|
<polygon fill="none" stroke="#000000" points="133.348,-1216 133.348,-1272 236.348,-1272 236.348,-1216 133.348,-1216"/>
|
||||||
<polygon fill="none" stroke="#000000" points="319,-738 319,-782 386,-782 386,-738 319,-738"/>
|
<text text-anchor="start" x="176.7895" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||||
<text text-anchor="start" x="328.884" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
<text text-anchor="start" x="152.334" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||||
<text text-anchor="start" x="336.668" y="-751" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
<text text-anchor="start" x="165.9515" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||||
</g>
|
<polygon fill="none" stroke="#000000" points="133.348,-1076 133.348,-1216 236.348,-1216 236.348,-1076 133.348,-1076"/>
|
||||||
<!-- A13->A14 -->
|
<text text-anchor="start" x="160.6835" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||||
<g id="edge16" class="edge">
|
<text text-anchor="start" x="158.7325" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||||
<title>A13->A14</title>
|
<text text-anchor="start" x="155.6785" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||||
<path fill="none" stroke="#000000" d="M380.4486,-957.853C373.2314,-914.2551 365.5447,-867.821 359.9831,-834.2247"/>
|
<text text-anchor="start" x="154.0085" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||||
<polygon fill="none" stroke="#000000" points="377.0391,-958.688 382.1254,-967.9821 383.9452,-957.5447 377.0391,-958.688"/>
|
<text text-anchor="start" x="152.058" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||||
|
<text text-anchor="start" x="167.061" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||||
|
<text text-anchor="start" x="161.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||||
|
<text text-anchor="start" x="145.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||||
|
<text text-anchor="start" x="154.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||||
|
<text text-anchor="start" x="143.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A15 -->
|
<!-- A15 -->
|
||||||
<g id="node16" class="node">
|
<g id="node16" class="node">
|
||||||
<title>A15</title>
|
<title>A15</title>
|
||||||
<polygon fill="none" stroke="#000000" points="417,-802 417,-834 484,-834 484,-802 417,-802"/>
|
<polygon fill="none" stroke="#000000" points="386.348,-904 386.348,-936 453.348,-936 453.348,-904 386.348,-904"/>
|
||||||
<text text-anchor="start" x="429.6585" y="-815" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
<text text-anchor="start" x="402.341" y="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||||
<polygon fill="none" stroke="#000000" points="417,-782 417,-802 484,-802 484,-782 417,-782"/>
|
<polygon fill="none" stroke="#000000" points="386.348,-884 386.348,-904 453.348,-904 453.348,-884 386.348,-884"/>
|
||||||
<polygon fill="none" stroke="#000000" points="417,-738 417,-782 484,-782 484,-738 417,-738"/>
|
<polygon fill="none" stroke="#000000" points="386.348,-840 386.348,-884 453.348,-884 453.348,-840 386.348,-840"/>
|
||||||
<text text-anchor="start" x="426.884" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
<text text-anchor="start" x="396.232" y="-865" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||||
<text text-anchor="start" x="434.668" y="-751" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
<text text-anchor="start" x="404.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||||
</g>
|
</g>
|
||||||
<!-- A13->A15 -->
|
<!-- A14->A15 -->
|
||||||
<g id="edge17" class="edge">
|
<g id="edge18" class="edge">
|
||||||
<title>A13->A15</title>
|
<title>A14->A15</title>
|
||||||
<path fill="none" stroke="#000000" d="M421.5514,-957.853C428.7686,-914.2551 436.4553,-867.821 442.0169,-834.2247"/>
|
<path fill="none" stroke="#000000" d="M242.8857,-1086.9876C246.5464,-1083.0913 250.3682,-1079.4032 254.348,-1076 298.2601,-1038.4501 335.1504,-1068.4478 374.348,-1026 397.0004,-1001.4693 408.2589,-965.3633 413.8498,-936.2357"/>
|
||||||
<polygon fill="none" stroke="#000000" points="418.0548,-957.5447 419.8746,-967.9821 424.9609,-958.688 418.0548,-957.5447"/>
|
<polygon fill="none" stroke="#000000" points="240.0515,-1084.9088 236.0452,-1094.717 245.2936,-1089.548 240.0515,-1084.9088"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- A14->A5 -->
|
<!-- A16 -->
|
||||||
|
<g id="node17" class="node">
|
||||||
|
<title>A16</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.348,-904 142.348,-936 209.348,-936 209.348,-904 142.348,-904"/>
|
||||||
|
<text text-anchor="start" x="155.0065" y="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.348,-884 142.348,-904 209.348,-904 209.348,-884 142.348,-884"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.348,-840 142.348,-884 209.348,-884 209.348,-840 142.348,-840"/>
|
||||||
|
<text text-anchor="start" x="152.232" y="-865" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||||
|
<text text-anchor="start" x="160.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A14->A16 -->
|
||||||
<g id="edge19" class="edge">
|
<g id="edge19" class="edge">
|
||||||
<title>A14->A5</title>
|
<title>A14->A16</title>
|
||||||
<path fill="none" stroke="#000000" d="M328.0662,-737.8133C310.5801,-702.608 286.0413,-653.2031 263.2551,-607.3269"/>
|
<path fill="none" stroke="#000000" d="M180.6399,-1065.5724C179.2846,-1020.0932 177.8303,-971.2935 176.7899,-936.3828"/>
|
||||||
<polygon fill="#000000" stroke="#000000" points="258.7238,-598.2039 267.2025,-605.1583 260.948,-602.682 263.1723,-607.1601 263.1723,-607.1601 263.1723,-607.1601 260.948,-602.682 259.142,-609.1619 258.7238,-598.2039 258.7238,-598.2039"/>
|
<polygon fill="none" stroke="#000000" points="177.1491,-1065.9355 180.9455,-1075.8267 184.146,-1065.7269 177.1491,-1065.9355"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- A15->A6 -->
|
<!-- A15->A6 -->
|
||||||
<g id="edge18" class="edge">
|
<g id="edge21" class="edge">
|
||||||
<title>A15->A6</title>
|
<title>A15->A6</title>
|
||||||
<path fill="none" stroke="#000000" d="M451.1169,-737.8133C452.1468,-693.3826 453.7008,-626.3353 454.9531,-572.3076"/>
|
<path fill="none" stroke="#000000" d="M420.5717,-839.9684C421.4566,-805.2366 422.6992,-756.4655 423.879,-710.1572"/>
|
||||||
<polygon fill="#000000" stroke="#000000" points="455.1913,-562.0332 459.4583,-572.1349 455.0754,-567.0319 454.9595,-572.0306 454.9595,-572.0306 454.9595,-572.0306 455.0754,-567.0319 450.4607,-571.9262 455.1913,-562.0332 455.1913,-562.0332"/>
|
<polygon fill="#000000" stroke="#000000" points="424.1376,-700.0098 428.3813,-710.1212 424.0102,-705.0082 423.8828,-710.0066 423.8828,-710.0066 423.8828,-710.0066 424.0102,-705.0082 419.3842,-709.8919 424.1376,-700.0098 424.1376,-700.0098"/>
|
||||||
|
</g>
|
||||||
|
<!-- A16->A7 -->
|
||||||
|
<g id="edge20" class="edge">
|
||||||
|
<title>A16->A7</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M174.8891,-839.9684C174.4696,-796.0581 173.8357,-729.7079 173.3059,-674.2644"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="173.2083,-664.0467 177.8037,-674.0032 173.2561,-669.0465 173.304,-674.0463 173.304,-674.0463 173.304,-674.0463 173.2561,-669.0465 168.8042,-674.0893 173.2083,-664.0467 173.2083,-664.0467"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 32 KiB |
@@ -4,13 +4,15 @@
|
|||||||
|
|
||||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||||
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
||||||
|
[Modbus|que;;snd_handler;rsp_handler;timeout:max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp()]
|
||||||
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list;state|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
||||||
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
||||||
[Message]^[SolarmanV5|control;serial;snr;switch|msg_unknown();;close()]
|
[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
|
||||||
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
|
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
|
||||||
|
[Talent]has-1>[Modbus]
|
||||||
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
|
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
|
||||||
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();__async_write();__async_forward()]^[ConnectionG3]
|
[SolarmanV5]has-1>[Modbus]
|
||||||
|
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
|
||||||
[AsyncStream]^[ConnectionG3P]
|
[AsyncStream]^[ConnectionG3P]
|
||||||
[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
|
[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
|
||||||
[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
|
[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
aiomqtt==2.0.1
|
aiomqtt==2.0.1
|
||||||
schema==0.7.5
|
schema==0.7.5
|
||||||
aiocron==1.8
|
aiocron==1.8
|
||||||
|
aiohttp==3.9.5
|
||||||
@@ -1,42 +1,77 @@
|
|||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from messages import hex_dump_memory
|
import time
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from messages import hex_dump_memory, State
|
||||||
|
from typing import Self
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
import gc
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
|
|
||||||
class AsyncStream():
|
class AsyncStream():
|
||||||
|
_ids = count(0)
|
||||||
|
MAX_PROC_TIME = 2
|
||||||
|
'''maximum processing time for a received msg in sec'''
|
||||||
|
MAX_START_TIME = 400
|
||||||
|
'''maximum time without a received msg in sec'''
|
||||||
|
MAX_INV_IDLE_TIME = 90
|
||||||
|
'''maximum time without a received msg from the inverter in sec'''
|
||||||
|
MAX_CLOUD_IDLE_TIME = 360
|
||||||
|
'''maximum time without a received msg from cloud side in sec'''
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr) -> None:
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
|
addr) -> None:
|
||||||
logger.debug('AsyncStream.__init__')
|
logger.debug('AsyncStream.__init__')
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
self.writer = writer
|
self.writer = writer
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.r_addr = ''
|
self.r_addr = ''
|
||||||
self.l_addr = ''
|
self.l_addr = ''
|
||||||
|
self.conn_no = next(self._ids)
|
||||||
|
self.proc_start = None # start processing start timestamp
|
||||||
|
self.proc_max = 0
|
||||||
|
|
||||||
async def server_loop(self, addr):
|
def __timeout(self) -> int:
|
||||||
|
if self.state == State.init:
|
||||||
|
to = self.MAX_START_TIME
|
||||||
|
else:
|
||||||
|
if self.server_side:
|
||||||
|
to = self.MAX_INV_IDLE_TIME
|
||||||
|
else:
|
||||||
|
to = self.MAX_CLOUD_IDLE_TIME
|
||||||
|
return to
|
||||||
|
|
||||||
|
async def server_loop(self, addr: str) -> None:
|
||||||
'''Loop for receiving messages from the inverter (server-side)'''
|
'''Loop for receiving messages from the inverter (server-side)'''
|
||||||
logging.info(f'Accept connection from {addr}')
|
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||||
|
f'Accept connection from {addr}')
|
||||||
self.inc_counter('Inverter_Cnt')
|
self.inc_counter('Inverter_Cnt')
|
||||||
await self.loop()
|
await self.loop()
|
||||||
self.dec_counter('Inverter_Cnt')
|
self.dec_counter('Inverter_Cnt')
|
||||||
logging.info(f'Server loop stopped for r{self.r_addr}')
|
logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for'
|
||||||
|
f' r{self.r_addr}')
|
||||||
|
|
||||||
# if the server connection closes, we also have to disconnect
|
# if the server connection closes, we also have to disconnect
|
||||||
# the connection to te TSUN cloud
|
# the connection to te TSUN cloud
|
||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
logging.debug("disconnect client connection")
|
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
|
||||||
self.remoteStream.disc()
|
f'connection: [{self.remoteStream.node_id}:'
|
||||||
|
f'{self.remoteStream.conn_no}]')
|
||||||
|
await self.remoteStream.disc()
|
||||||
try:
|
try:
|
||||||
await self._async_publ_mqtt_proxy_stat('proxy')
|
await self._async_publ_mqtt_proxy_stat('proxy')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def client_loop(self, addr):
|
async def client_loop(self, addr: str) -> None:
|
||||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
clientStream = await self.remoteStream.loop()
|
clientStream = await self.remoteStream.loop()
|
||||||
logging.info(f'Client loop stopped for l{clientStream.l_addr}')
|
logger.info(f'[{clientStream.node_id}:{clientStream.conn_no}] '
|
||||||
|
'Client loop stopped for'
|
||||||
|
f' l{clientStream.l_addr}')
|
||||||
|
|
||||||
# if the client connection closes, we don't touch the server
|
# if the client connection closes, we don't touch the server
|
||||||
# connection. Instead we erase the client connection stream,
|
# connection. Instead we erase the client connection stream,
|
||||||
@@ -52,69 +87,120 @@ class AsyncStream():
|
|||||||
# than erase client connection
|
# than erase client connection
|
||||||
self.remoteStream = None
|
self.remoteStream = None
|
||||||
|
|
||||||
async def loop(self):
|
async def loop(self) -> Self:
|
||||||
|
"""Async loop handler for precessing all received messages"""
|
||||||
self.r_addr = self.writer.get_extra_info('peername')
|
self.r_addr = self.writer.get_extra_info('peername')
|
||||||
self.l_addr = self.writer.get_extra_info('sockname')
|
self.l_addr = self.writer.get_extra_info('sockname')
|
||||||
|
self.proc_start = time.time()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
await self.__async_read()
|
proc = time.time() - self.proc_start
|
||||||
|
if proc > self.proc_max:
|
||||||
|
self.proc_max = proc
|
||||||
|
self.proc_start = None
|
||||||
|
dead_conn_to = self.__timeout()
|
||||||
|
await asyncio.wait_for(self.__async_read(),
|
||||||
|
dead_conn_to)
|
||||||
|
|
||||||
if self.unique_id:
|
if self.unique_id:
|
||||||
await self.__async_write()
|
await self.async_write()
|
||||||
await self.__async_forward()
|
await self.__async_forward()
|
||||||
await self.async_publ_mqtt()
|
await self.async_publ_mqtt()
|
||||||
|
|
||||||
except (ConnectionResetError,
|
except asyncio.TimeoutError:
|
||||||
ConnectionAbortedError,
|
logger.warning(f'[{self.node_id}:{self.conn_no}] Dead '
|
||||||
BrokenPipeError,
|
f'connection timeout ({dead_conn_to}s) '
|
||||||
RuntimeError) as error:
|
f'for {self.l_addr}')
|
||||||
logger.warning(f'In loop for l{self.l_addr} | '
|
await self.disc()
|
||||||
f'r{self.r_addr}: {error}')
|
|
||||||
self.close()
|
self.close()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
except OSError as error:
|
||||||
|
logger.error(f'[{self.node_id}:{self.conn_no}] '
|
||||||
|
f'{error} for l{self.l_addr} | '
|
||||||
|
f'r{self.r_addr}')
|
||||||
|
await self.disc()
|
||||||
|
self.close()
|
||||||
|
return self
|
||||||
|
|
||||||
|
except RuntimeError as error:
|
||||||
|
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||||
|
f'{error} for {self.l_addr}')
|
||||||
|
await self.disc()
|
||||||
|
self.close()
|
||||||
|
return self
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.inc_counter('SW_Exception')
|
self.inc_counter('SW_Exception')
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Exception for {self.addr}:\n"
|
f"Exception for {self.addr}:\n"
|
||||||
f"{traceback.format_exc()}")
|
f"{traceback.format_exc()}")
|
||||||
self.close()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def disc(self) -> None:
|
async def async_write(self, headline: str = 'Transmit to ') -> None:
|
||||||
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
"""Async write handler to transmit the send_buffer"""
|
||||||
self.writer.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
|
||||||
self.writer.close()
|
|
||||||
|
|
||||||
'''
|
|
||||||
Our private methods
|
|
||||||
'''
|
|
||||||
async def __async_read(self) -> None:
|
|
||||||
data = await self.reader.read(4096)
|
|
||||||
if data:
|
|
||||||
self._recv_buffer += data
|
|
||||||
self.read() # call read in parent class
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Peer closed.")
|
|
||||||
|
|
||||||
async def __async_write(self) -> None:
|
|
||||||
if self._send_buffer:
|
if self._send_buffer:
|
||||||
hex_dump_memory(logging.INFO, f'Transmit to {self.addr}:',
|
hex_dump_memory(logging.INFO, f'{headline}{self.addr}:',
|
||||||
self._send_buffer, len(self._send_buffer))
|
self._send_buffer, len(self._send_buffer))
|
||||||
self.writer.write(self._send_buffer)
|
self.writer.write(self._send_buffer)
|
||||||
await self.writer.drain()
|
await self.writer.drain()
|
||||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||||
|
|
||||||
|
async def disc(self) -> None:
|
||||||
|
"""Async disc handler for graceful disconnect"""
|
||||||
|
if self.writer.is_closing():
|
||||||
|
return
|
||||||
|
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
||||||
|
self.writer.close()
|
||||||
|
await self.writer.wait_closed()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""close handler for a no waiting disconnect
|
||||||
|
|
||||||
|
hint: must be called before releasing the connection instance
|
||||||
|
"""
|
||||||
|
self.reader.feed_eof() # abort awaited read
|
||||||
|
if self.writer.is_closing():
|
||||||
|
return
|
||||||
|
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
||||||
|
self.writer.close()
|
||||||
|
|
||||||
|
def healthy(self) -> bool:
|
||||||
|
elapsed = 0
|
||||||
|
if self.proc_start is not None:
|
||||||
|
elapsed = time.time() - self.proc_start
|
||||||
|
if self.state == State.closed or elapsed > self.MAX_PROC_TIME:
|
||||||
|
logging.debug(f'[{self.node_id}:{self.conn_no}:'
|
||||||
|
f'{type(self).__name__}]'
|
||||||
|
f' act:{round(1000*elapsed)}ms'
|
||||||
|
f' max:{round(1000*self.proc_max)}ms')
|
||||||
|
logging.debug(f'Healthy()) refs: {gc.get_referrers(self)}')
|
||||||
|
return elapsed < 5
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our private methods
|
||||||
|
'''
|
||||||
|
async def __async_read(self) -> None:
|
||||||
|
"""Async read handler to read received data from TCP stream"""
|
||||||
|
data = await self.reader.read(4096)
|
||||||
|
if data:
|
||||||
|
self.proc_start = time.time()
|
||||||
|
self._recv_buffer += data
|
||||||
|
wait = self.read() # call read in parent class
|
||||||
|
if wait > 0:
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Peer closed.")
|
||||||
|
|
||||||
async def __async_forward(self) -> None:
|
async def __async_forward(self) -> None:
|
||||||
if self._forward_buffer:
|
"""forward handler transmits data over the remote connection"""
|
||||||
|
if not self._forward_buffer:
|
||||||
|
return
|
||||||
|
try:
|
||||||
if not self.remoteStream:
|
if not self.remoteStream:
|
||||||
await self.async_create_remote()
|
await self.async_create_remote()
|
||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
if self.remoteStream._init_new_client_conn():
|
if self.remoteStream._init_new_client_conn():
|
||||||
await self.remoteStream.__async_write()
|
await self.remoteStream.async_write()
|
||||||
|
|
||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
self.remoteStream._update_header(self._forward_buffer)
|
self.remoteStream._update_header(self._forward_buffer)
|
||||||
@@ -126,6 +212,30 @@ class AsyncStream():
|
|||||||
await self.remoteStream.writer.drain()
|
await self.remoteStream.writer.drain()
|
||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
|
|
||||||
|
except OSError as error:
|
||||||
|
if self.remoteStream:
|
||||||
|
rmt = self.remoteStream
|
||||||
|
self.remoteStream = None
|
||||||
|
logger.error(f'[{rmt.node_id}:{rmt.conn_no}] Fwd: {error} for '
|
||||||
|
f'l{rmt.l_addr} | r{rmt.r_addr}')
|
||||||
|
await rmt.disc()
|
||||||
|
rmt.close()
|
||||||
|
|
||||||
|
except RuntimeError as error:
|
||||||
|
if self.remoteStream:
|
||||||
|
rmt = self.remoteStream
|
||||||
|
self.remoteStream = None
|
||||||
|
logger.info(f'[{rmt.node_id}:{rmt.conn_no}] '
|
||||||
|
f'Fwd: {error} for {rmt.l_addr}')
|
||||||
|
await rmt.disc()
|
||||||
|
rmt.close()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.inc_counter('SW_Exception')
|
||||||
|
logger.error(
|
||||||
|
f"Fwd Exception for {self.addr}:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import tomllib
|
import tomllib
|
||||||
import logging
|
import logging
|
||||||
from schema import Schema, And, Use, Optional
|
from schema import Schema, And, Or, Use, Optional
|
||||||
|
|
||||||
|
|
||||||
class Config():
|
class Config():
|
||||||
@@ -38,6 +38,14 @@ class Config():
|
|||||||
'proxy_node_id': Use(str),
|
'proxy_node_id': Use(str),
|
||||||
'proxy_unique_id': Use(str)
|
'proxy_unique_id': Use(str)
|
||||||
},
|
},
|
||||||
|
'gen3plus': {
|
||||||
|
'at_acl': {
|
||||||
|
Or('mqtt', 'tsun'): {
|
||||||
|
'allow': [str],
|
||||||
|
Optional('block', default=[]): [str]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
||||||
Optional('monitor_sn', default=0): Use(int),
|
Optional('monitor_sn', default=0): Use(int),
|
||||||
@@ -76,51 +84,77 @@ class Config():
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls) -> None:
|
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
|
'''Read config file, merge it with the default config
|
||||||
and sanitize the result'''
|
and sanitize the result'''
|
||||||
|
err = None
|
||||||
config = {}
|
config = {}
|
||||||
logger = logging.getLogger('data')
|
logger = logging.getLogger('data')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# make the default config transparaent by copying it
|
|
||||||
# in the config.example file
|
|
||||||
shutil.copy2("default_config.toml", "config/config.example.toml")
|
|
||||||
|
|
||||||
# read example config file as default configuration
|
# read example config file as default configuration
|
||||||
with open("default_config.toml", "rb") as f:
|
cls.def_config = {}
|
||||||
|
with open(f"{path}default_config.toml", "rb") as f:
|
||||||
def_config = tomllib.load(f)
|
def_config = tomllib.load(f)
|
||||||
|
cls.def_config = cls.conf_schema.validate(def_config)
|
||||||
|
|
||||||
# overwrite the default values, with values from
|
# overwrite the default values, with values from
|
||||||
# the config.toml file
|
# 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:
|
try:
|
||||||
with open("config/config.toml", "rb") as f:
|
cls.config = cls.conf_schema.validate(config)
|
||||||
usr_config = tomllib.load(f)
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logging.error(f'Config.read: {error}')
|
err = f'Config.read: {error}'
|
||||||
logging.info(
|
logging.error(err)
|
||||||
'\n To create the missing config.toml file, '
|
|
||||||
'you can rename the template config.example.toml\n'
|
|
||||||
' and customize it for your scenario.\n')
|
|
||||||
usr_config = def_config
|
|
||||||
|
|
||||||
config['tsun'] = def_config['tsun'] | usr_config['tsun']
|
|
||||||
config['solarman'] = def_config['solarman'] | \
|
|
||||||
usr_config['solarman']
|
|
||||||
config['mqtt'] = def_config['mqtt'] | usr_config['mqtt']
|
|
||||||
config['ha'] = def_config['ha'] | usr_config['ha']
|
|
||||||
config['inverters'] = def_config['inverters'] | \
|
|
||||||
usr_config['inverters']
|
|
||||||
|
|
||||||
cls.config = cls.conf_schema.validate(config)
|
|
||||||
cls.def_config = cls.conf_schema.validate(def_config)
|
|
||||||
# logging.debug(f'Readed config: "{cls.config}" ')
|
# logging.debug(f'Readed config: "{cls.config}" ')
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error(f'Config.read: {error}')
|
err = f'Config.read: {error}'
|
||||||
|
logger.error(err)
|
||||||
cls.config = {}
|
cls.config = {}
|
||||||
|
|
||||||
|
return err
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, member: str = None):
|
def get(cls, member: str = None):
|
||||||
'''Get a named attribute from the proxy config. If member ==
|
'''Get a named attribute from the proxy config. If member ==
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
# import gc
|
# import gc
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
from async_stream import AsyncStream
|
from async_stream import AsyncStream
|
||||||
from gen3.talent import Talent
|
from gen3.talent import Talent
|
||||||
|
|
||||||
@@ -8,12 +9,13 @@ logger = logging.getLogger('conn')
|
|||||||
|
|
||||||
class ConnectionG3(AsyncStream, Talent):
|
class ConnectionG3(AsyncStream, Talent):
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr, remote_stream, server_side: bool,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
|
addr, remote_stream: 'ConnectionG3', server_side: bool,
|
||||||
id_str=b'') -> None:
|
id_str=b'') -> None:
|
||||||
AsyncStream.__init__(self, reader, writer, addr)
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
Talent.__init__(self, server_side, id_str)
|
Talent.__init__(self, server_side, id_str)
|
||||||
|
|
||||||
self.remoteStream = remote_stream
|
self.remoteStream: 'ConnectionG3' = remote_stream
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
@@ -29,6 +31,10 @@ class ConnectionG3(AsyncStream, Talent):
|
|||||||
async def async_publ_mqtt(self) -> None:
|
async def async_publ_mqtt(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def healthy(self) -> bool:
|
||||||
|
logger.debug('ConnectionG3 healthy()')
|
||||||
|
return AsyncStream.healthy(self)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our private methods
|
Our private methods
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class RegisterMap:
|
|||||||
0xffffff05: Register.UNKNOWN_CTRL,
|
0xffffff05: Register.UNKNOWN_CTRL,
|
||||||
0xffffff06: Register.OTA_START_MSG,
|
0xffffff06: Register.OTA_START_MSG,
|
||||||
0xffffff07: Register.SW_EXCEPTION,
|
0xffffff07: Register.SW_EXCEPTION,
|
||||||
|
0xffffff08: Register.MAX_DESIGNED_POWER,
|
||||||
0xfffffffe: Register.TEST_REG1,
|
0xfffffffe: Register.TEST_REG1,
|
||||||
0xffffffff: Register.TEST_REG2,
|
0xffffffff: Register.TEST_REG2,
|
||||||
0x00000640: Register.OUTPUT_POWER,
|
0x00000640: Register.OUTPUT_POWER,
|
||||||
@@ -104,7 +105,8 @@ class InfosG3(Infos):
|
|||||||
if res:
|
if res:
|
||||||
yield res
|
yield res
|
||||||
|
|
||||||
def parse(self, buf, ind=0) -> Generator[tuple[str, bool], None, None]:
|
def parse(self, buf, ind=0, node_id: str = '') -> \
|
||||||
|
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
|
||||||
|
|
||||||
@@ -130,11 +132,24 @@ class InfosG3(Infos):
|
|||||||
errors='replace')
|
errors='replace')
|
||||||
ind += str_len+1
|
ind += str_len+1
|
||||||
|
|
||||||
|
elif data_type == 0x00: # 'Nul' -> end
|
||||||
|
i = elms # abort the loop
|
||||||
|
|
||||||
|
elif data_type == 0x41: # 'A' -> Nop ??
|
||||||
|
# result = struct.unpack_from('!l', buf, ind)[0]
|
||||||
|
ind += 0
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif data_type == 0x42: # 'B' -> byte, int8
|
||||||
|
result = struct.unpack_from('!B', buf, ind)[0]
|
||||||
|
ind += 1
|
||||||
|
|
||||||
elif data_type == 0x49: # 'I' -> int32
|
elif data_type == 0x49: # 'I' -> int32
|
||||||
result = struct.unpack_from('!l', buf, ind)[0]
|
result = struct.unpack_from('!l', buf, ind)[0]
|
||||||
ind += 4
|
ind += 4
|
||||||
|
|
||||||
elif data_type == 0x53: # 'S' -> short
|
elif data_type == 0x53: # 'S' -> short, int16
|
||||||
result = struct.unpack_from('!h', buf, ind)[0]
|
result = struct.unpack_from('!h', buf, ind)[0]
|
||||||
ind += 2
|
ind += 2
|
||||||
|
|
||||||
@@ -142,13 +157,14 @@ class InfosG3(Infos):
|
|||||||
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
||||||
ind += 4
|
ind += 4
|
||||||
|
|
||||||
elif data_type == 0x4c: # 'L' -> int64
|
elif data_type == 0x4c: # 'L' -> long, int64
|
||||||
result = struct.unpack_from('!q', buf, ind)[0]
|
result = struct.unpack_from('!q', buf, ind)[0]
|
||||||
ind += 8
|
ind += 8
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.inc_counter('Invalid_Data_Type')
|
self.inc_counter('Invalid_Data_Type')
|
||||||
logging.error(f"Infos.parse: data_type: {data_type}"
|
logging.error(f"Infos.parse: data_type: {data_type}"
|
||||||
|
f" @0x{addr:04x} No:{i}"
|
||||||
" not supported")
|
" not supported")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -161,7 +177,8 @@ class InfosG3(Infos):
|
|||||||
update = False
|
update = False
|
||||||
name = str(f'info-id.0x{addr:x}')
|
name = str(f'info-id.0x{addr:x}')
|
||||||
|
|
||||||
self.tracer.log(level, f'GEN3: {name} : {result}{unit}'
|
if update:
|
||||||
f' update: {update}')
|
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
||||||
|
f' {result}{unit}')
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
import asyncio
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
from config import Config
|
from config import Config
|
||||||
from inverter import Inverter
|
from inverter import Inverter
|
||||||
from gen3.connection_g3 import ConnectionG3
|
from gen3.connection_g3 import ConnectionG3
|
||||||
@@ -44,7 +45,7 @@ class InverterG3(Inverter, ConnectionG3):
|
|||||||
destroyed
|
destroyed
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr):
|
def __init__(self, reader: StreamReader, writer: StreamWriter, addr):
|
||||||
super().__init__(reader, writer, addr, None, True)
|
super().__init__(reader, writer, addr, None, True)
|
||||||
self.__ha_restarts = -1
|
self.__ha_restarts = -1
|
||||||
|
|
||||||
@@ -56,11 +57,14 @@ class InverterG3(Inverter, ConnectionG3):
|
|||||||
addr = (host, port)
|
addr = (host, port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info(f'Connected to {addr}')
|
logging.info(f'[{self.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
|
||||||
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
||||||
False, self.id_str)
|
False, self.id_str)
|
||||||
|
logging.info(f'[{self.remoteStream.node_id}:'
|
||||||
|
f'{self.remoteStream.conn_no}] '
|
||||||
|
f'Connected to {addr}')
|
||||||
asyncio.create_task(self.client_loop(addr))
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
except (ConnectionRefusedError, TimeoutError) as error:
|
except (ConnectionRefusedError, TimeoutError) as error:
|
||||||
@@ -121,7 +125,7 @@ class InverterG3(Inverter, ConnectionG3):
|
|||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
|
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
|
||||||
super().close() # call close handler in the parent class
|
super().close() # call close handler in the parent class
|
||||||
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
# logging.info(f'Inverter refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
logging.debug("InverterG3.__del__")
|
logging.debug("InverterG3.__del__")
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if __name__ == "app.src.gen3.talent":
|
if __name__ == "app.src.gen3.talent":
|
||||||
from app.src.messages import hex_dump_memory, Message
|
from app.src.messages import hex_dump_memory, Message, State
|
||||||
|
from app.src.modbus import Modbus
|
||||||
|
from app.src.my_timer import Timer
|
||||||
from app.src.config import Config
|
from app.src.config import Config
|
||||||
from app.src.gen3.infos_g3 import InfosG3
|
from app.src.gen3.infos_g3 import InfosG3
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
from messages import hex_dump_memory, Message
|
from messages import hex_dump_memory, Message, State
|
||||||
|
from modbus import Modbus
|
||||||
|
from my_timer import Timer
|
||||||
from config import Config
|
from config import Config
|
||||||
from gen3.infos_g3 import InfosG3
|
from gen3.infos_g3 import InfosG3
|
||||||
|
|
||||||
@@ -33,31 +37,54 @@ class Control:
|
|||||||
|
|
||||||
|
|
||||||
class Talent(Message):
|
class Talent(Message):
|
||||||
|
MB_START_TIMEOUT = 40
|
||||||
|
MB_REGULAR_TIMEOUT = 60
|
||||||
|
|
||||||
def __init__(self, server_side: bool, id_str=b''):
|
def __init__(self, server_side: bool, id_str=b''):
|
||||||
super().__init__(server_side)
|
super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
|
||||||
self.await_conn_resp_cnt = 0
|
self.await_conn_resp_cnt = 0
|
||||||
self.id_str = id_str
|
self.id_str = id_str
|
||||||
self.contact_name = b''
|
self.contact_name = b''
|
||||||
self.contact_mail = b''
|
self.contact_mail = b''
|
||||||
|
self.ts_offset = 0 # time offset between tsun cloud and local
|
||||||
self.db = InfosG3()
|
self.db = InfosG3()
|
||||||
self.switch = {
|
self.switch = {
|
||||||
0x00: self.msg_contact_info,
|
0x00: self.msg_contact_info,
|
||||||
0x13: self.msg_ota_update,
|
0x13: self.msg_ota_update,
|
||||||
0x22: self.msg_get_time,
|
0x22: self.msg_get_time,
|
||||||
0x71: self.msg_collector_data,
|
0x71: self.msg_collector_data,
|
||||||
|
# 0x76:
|
||||||
|
0x77: self.msg_modbus,
|
||||||
|
# 0x78:
|
||||||
0x04: self.msg_inverter_data,
|
0x04: self.msg_inverter_data,
|
||||||
}
|
}
|
||||||
|
self.log_lvl = {
|
||||||
|
0x00: logging.INFO,
|
||||||
|
0x13: logging.INFO,
|
||||||
|
0x22: logging.INFO,
|
||||||
|
0x71: logging.INFO,
|
||||||
|
# 0x76:
|
||||||
|
0x77: self.get_modbus_log_lvl,
|
||||||
|
# 0x78:
|
||||||
|
0x04: logging.INFO,
|
||||||
|
}
|
||||||
|
self.modbus_elms = 0 # for unit tests
|
||||||
|
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
||||||
|
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug('Talent.close()')
|
logging.debug('Talent.close()')
|
||||||
# we have refernces 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.state = State.closed
|
||||||
|
self.mb_timer.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
def __set_serial_no(self, serial_no: str):
|
def __set_serial_no(self, serial_no: str):
|
||||||
|
|
||||||
@@ -85,7 +112,7 @@ class Talent(Message):
|
|||||||
|
|
||||||
self.unique_id = serial_no
|
self.unique_id = serial_no
|
||||||
|
|
||||||
def read(self) -> None:
|
def read(self) -> float:
|
||||||
self._read()
|
self._read()
|
||||||
|
|
||||||
if not self.header_valid:
|
if not self.header_valid:
|
||||||
@@ -93,13 +120,20 @@ class Talent(Message):
|
|||||||
|
|
||||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||||
self.data_len):
|
self.data_len):
|
||||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
if self.state == State.init:
|
||||||
|
self.state = State.received # received 1st package
|
||||||
|
|
||||||
|
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
|
||||||
|
if callable(log_lvl):
|
||||||
|
log_lvl = log_lvl()
|
||||||
|
|
||||||
|
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
||||||
self._recv_buffer, self.header_len+self.data_len)
|
self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
self.__set_serial_no(self.id_str.decode("utf-8"))
|
self.__set_serial_no(self.id_str.decode("utf-8"))
|
||||||
self.__dispatch_msg()
|
self.__dispatch_msg()
|
||||||
self.__flush_recv_msg()
|
self.__flush_recv_msg()
|
||||||
return
|
return 0.5 # wait 500ms before sending a response
|
||||||
|
|
||||||
def forward(self, buffer, buflen) -> None:
|
def forward(self, buffer, buflen) -> None:
|
||||||
tsun = Config.get('tsun')
|
tsun = Config.get('tsun')
|
||||||
@@ -115,6 +149,42 @@ class Talent(Message):
|
|||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
|
' cause the state is not UP anymore')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__build_header(0x70, 0x77)
|
||||||
|
self._send_buffer += b'\x00\x01\xa3\x28' # fixme
|
||||||
|
self._send_buffer += struct.pack('!B', len(modbus_pdu))
|
||||||
|
self._send_buffer += modbus_pdu
|
||||||
|
self.__finish_send_msg()
|
||||||
|
|
||||||
|
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
||||||
|
self._send_buffer, len(self._send_buffer))
|
||||||
|
self.writer.write(self._send_buffer)
|
||||||
|
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||||
|
|
||||||
|
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
|
' as the state is not UP')
|
||||||
|
return
|
||||||
|
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||||
|
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
def mb_timout_cb(self, exp_cnt):
|
||||||
|
self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
|
||||||
|
|
||||||
|
if 0 == (exp_cnt % 30):
|
||||||
|
# logging.info("Regular Modbus Status request")
|
||||||
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
||||||
|
else:
|
||||||
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, 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
|
||||||
contact_mail = self.contact_mail
|
contact_mail = self.contact_mail
|
||||||
@@ -156,6 +226,24 @@ class Talent(Message):
|
|||||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||||
return round(ts*1000)
|
return round(ts*1000)
|
||||||
|
|
||||||
|
def _update_header(self, _forward_buffer):
|
||||||
|
'''update header for message before forwarding,
|
||||||
|
add time offset to timestamp'''
|
||||||
|
_len = len(_forward_buffer)
|
||||||
|
result = struct.unpack_from('!lB', _forward_buffer, 0)
|
||||||
|
id_len = result[1] # len of variable id string
|
||||||
|
if _len < 2*id_len + 21:
|
||||||
|
return
|
||||||
|
|
||||||
|
result = struct.unpack_from('!B', _forward_buffer, id_len+6)
|
||||||
|
msg_code = result[0]
|
||||||
|
if msg_code == 0x71 or msg_code == 0x04:
|
||||||
|
result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len)
|
||||||
|
ts = result[0] + self.ts_offset
|
||||||
|
logger.debug(f'offset: {self.ts_offset:08x}'
|
||||||
|
f' proxy-time: {ts:08x}')
|
||||||
|
struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts)
|
||||||
|
|
||||||
# check if there is a complete header in the buffer, parse it
|
# check if there is a complete header in the buffer, parse it
|
||||||
# and set
|
# and set
|
||||||
# self.header_len
|
# self.header_len
|
||||||
@@ -190,11 +278,13 @@ class Talent(Message):
|
|||||||
self.header_valid = True
|
self.header_valid = True
|
||||||
return
|
return
|
||||||
|
|
||||||
def __build_header(self, ctrl) -> None:
|
def __build_header(self, ctrl, msg_id=None) -> None:
|
||||||
|
if not msg_id:
|
||||||
|
msg_id = self.msg_id
|
||||||
self.send_msg_ofs = len(self._send_buffer)
|
self.send_msg_ofs = len(self._send_buffer)
|
||||||
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
|
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
|
||||||
0, self.id_str, ctrl, self.msg_id)
|
0, self.id_str, ctrl, msg_id)
|
||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
fnc = self.switch.get(msg_id, self.msg_unknown)
|
||||||
logger.info(self.__flow_str(self.server_side, 'tx') +
|
logger.info(self.__flow_str(self.server_side, 'tx') +
|
||||||
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
|
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
|
||||||
@@ -206,7 +296,8 @@ class Talent(Message):
|
|||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||||
if self.unique_id:
|
if self.unique_id:
|
||||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
f' Ctl: {int(self.ctrl):#02x} ({self.state}) '
|
||||||
|
f'Msg: {fnc.__name__!r}')
|
||||||
fnc()
|
fnc()
|
||||||
else:
|
else:
|
||||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||||
@@ -255,39 +346,37 @@ class Talent(Message):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def msg_get_time(self):
|
def msg_get_time(self):
|
||||||
tsun = Config.get('tsun')
|
if self.ctrl.is_ind():
|
||||||
if tsun['enabled']:
|
if self.data_len == 0:
|
||||||
if self.ctrl.is_ind():
|
self.state = State.pend # block MODBUS cmds
|
||||||
if self.data_len >= 8:
|
self.mb_timer.start(self.MB_START_TIMEOUT)
|
||||||
ts = self._timestamp()
|
ts = self._timestamp()
|
||||||
result = struct.unpack_from('!q', self._recv_buffer,
|
logger.debug(f'time: {ts:08x}')
|
||||||
self.header_len)
|
self.__build_header(0x91)
|
||||||
logger.debug(f'tsun-time: {result[0]:08x}'
|
self._send_buffer += struct.pack('!q', ts)
|
||||||
f' proxy-time: {ts:08x}')
|
self.__finish_send_msg()
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
elif self.data_len >= 8:
|
||||||
self.inc_counter('Unknown_Ctrl')
|
ts = self._timestamp()
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
result = struct.unpack_from('!q', self._recv_buffer,
|
||||||
|
self.header_len)
|
||||||
|
self.ts_offset = result[0]-ts
|
||||||
|
logger.debug(f'tsun-time: {int(result[0]):08x}'
|
||||||
|
f' proxy-time: {ts:08x}'
|
||||||
|
f' offset: {self.ts_offset}')
|
||||||
|
return # ignore received response
|
||||||
else:
|
else:
|
||||||
if self.ctrl.is_ind():
|
logger.warning('Unknown Ctrl')
|
||||||
if self.data_len == 0:
|
self.inc_counter('Unknown_Ctrl')
|
||||||
ts = self._timestamp()
|
|
||||||
logger.debug(f'time: {ts:08x}')
|
|
||||||
|
|
||||||
self.__build_header(0x91)
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
self._send_buffer += struct.pack('!q', ts)
|
|
||||||
self.__finish_send_msg()
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
|
|
||||||
def parse_msg_header(self):
|
def parse_msg_header(self):
|
||||||
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
||||||
|
|
||||||
data_id = result[0] # len of complete message
|
data_id = result[0] # len of complete message
|
||||||
id_len = result[1] # len of variable id string
|
id_len = result[1] # len of variable id string
|
||||||
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
|
logger.debug(f'Data_ID: 0x{data_id:08x} id_len: {id_len}')
|
||||||
|
|
||||||
msg_hdr_len = 5+id_len+9
|
msg_hdr_len = 5+id_len+9
|
||||||
|
|
||||||
@@ -321,6 +410,7 @@ class Talent(Message):
|
|||||||
self._send_buffer += b'\x01'
|
self._send_buffer += b'\x01'
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
self.__process_data()
|
self.__process_data()
|
||||||
|
self.state = State.up # allow MODBUS cmds
|
||||||
|
|
||||||
elif self.ctrl.is_resp():
|
elif self.ctrl.is_resp():
|
||||||
return # ignore received response
|
return # ignore received response
|
||||||
@@ -334,7 +424,7 @@ class Talent(Message):
|
|||||||
msg_hdr_len = self.parse_msg_header()
|
msg_hdr_len = self.parse_msg_header()
|
||||||
|
|
||||||
for key, update in self.db.parse(self._recv_buffer, self.header_len
|
for key, update in self.db.parse(self._recv_buffer, self.header_len
|
||||||
+ msg_hdr_len):
|
+ msg_hdr_len, self.node_id):
|
||||||
if update:
|
if update:
|
||||||
self.new_data[key] = True
|
self.new_data[key] = True
|
||||||
|
|
||||||
@@ -348,6 +438,59 @@ class Talent(Message):
|
|||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def parse_modbus_header(self):
|
||||||
|
|
||||||
|
msg_hdr_len = 5
|
||||||
|
|
||||||
|
result = struct.unpack_from('!lBB', self._recv_buffer,
|
||||||
|
self.header_len)
|
||||||
|
modbus_len = result[1]
|
||||||
|
# logger.debug(f'Ref: {result[0]}')
|
||||||
|
# logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}')
|
||||||
|
return msg_hdr_len, modbus_len
|
||||||
|
|
||||||
|
def get_modbus_log_lvl(self) -> int:
|
||||||
|
if self.ctrl.is_req():
|
||||||
|
return logging.INFO
|
||||||
|
elif self.ctrl.is_ind():
|
||||||
|
if self.server_side:
|
||||||
|
return self.mb.last_log_lvl
|
||||||
|
return logging.WARNING
|
||||||
|
|
||||||
|
def msg_modbus(self):
|
||||||
|
hdr_len, modbus_len = self.parse_modbus_header()
|
||||||
|
data = self._recv_buffer[self.header_len:
|
||||||
|
self.header_len+self.data_len]
|
||||||
|
|
||||||
|
if self.ctrl.is_req():
|
||||||
|
if self.remoteStream.mb.recv_req(data[hdr_len:],
|
||||||
|
self.remoteStream.
|
||||||
|
msg_forward):
|
||||||
|
self.inc_counter('Modbus_Command')
|
||||||
|
else:
|
||||||
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
|
elif self.ctrl.is_ind():
|
||||||
|
self.modbus_elms = 0
|
||||||
|
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
|
||||||
|
if not self.server_side:
|
||||||
|
logger.warning('Unknown Message')
|
||||||
|
self.inc_counter('Unknown_Msg')
|
||||||
|
return
|
||||||
|
|
||||||
|
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||||
|
hdr_len:],
|
||||||
|
self.node_id):
|
||||||
|
if update:
|
||||||
|
self.new_data[key] = True
|
||||||
|
self.modbus_elms += 1 # count for unit tests
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def msg_forward(self):
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
def msg_unknown(self):
|
def msg_unknown(self):
|
||||||
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
|
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
|
||||||
self.inc_counter('Unknown_Msg')
|
self.inc_counter('Unknown_Msg')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
# import gc
|
# import gc
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
from async_stream import AsyncStream
|
from async_stream import AsyncStream
|
||||||
from gen3plus.solarman_v5 import SolarmanV5
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
|
|
||||||
@@ -8,12 +9,13 @@ logger = logging.getLogger('conn')
|
|||||||
|
|
||||||
class ConnectionG3P(AsyncStream, SolarmanV5):
|
class ConnectionG3P(AsyncStream, SolarmanV5):
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr, remote_stream,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
|
addr, remote_stream: 'ConnectionG3P',
|
||||||
server_side: bool) -> None:
|
server_side: bool) -> None:
|
||||||
AsyncStream.__init__(self, reader, writer, addr)
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
SolarmanV5.__init__(self, server_side)
|
SolarmanV5.__init__(self, server_side)
|
||||||
|
|
||||||
self.remoteStream = remote_stream
|
self.remoteStream: 'ConnectionG3P' = remote_stream
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
@@ -29,6 +31,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
|
|||||||
async def async_publ_mqtt(self) -> None:
|
async def async_publ_mqtt(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def healthy(self) -> bool:
|
||||||
|
logger.debug('ConnectionG3P healthy()')
|
||||||
|
return AsyncStream.healthy(self)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our private methods
|
Our private methods
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ class RegisterMap:
|
|||||||
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}, # noqa: E501
|
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60}, # noqa: E501
|
||||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'eval': 'round(result/60)'}, # noqa: E501
|
||||||
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||||
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
||||||
0x4102004c: {'reg': Register.IP_ADRESS, 'fmt': '!16s'}, # noqa: E501
|
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
||||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||||
|
|
||||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||||
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||||
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
|
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # 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
|
||||||
@@ -58,7 +58,7 @@ class RegisterMap:
|
|||||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
|
0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
|
||||||
|
|
||||||
0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class InfosG3P(Infos):
|
|||||||
if res:
|
if res:
|
||||||
yield res
|
yield res
|
||||||
|
|
||||||
def parse(self, buf, msg_type: int, rcv_ftype: int) \
|
def parse(self, buf, msg_type: int, rcv_ftype: int, 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
|
||||||
@@ -122,5 +122,6 @@ class InfosG3P(Infos):
|
|||||||
name = str(f'info-id.0x{addr:x}')
|
name = str(f'info-id.0x{addr:x}')
|
||||||
update = False
|
update = False
|
||||||
|
|
||||||
self.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}'
|
if update:
|
||||||
f' update: {update}')
|
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||||
|
f' : {result}{unit}')
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
import asyncio
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
from config import Config
|
from config import Config
|
||||||
from inverter import Inverter
|
from inverter import Inverter
|
||||||
from gen3plus.connection_g3p import ConnectionG3P
|
from gen3plus.connection_g3p import ConnectionG3P
|
||||||
@@ -44,7 +45,7 @@ class InverterG3P(Inverter, ConnectionG3P):
|
|||||||
destroyed
|
destroyed
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr):
|
def __init__(self, reader: StreamReader, writer: StreamWriter, addr):
|
||||||
super().__init__(reader, writer, addr, None, True)
|
super().__init__(reader, writer, addr, None, True)
|
||||||
self.__ha_restarts = -1
|
self.__ha_restarts = -1
|
||||||
|
|
||||||
@@ -56,11 +57,14 @@ class InverterG3P(Inverter, ConnectionG3P):
|
|||||||
addr = (host, port)
|
addr = (host, port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info(f'Connected to {addr}')
|
logging.info(f'[{self.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
|
||||||
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
||||||
False)
|
False)
|
||||||
|
logging.info(f'[{self.remoteStream.node_id}:'
|
||||||
|
f'{self.remoteStream.conn_no}] '
|
||||||
|
f'Connected to {addr}')
|
||||||
asyncio.create_task(self.client_loop(addr))
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
except (ConnectionRefusedError, TimeoutError) as error:
|
except (ConnectionRefusedError, TimeoutError) as error:
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ import struct
|
|||||||
# import json
|
# import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||||
from app.src.messages import hex_dump_memory, Message
|
from app.src.messages import hex_dump_memory, Message, State
|
||||||
|
from app.src.modbus import Modbus
|
||||||
|
from app.src.my_timer import Timer
|
||||||
from app.src.config import Config
|
from app.src.config import Config
|
||||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||||
from app.src.infos import Register
|
from app.src.infos import Register
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
from messages import hex_dump_memory, Message
|
from messages import hex_dump_memory, Message, State
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from modbus import Modbus
|
||||||
|
from my_timer import Timer
|
||||||
from gen3plus.infos_g3p import InfosG3P
|
from gen3plus.infos_g3p import InfosG3P
|
||||||
from infos import Register
|
from infos import Register
|
||||||
# import traceback
|
# import traceback
|
||||||
@@ -46,9 +51,13 @@ class Sequence():
|
|||||||
|
|
||||||
|
|
||||||
class SolarmanV5(Message):
|
class SolarmanV5(Message):
|
||||||
|
AT_CMD = 1
|
||||||
|
MB_RTU_CMD = 2
|
||||||
|
MB_START_TIMEOUT = 40
|
||||||
|
MB_REGULAR_TIMEOUT = 60
|
||||||
|
|
||||||
def __init__(self, server_side: bool):
|
def __init__(self, server_side: bool):
|
||||||
super().__init__(server_side)
|
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
|
||||||
|
|
||||||
self.header_len = 11 # overwrite construcor in class Message
|
self.header_len = 11 # overwrite construcor in class Message
|
||||||
self.control = 0
|
self.control = 0
|
||||||
@@ -56,6 +65,7 @@ class SolarmanV5(Message):
|
|||||||
self.snr = 0
|
self.snr = 0
|
||||||
self.db = InfosG3P()
|
self.db = InfosG3P()
|
||||||
self.time_ofs = 0
|
self.time_ofs = 0
|
||||||
|
self.forward_at_cmd_resp = False
|
||||||
self.switch = {
|
self.switch = {
|
||||||
|
|
||||||
0x4210: self.msg_data_ind, # real time data
|
0x4210: self.msg_data_ind, # real time data
|
||||||
@@ -84,18 +94,54 @@ class SolarmanV5(Message):
|
|||||||
#
|
#
|
||||||
# MODbus or AT cmd
|
# MODbus or AT cmd
|
||||||
0x4510: self.msg_command_req, # from server
|
0x4510: self.msg_command_req, # from server
|
||||||
0x1510: self.msg_response, # from inverter
|
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)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug('Solarman.close()')
|
logging.debug('Solarman.close()')
|
||||||
# we have refernces 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.state = State.closed
|
||||||
|
self.mb_timer.close()
|
||||||
|
super().close()
|
||||||
|
|
||||||
def __set_serial_no(self, snr: int):
|
def __set_serial_no(self, snr: int):
|
||||||
serial_no = str(snr)
|
serial_no = str(snr)
|
||||||
@@ -128,7 +174,7 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
self.unique_id = serial_no
|
self.unique_id = serial_no
|
||||||
|
|
||||||
def read(self) -> None:
|
def read(self) -> float:
|
||||||
self._read()
|
self._read()
|
||||||
|
|
||||||
if not self.header_valid:
|
if not self.header_valid:
|
||||||
@@ -136,14 +182,20 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||||
self.data_len+2):
|
self.data_len+2):
|
||||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||||
|
if callable(log_lvl):
|
||||||
|
log_lvl = log_lvl()
|
||||||
|
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
||||||
self._recv_buffer, self.header_len+self.data_len+2)
|
self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||||
+ self.data_len + 2):
|
+ self.data_len + 2):
|
||||||
|
if self.state == State.init:
|
||||||
|
self.state = State.received
|
||||||
|
|
||||||
self.__set_serial_no(self.snr)
|
self.__set_serial_no(self.snr)
|
||||||
self.__dispatch_msg()
|
self.__dispatch_msg()
|
||||||
self.__flush_recv_msg()
|
self.__flush_recv_msg()
|
||||||
return
|
return 0 # wait 0s before sending a response
|
||||||
|
|
||||||
def forward(self, buffer, buflen) -> None:
|
def forward(self, buffer, buflen) -> None:
|
||||||
tsun = Config.get('solarman')
|
tsun = Config.get('solarman')
|
||||||
@@ -209,6 +261,10 @@ class SolarmanV5(Message):
|
|||||||
self.snr = result[4]
|
self.snr = result[4]
|
||||||
|
|
||||||
if start != 0xA5:
|
if start != 0xA5:
|
||||||
|
hex_dump_memory(logging.ERROR,
|
||||||
|
'Drop packet w invalid start byte from'
|
||||||
|
f' {self.addr}:', buf, buf_len)
|
||||||
|
|
||||||
self.inc_counter('Invalid_Msg_Format')
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
# erase broken recv buffer
|
# erase broken recv buffer
|
||||||
self._recv_buffer = bytearray()
|
self._recv_buffer = bytearray()
|
||||||
@@ -220,6 +276,9 @@ class SolarmanV5(Message):
|
|||||||
crc = buf[self.data_len+11]
|
crc = buf[self.data_len+11]
|
||||||
stop = buf[self.data_len+12]
|
stop = buf[self.data_len+12]
|
||||||
if stop != 0x15:
|
if stop != 0x15:
|
||||||
|
hex_dump_memory(logging.ERROR,
|
||||||
|
'Drop packet w invalid stop byte from '
|
||||||
|
f'{self.addr}:', buf, buf_len)
|
||||||
self.inc_counter('Invalid_Msg_Format')
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
if len(self._recv_buffer) > (self.data_len+13):
|
if len(self._recv_buffer) > (self.data_len+13):
|
||||||
next_start = buf[self.data_len+13]
|
next_start = buf[self.data_len+13]
|
||||||
@@ -293,41 +352,101 @@ class SolarmanV5(Message):
|
|||||||
self._heartbeat())
|
self._heartbeat())
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
|
|
||||||
def send_at_cmd(self, AT_cmd: str) -> None:
|
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
|
' cause the state is not UP anymore')
|
||||||
|
return
|
||||||
self.__build_header(0x4510)
|
self.__build_header(0x4510)
|
||||||
self._send_buffer += struct.pack(f'<BHLLL{len(AT_cmd)}sc', 1, 2,
|
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||||
0, 0, 0, AT_cmd.encode('utf-8'),
|
0x2b0, 0, 0, 0)
|
||||||
|
self._send_buffer += pdu
|
||||||
|
self.__finish_send_msg()
|
||||||
|
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
||||||
|
self._send_buffer, len(self._send_buffer))
|
||||||
|
self.writer.write(self._send_buffer)
|
||||||
|
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||||
|
|
||||||
|
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||||
|
' as the state is not UP')
|
||||||
|
return
|
||||||
|
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||||
|
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||||
|
|
||||||
|
def mb_timout_cb(self, exp_cnt):
|
||||||
|
self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
|
||||||
|
|
||||||
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
|
||||||
|
|
||||||
|
if 0 == (exp_cnt % 30):
|
||||||
|
# logging.info("Regular Modbus Status request")
|
||||||
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
||||||
|
|
||||||
|
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||||
|
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||||
|
cmd.startswith(tuple(self.at_acl[connection]['block']))
|
||||||
|
|
||||||
|
async def send_at_cmd(self, AT_cmd: str) -> None:
|
||||||
|
if self.state != State.up:
|
||||||
|
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
|
||||||
|
' as the state is not UP')
|
||||||
|
return
|
||||||
|
AT_cmd = AT_cmd.strip()
|
||||||
|
|
||||||
|
if self.at_cmd_forbidden(cmd=AT_cmd, connection='mqtt'):
|
||||||
|
data_json = f'\'{AT_cmd}\' is forbidden'
|
||||||
|
node_id = self.node_id
|
||||||
|
key = 'at_resp'
|
||||||
|
logger.info(f'{key}: {data_json}')
|
||||||
|
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
|
return
|
||||||
|
|
||||||
|
self.forward_at_cmd_resp = False
|
||||||
|
self.__build_header(0x4510)
|
||||||
|
self._send_buffer += struct.pack(f'<BHLLL{len(AT_cmd)}sc', self.AT_CMD,
|
||||||
|
2, 0, 0, 0, AT_cmd.encode('utf-8'),
|
||||||
b'\r')
|
b'\r')
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
|
try:
|
||||||
|
await self.async_write('Send AT Command:')
|
||||||
|
except Exception:
|
||||||
|
self._send_buffer = bytearray(0)
|
||||||
|
|
||||||
def __forward_msg(self):
|
def __forward_msg(self):
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def __build_model_name(self):
|
||||||
|
db = self.db
|
||||||
|
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
Rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
Model = None
|
||||||
|
if MaxPow == 2000:
|
||||||
|
if Rated == 800 or Rated == 600:
|
||||||
|
Model = f'TSOL-MS{MaxPow}({Rated})'
|
||||||
|
else:
|
||||||
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
|
elif MaxPow == 1800 or MaxPow == 1600:
|
||||||
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
|
if Model:
|
||||||
|
logger.info(f'Model: {Model}')
|
||||||
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
|
||||||
|
|
||||||
def __process_data(self, ftype):
|
def __process_data(self, ftype):
|
||||||
inv_update = False
|
inv_update = False
|
||||||
msg_type = self.control >> 8
|
msg_type = self.control >> 8
|
||||||
for key, update in self.db.parse(self._recv_buffer, msg_type, ftype):
|
for key, update in self.db.parse(self._recv_buffer, msg_type, ftype,
|
||||||
|
self.node_id):
|
||||||
if update:
|
if update:
|
||||||
if key == 'inverter':
|
if key == 'inverter':
|
||||||
inv_update = True
|
inv_update = True
|
||||||
self.new_data[key] = True
|
self.new_data[key] = True
|
||||||
|
|
||||||
if inv_update:
|
if inv_update:
|
||||||
db = self.db
|
self.__build_model_name()
|
||||||
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
|
||||||
Rated = db.get_db_value(Register.RATED_POWER, 0)
|
|
||||||
Model = None
|
|
||||||
if MaxPow == 2000:
|
|
||||||
if Rated == 800 or Rated == 600:
|
|
||||||
Model = f'TSOL-MS{MaxPow}({Rated})'
|
|
||||||
else:
|
|
||||||
Model = f'TSOL-MS{MaxPow}'
|
|
||||||
elif MaxPow == 1800 or MaxPow == 1600:
|
|
||||||
Model = f'TSOL-MS{MaxPow}'
|
|
||||||
if Model:
|
|
||||||
logger.info(f'Model: {Model}')
|
|
||||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Message handler methods
|
Message handler methods
|
||||||
'''
|
'''
|
||||||
@@ -340,14 +459,14 @@ class SolarmanV5(Message):
|
|||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
result = struct.unpack_from('<BLLL', data, 0)
|
result = struct.unpack_from('<BLLL', data, 0)
|
||||||
ftype = result[0] # always 2
|
ftype = result[0] # always 2
|
||||||
total = result[1]
|
# total = result[1]
|
||||||
tim = result[2]
|
tim = result[2]
|
||||||
res = result[3] # always zero
|
res = result[3] # always zero
|
||||||
logger.info(f'frame type:{ftype:02x}'
|
logger.info(f'frame type:{ftype:02x}'
|
||||||
f' timer:{tim:08x}s null:{res}')
|
f' timer:{tim:08x}s null:{res}')
|
||||||
if self.time_ofs:
|
# if self.time_ofs:
|
||||||
dt = datetime.fromtimestamp(total + self.time_ofs)
|
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||||
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
|
||||||
self.__process_data(ftype)
|
self.__process_data(ftype)
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
@@ -357,7 +476,7 @@ class SolarmanV5(Message):
|
|||||||
data = self._recv_buffer
|
data = self._recv_buffer
|
||||||
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
|
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
|
||||||
ftype = result[0] # 1 or 0x81
|
ftype = result[0] # 1 or 0x81
|
||||||
total = result[2]
|
# total = result[2]
|
||||||
tim = result[3]
|
tim = result[3]
|
||||||
if 1 == ftype:
|
if 1 == ftype:
|
||||||
self.time_ofs = result[4]
|
self.time_ofs = result[4]
|
||||||
@@ -365,13 +484,16 @@ class SolarmanV5(Message):
|
|||||||
cnt = result[6]
|
cnt = result[6]
|
||||||
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
|
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
|
||||||
f' ??: {unkn:04x} cnt:{cnt}')
|
f' ??: {unkn:04x} cnt:{cnt}')
|
||||||
if self.time_ofs:
|
# if self.time_ofs:
|
||||||
dt = datetime.fromtimestamp(total + self.time_ofs)
|
# dt = datetime.fromtimestamp(total + self.time_ofs)
|
||||||
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
|
||||||
self.__process_data(ftype)
|
self.__process_data(ftype)
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1210, ftype)
|
self.__send_ack_rsp(0x1210, ftype)
|
||||||
|
if self.state is not State.up:
|
||||||
|
self.state = State.up
|
||||||
|
self.mb_timer.start(self.MB_START_TIMEOUT)
|
||||||
|
|
||||||
def msg_sync_start(self):
|
def msg_sync_start(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
@@ -387,13 +509,79 @@ class SolarmanV5(Message):
|
|||||||
self.__send_ack_rsp(0x1310, ftype)
|
self.__send_ack_rsp(0x1310, ftype)
|
||||||
|
|
||||||
def msg_command_req(self):
|
def msg_command_req(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:
|
||||||
|
self.header_len+self.data_len]
|
||||||
result = struct.unpack_from('<B', data, 0)
|
result = struct.unpack_from('<B', data, 0)
|
||||||
ftype = result[0]
|
ftype = result[0]
|
||||||
|
if ftype == self.AT_CMD:
|
||||||
|
AT_cmd = data[15:].decode()
|
||||||
|
if self.at_cmd_forbidden(cmd=AT_cmd, connection='tsun'):
|
||||||
|
self.inc_counter('AT_Command_Blocked')
|
||||||
|
return
|
||||||
|
self.inc_counter('AT_Command')
|
||||||
|
self.forward_at_cmd_resp = True
|
||||||
|
|
||||||
|
elif ftype == self.MB_RTU_CMD:
|
||||||
|
if self.remoteStream.mb.recv_req(data[15:],
|
||||||
|
self.remoteStream.
|
||||||
|
__forward_msg):
|
||||||
|
self.inc_counter('Modbus_Command')
|
||||||
|
else:
|
||||||
|
logger.error('Invalid Modbus Msg')
|
||||||
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
|
return
|
||||||
|
|
||||||
self.inc_counter('AT_Command')
|
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1510, ftype)
|
|
||||||
|
def publish_mqtt(self, key, data):
|
||||||
|
asyncio.ensure_future(
|
||||||
|
self.mqtt.publish(key, data))
|
||||||
|
|
||||||
|
def get_cmd_rsp_log_lvl(self) -> int:
|
||||||
|
ftype = self._recv_buffer[self.header_len]
|
||||||
|
if ftype == self.AT_CMD:
|
||||||
|
if self.forward_at_cmd_resp:
|
||||||
|
return logging.INFO
|
||||||
|
return logging.DEBUG
|
||||||
|
elif ftype == self.MB_RTU_CMD:
|
||||||
|
if self.server_side:
|
||||||
|
return self.mb.last_log_lvl
|
||||||
|
|
||||||
|
return logging.WARNING
|
||||||
|
|
||||||
|
def msg_command_rsp(self):
|
||||||
|
data = self._recv_buffer[self.header_len:
|
||||||
|
self.header_len+self.data_len]
|
||||||
|
ftype = data[0]
|
||||||
|
if ftype == self.AT_CMD:
|
||||||
|
if not self.forward_at_cmd_resp:
|
||||||
|
data_json = data[14:].decode("utf-8")
|
||||||
|
node_id = self.node_id
|
||||||
|
key = 'at_resp'
|
||||||
|
logger.info(f'{key}: {data_json}')
|
||||||
|
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
|
return
|
||||||
|
elif ftype == self.MB_RTU_CMD:
|
||||||
|
valid = data[1]
|
||||||
|
modbus_msg_len = self.data_len - 14
|
||||||
|
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||||
|
if valid == 1 and modbus_msg_len > 4:
|
||||||
|
# logger.info(f'first byte modbus:{data[14]}')
|
||||||
|
inv_update = False
|
||||||
|
self.modbus_elms = 0
|
||||||
|
|
||||||
|
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
||||||
|
self.node_id):
|
||||||
|
self.modbus_elms += 1
|
||||||
|
if update:
|
||||||
|
if key == 'inverter':
|
||||||
|
inv_update = True
|
||||||
|
self.new_data[key] = True
|
||||||
|
|
||||||
|
if inv_update:
|
||||||
|
self.__build_model_name()
|
||||||
|
return
|
||||||
|
self.__forward_msg()
|
||||||
|
|
||||||
def msg_hbeat_ind(self):
|
def msg_hbeat_ind(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
@@ -402,6 +590,9 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1710, ftype)
|
self.__send_ack_rsp(0x1710, ftype)
|
||||||
|
if self.state is not State.up:
|
||||||
|
self.state = State.up
|
||||||
|
self.mb_timer.start(self.MB_START_TIMEOUT)
|
||||||
|
|
||||||
def msg_sync_end(self):
|
def msg_sync_end(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
@@ -423,8 +614,8 @@ class SolarmanV5(Message):
|
|||||||
valid = result[1] == 1 # status
|
valid = result[1] == 1 # status
|
||||||
ts = result[2]
|
ts = result[2]
|
||||||
set_hb = result[3] # always 60 or 120
|
set_hb = result[3] # always 60 or 120
|
||||||
logger.info(f'ftype:{ftype} accepted:{valid}'
|
logger.debug(f'ftype:{ftype} accepted:{valid}'
|
||||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||||
|
|
||||||
dt = datetime.fromtimestamp(ts)
|
dt = datetime.fromtimestamp(ts)
|
||||||
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class Register(Enum):
|
|||||||
SW_EXCEPTION = 57
|
SW_EXCEPTION = 57
|
||||||
INVALID_MSG_FMT = 58
|
INVALID_MSG_FMT = 58
|
||||||
AT_COMMAND = 59
|
AT_COMMAND = 59
|
||||||
|
MODBUS_COMMAND = 60
|
||||||
|
AT_COMMAND_BLOCKED = 61
|
||||||
OUTPUT_POWER = 83
|
OUTPUT_POWER = 83
|
||||||
RATED_POWER = 84
|
RATED_POWER = 84
|
||||||
INVERTER_TEMP = 85
|
INVERTER_TEMP = 85
|
||||||
@@ -86,7 +88,7 @@ class Register(Enum):
|
|||||||
DATA_UP_INTERVAL = 404
|
DATA_UP_INTERVAL = 404
|
||||||
CONNECT_COUNT = 405
|
CONNECT_COUNT = 405
|
||||||
HEARTBEAT_INTERVAL = 406
|
HEARTBEAT_INTERVAL = 406
|
||||||
IP_ADRESS = 407
|
IP_ADDRESS = 407
|
||||||
EVENT_401 = 500
|
EVENT_401 = 500
|
||||||
EVENT_402 = 501
|
EVENT_402 = 501
|
||||||
EVENT_403 = 502
|
EVENT_403 = 502
|
||||||
@@ -145,7 +147,7 @@ class Infos:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def static_init(cls):
|
def static_init(cls):
|
||||||
logging.info('Initialize proxy statistics')
|
logging.debug('Initialize proxy statistics')
|
||||||
# init proxy counter in the class.stat dictionary
|
# init proxy counter in the class.stat dictionary
|
||||||
cls.stat['proxy'] = {}
|
cls.stat['proxy'] = {}
|
||||||
for key in cls.__info_defs:
|
for key in cls.__info_defs:
|
||||||
@@ -192,7 +194,7 @@ class Infos:
|
|||||||
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
|
||||||
Register.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
|
||||||
@@ -209,16 +211,18 @@ class Infos:
|
|||||||
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
|
||||||
|
|
||||||
# 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': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi: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': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
||||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
||||||
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': '| int', 'name': 'AT Command Blocked', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': '| int', 'name': 'Modbus Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
||||||
|
|
||||||
# events
|
# events
|
||||||
@@ -230,7 +234,7 @@ class Infos:
|
|||||||
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_409: {'name': ['events', '409_'], '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_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EVENT_411: {'name': ['events', '411_'], '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_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
@@ -245,28 +249,27 @@ 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': '| 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': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
||||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
||||||
Register.VALUE_1: {'name': ['env', 'Value_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'value_1_', 'fmt': '| int', 'name': 'Value 1', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
|
||||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:counter'}}, # noqa: E501
|
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:counter'}}, # noqa: E501
|
||||||
|
|
||||||
# input measures:
|
# input measures:
|
||||||
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
@@ -285,13 +288,13 @@ class Infos:
|
|||||||
|
|
||||||
# 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': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| float', 'name': 'Power on Time', 'nat_prc': '3', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| int', 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " s"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.IP_ADRESS: {'name': ['controller', 'IP_Adress'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_adress_', 'fmt': '| string', 'name': 'IP Adress', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -340,7 +343,7 @@ class Infos:
|
|||||||
dict[counter] -= 1
|
dict[counter] -= 1
|
||||||
|
|
||||||
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
||||||
-> Generator[tuple[dict, str], None, None]:
|
-> Generator[tuple[str, str, str, str], None, None]:
|
||||||
'''Generator function yields json register struct for home-assistant
|
'''Generator function yields json register struct for home-assistant
|
||||||
auto configuration and the unique entity string, for all proxy
|
auto configuration and the unique entity string, for all proxy
|
||||||
registers
|
registers
|
||||||
@@ -403,7 +406,7 @@ class Infos:
|
|||||||
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
||||||
if 'icon' in ha:
|
if 'icon' in ha:
|
||||||
attr['ic'] = ha['icon'] # icon for the entity
|
attr['ic'] = ha['icon'] # icon for the entity
|
||||||
if 'nat_prc' in ha:
|
if 'nat_prc' in ha: # pragma: no cover
|
||||||
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
||||||
if 'ent_cat' in ha:
|
if 'ent_cat' in ha:
|
||||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[loggers]
|
[loggers]
|
||||||
keys=root,tracer,mesg,conn,data,mqtt
|
keys=root,tracer,mesg,conn,data,mqtt,asyncio
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys=console_handler,file_handler_name1,file_handler_name2
|
keys=console_handler,file_handler_name1,file_handler_name2
|
||||||
@@ -24,6 +24,12 @@ handlers=console_handler,file_handler_name1
|
|||||||
propagate=0
|
propagate=0
|
||||||
qualname=mqtt
|
qualname=mqtt
|
||||||
|
|
||||||
|
[logger_asyncio]
|
||||||
|
level=INFO
|
||||||
|
handlers=console_handler,file_handler_name1
|
||||||
|
propagate=0
|
||||||
|
qualname=asyncio
|
||||||
|
|
||||||
[logger_data]
|
[logger_data]
|
||||||
level=DEBUG
|
level=DEBUG
|
||||||
handlers=file_handler_name1
|
handlers=file_handler_name1
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
import weakref
|
import weakref
|
||||||
|
from typing import Callable, Generator
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "app.src.messages":
|
if __name__ == "app.src.messages":
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Infos
|
||||||
|
from app.src.modbus import Modbus
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
from infos import Infos
|
from infos import Infos
|
||||||
|
from modbus import Modbus
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -41,30 +46,51 @@ def hex_dump_memory(level, info, data, num):
|
|||||||
|
|
||||||
|
|
||||||
class IterRegistry(type):
|
class IterRegistry(type):
|
||||||
def __iter__(cls):
|
def __iter__(cls) -> Generator['Message', None, None]:
|
||||||
for ref in cls._registry:
|
for ref in cls._registry:
|
||||||
obj = ref()
|
obj = ref()
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
|
class State(Enum):
|
||||||
|
'''state of the logical connection'''
|
||||||
|
init = 0
|
||||||
|
'''just created'''
|
||||||
|
received = 1
|
||||||
|
'''at least one packet received'''
|
||||||
|
up = 2
|
||||||
|
'''at least one cmd-rsp transaction'''
|
||||||
|
pend = 3
|
||||||
|
'''inverter transaction pending, don't send MODBUS cmds'''
|
||||||
|
closed = 4
|
||||||
|
'''connection closed'''
|
||||||
|
|
||||||
|
|
||||||
class Message(metaclass=IterRegistry):
|
class Message(metaclass=IterRegistry):
|
||||||
_registry = []
|
_registry = []
|
||||||
|
|
||||||
def __init__(self, server_side: bool):
|
def __init__(self, server_side: bool, 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
|
||||||
|
if server_side:
|
||||||
|
self.mb = Modbus(send_modbus_cb, mb_timeout)
|
||||||
|
else:
|
||||||
|
self.mb = 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.node_id = '' # will be overwritten in the child class's __init__
|
||||||
self.sug_area = ''
|
self.sug_area = ''
|
||||||
self._recv_buffer = bytearray(0)
|
self._recv_buffer = bytearray(0)
|
||||||
self._send_buffer = bytearray(0)
|
self._send_buffer = bytearray(0)
|
||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
self.new_data = {}
|
self.new_data = {}
|
||||||
|
self.state = State.init
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Empty methods, that have to be implemented in any child class which
|
Empty methods, that have to be implemented in any child class which
|
||||||
@@ -82,6 +108,9 @@ class Message(metaclass=IterRegistry):
|
|||||||
Our puplic methods
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
if self.mb:
|
||||||
|
self.mb.close()
|
||||||
|
self.mb = None
|
||||||
pass # pragma: no cover
|
pass # pragma: no cover
|
||||||
|
|
||||||
def inc_counter(self, counter: str) -> None:
|
def inc_counter(self, counter: str) -> None:
|
||||||
|
|||||||
320
app/src/modbus.py
Normal file
320
app/src/modbus.py
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
'''MODBUS module for TSUN inverter support
|
||||||
|
|
||||||
|
TSUN uses the MODBUS in the RTU transmission mode over serial line.
|
||||||
|
see: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
|
||||||
|
see: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
|
||||||
|
|
||||||
|
A Modbus PDU consists of: 'Function-Code' + 'Data'
|
||||||
|
A Modbus RTU message consists of: 'Addr' + 'Modbus-PDU' + 'CRC-16'
|
||||||
|
The inverter is a MODBUS server and the proxy the MODBUS client.
|
||||||
|
|
||||||
|
The 16-bit CRC is known as CRC-16-ANSI(reverse)
|
||||||
|
see: https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks
|
||||||
|
'''
|
||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from typing import Generator, Callable
|
||||||
|
|
||||||
|
if __name__ == "app.src.modbus":
|
||||||
|
from app.src.infos import Register
|
||||||
|
else: # pragma: no cover
|
||||||
|
from infos import Register
|
||||||
|
|
||||||
|
logger = logging.getLogger('data')
|
||||||
|
|
||||||
|
CRC_POLY = 0xA001 # (LSBF/reverse)
|
||||||
|
CRC_INIT = 0xFFFF
|
||||||
|
|
||||||
|
|
||||||
|
class Modbus():
|
||||||
|
'''Simple MODBUS implementation with TX queue and retransmit timer'''
|
||||||
|
INV_ADDR = 1
|
||||||
|
'''MODBUS server address of the TSUN inverter'''
|
||||||
|
READ_REGS = 3
|
||||||
|
'''MODBUS function code: Read Holding Register'''
|
||||||
|
READ_INPUTS = 4
|
||||||
|
'''MODBUS function code: Read Input Register'''
|
||||||
|
WRITE_SINGLE_REG = 6
|
||||||
|
'''Modbus function code: Write Single Register'''
|
||||||
|
|
||||||
|
__crc_tab = []
|
||||||
|
map = {
|
||||||
|
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
|
# 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||||
|
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||||
|
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
|
||||||
|
# 0x300d
|
||||||
|
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
|
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3010: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3011: {'reg': Register.PV1_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3012: {'reg': Register.PV1_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3013: {'reg': Register.PV2_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3014: {'reg': Register.PV2_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3015: {'reg': Register.PV2_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3016: {'reg': Register.PV3_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3017: {'reg': Register.PV3_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3018: {'reg': Register.PV3_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x3019: {'reg': Register.PV4_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x301a: {'reg': Register.PV4_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x301b: {'reg': Register.PV4_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x301c: {'reg': Register.DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x301d: {'reg': Register.TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x301f: {'reg': Register.PV1_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3020: {'reg': Register.PV1_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3022: {'reg': Register.PV2_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3023: {'reg': Register.PV2_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x3025: {'reg': Register.PV3_DAILY_GENERATION, 'fmt': '!H', '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
|
||||||
|
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
||||||
|
timeout: int = 1):
|
||||||
|
if not len(self.__crc_tab):
|
||||||
|
self.__build_crc_tab(CRC_POLY)
|
||||||
|
self.que = asyncio.Queue(100)
|
||||||
|
self.snd_handler = snd_handler
|
||||||
|
'''Send handler to transmit a MODBUS RTU request'''
|
||||||
|
self.rsp_handler = None
|
||||||
|
'''Response handler to forward the response'''
|
||||||
|
self.timeout = timeout
|
||||||
|
'''MODBUS response timeout in seconds'''
|
||||||
|
self.max_retries = 1
|
||||||
|
'''Max retransmit for MODBUS requests'''
|
||||||
|
self.retry_cnt = 0
|
||||||
|
self.last_req = b''
|
||||||
|
self.counter = {}
|
||||||
|
'''Dictenary with statistic counter'''
|
||||||
|
self.counter['timeouts'] = 0
|
||||||
|
self.counter['retries'] = {}
|
||||||
|
for i in range(0, self.max_retries+1):
|
||||||
|
self.counter['retries'][f'{i}'] = 0
|
||||||
|
self.last_log_lvl = logging.DEBUG
|
||||||
|
self.last_addr = 0
|
||||||
|
self.last_fcode = 0
|
||||||
|
self.last_len = 0
|
||||||
|
self.last_reg = 0
|
||||||
|
self.err = 0
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
self.req_pend = False
|
||||||
|
self.tim = None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""free the queue and erase the callback handlers"""
|
||||||
|
logging.debug('Modbus close:')
|
||||||
|
self.__stop_timer()
|
||||||
|
self.rsp_handler = None
|
||||||
|
self.snd_handler = None
|
||||||
|
while not self.que.empty:
|
||||||
|
self.que.get_nowait()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""log statistics on the deleting of a MODBUS instance"""
|
||||||
|
logging.debug(f'Modbus __del__:\n {self.counter}')
|
||||||
|
|
||||||
|
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||||
|
log_lvl=logging.DEBUG) -> None:
|
||||||
|
"""Build MODBUS RTU request frame and add it to the tx queue
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
addr: RTU server address (inverter)
|
||||||
|
func: MODBUS function code
|
||||||
|
reg: 16-bit register number
|
||||||
|
val: 16 bit value
|
||||||
|
"""
|
||||||
|
msg = struct.pack('>BBHH', addr, func, reg, val)
|
||||||
|
msg += struct.pack('<H', self.__calc_crc(msg))
|
||||||
|
self.que.put_nowait({'req': msg,
|
||||||
|
'rsp_hdl': None,
|
||||||
|
'log_lvl': log_lvl})
|
||||||
|
if self.que.qsize() == 1:
|
||||||
|
self.__send_next_from_que()
|
||||||
|
|
||||||
|
def recv_req(self, buf: bytearray,
|
||||||
|
rsp_handler: Callable[[None], None] = None) -> bool:
|
||||||
|
"""Add the received Modbus RTU request to the tx queue
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
buf: Modbus RTU pdu incl ADDR byte and trailing CRC
|
||||||
|
rsp_handler: Callback, if the received pdu is valid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True: PDU was added to the queue
|
||||||
|
False: PDU was ignored, due to an error
|
||||||
|
"""
|
||||||
|
# logging.info(f'recv_req: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||||
|
if not self.__check_crc(buf):
|
||||||
|
self.err = 1
|
||||||
|
logger.error('Modbus recv: CRC error')
|
||||||
|
return False
|
||||||
|
self.que.put_nowait({'req': buf,
|
||||||
|
'rsp_hdl': rsp_handler,
|
||||||
|
'log_lvl': logging.INFO})
|
||||||
|
if self.que.qsize() == 1:
|
||||||
|
self.__send_next_from_que()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \
|
||||||
|
Generator[tuple[str, bool, int | float | str], None, None]:
|
||||||
|
"""Generator which check and parse a received MODBUS response.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
info_db: database for info lockups
|
||||||
|
buf: received Modbus RTU response frame
|
||||||
|
node_id: string for logging which identifies the slave
|
||||||
|
|
||||||
|
Returns on error and set Self.err to:
|
||||||
|
1: CRC error
|
||||||
|
2: Wrong server address
|
||||||
|
3: Unexpected function code
|
||||||
|
4: Unexpected data length
|
||||||
|
5: No MODBUS request pending
|
||||||
|
"""
|
||||||
|
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||||
|
if not self.req_pend:
|
||||||
|
self.err = 5
|
||||||
|
return
|
||||||
|
if not self.__check_crc(buf):
|
||||||
|
logger.error(f'[{node_id}] Modbus resp: CRC error')
|
||||||
|
self.err = 1
|
||||||
|
return
|
||||||
|
if buf[0] != self.last_addr:
|
||||||
|
logger.info(f'[{node_id}] Modbus resp: Wrong addr {buf[0]}')
|
||||||
|
self.err = 2
|
||||||
|
return
|
||||||
|
fcode = buf[1]
|
||||||
|
if fcode != self.last_fcode:
|
||||||
|
logger.info(f'[{node_id}] Modbus: Wrong fcode {fcode}'
|
||||||
|
f' != {self.last_fcode}')
|
||||||
|
self.err = 3
|
||||||
|
return
|
||||||
|
if self.last_addr == self.INV_ADDR and \
|
||||||
|
(fcode == 3 or fcode == 4):
|
||||||
|
elmlen = buf[2] >> 1
|
||||||
|
if elmlen != self.last_len:
|
||||||
|
logger.info(f'[{node_id}] Modbus: len error {elmlen}'
|
||||||
|
f' != {self.last_len}')
|
||||||
|
self.err = 4
|
||||||
|
return
|
||||||
|
first_reg = self.last_reg # save last_reg before sending next pdu
|
||||||
|
self.__stop_timer() # stop timer and send next pdu
|
||||||
|
|
||||||
|
for i in range(0, elmlen):
|
||||||
|
addr = first_reg+i
|
||||||
|
if addr in self.map:
|
||||||
|
row = self.map[addr]
|
||||||
|
info_id = row['reg']
|
||||||
|
fmt = row['fmt']
|
||||||
|
val = struct.unpack_from(fmt, buf, 3+2*i)
|
||||||
|
result = val[0]
|
||||||
|
|
||||||
|
if 'eval' in row:
|
||||||
|
result = eval(row['eval'])
|
||||||
|
if 'ratio' in row:
|
||||||
|
result = round(result * row['ratio'], 2)
|
||||||
|
|
||||||
|
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||||
|
|
||||||
|
if keys:
|
||||||
|
name, update = info_db.update_db(keys, must_incr,
|
||||||
|
result)
|
||||||
|
yield keys[0], update, result
|
||||||
|
if update:
|
||||||
|
info_db.tracer.log(level,
|
||||||
|
f'[{node_id}] MODBUS: {name}'
|
||||||
|
f' : {result}{unit}')
|
||||||
|
else:
|
||||||
|
self.__stop_timer()
|
||||||
|
|
||||||
|
self.counter['retries'][f'{self.retry_cnt}'] += 1
|
||||||
|
if self.rsp_handler:
|
||||||
|
self.rsp_handler()
|
||||||
|
self.__send_next_from_que()
|
||||||
|
|
||||||
|
'''
|
||||||
|
MODBUS response timer
|
||||||
|
'''
|
||||||
|
def __start_timer(self) -> None:
|
||||||
|
'''Start response timer and set `req_pend` to True'''
|
||||||
|
self.req_pend = True
|
||||||
|
self.tim = self.loop.call_later(self.timeout, self.__timeout_cb)
|
||||||
|
# logging.debug(f'Modbus start timer {self}')
|
||||||
|
|
||||||
|
def __stop_timer(self) -> None:
|
||||||
|
'''Stop response timer and set `req_pend` to False'''
|
||||||
|
self.req_pend = False
|
||||||
|
# logging.debug(f'Modbus stop timer {self}')
|
||||||
|
if self.tim:
|
||||||
|
self.tim.cancel()
|
||||||
|
self.tim = None
|
||||||
|
|
||||||
|
def __timeout_cb(self) -> None:
|
||||||
|
'''Rsponse timeout handler retransmit pdu or send next pdu'''
|
||||||
|
self.req_pend = False
|
||||||
|
|
||||||
|
if self.retry_cnt < self.max_retries:
|
||||||
|
logger.debug(f'Modbus retrans {self}')
|
||||||
|
self.retry_cnt += 1
|
||||||
|
self.__start_timer()
|
||||||
|
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
|
||||||
|
else:
|
||||||
|
logger.info(f'Modbus timeout {self}')
|
||||||
|
self.counter['timeouts'] += 1
|
||||||
|
self.__send_next_from_que()
|
||||||
|
|
||||||
|
def __send_next_from_que(self) -> None:
|
||||||
|
'''Get next MODBUS pdu from queue and transmit it'''
|
||||||
|
if self.req_pend:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
item = self.que.get_nowait()
|
||||||
|
req = item['req']
|
||||||
|
self.last_req = req
|
||||||
|
self.rsp_handler = item['rsp_hdl']
|
||||||
|
self.last_log_lvl = item['log_lvl']
|
||||||
|
self.last_addr = req[0]
|
||||||
|
self.last_fcode = req[1]
|
||||||
|
|
||||||
|
res = struct.unpack_from('>HH', req, 2)
|
||||||
|
self.last_reg = res[0]
|
||||||
|
self.last_len = res[1]
|
||||||
|
self.retry_cnt = 0
|
||||||
|
self.__start_timer()
|
||||||
|
self.snd_handler(self.last_req, self.last_log_lvl, state='Command')
|
||||||
|
except asyncio.QueueEmpty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
'''
|
||||||
|
Helper function for CRC-16 handling
|
||||||
|
'''
|
||||||
|
def __check_crc(self, msg: bytearray) -> bool:
|
||||||
|
'''Check CRC-16 and returns True if valid'''
|
||||||
|
return 0 == self.__calc_crc(msg)
|
||||||
|
|
||||||
|
def __calc_crc(self, buffer: bytearray) -> int:
|
||||||
|
'''Build CRC-16 for buffer and returns it'''
|
||||||
|
crc = CRC_INIT
|
||||||
|
|
||||||
|
for cur in buffer:
|
||||||
|
crc = (crc >> 8) ^ self.__crc_tab[(crc ^ cur) & 0xFF]
|
||||||
|
return crc
|
||||||
|
|
||||||
|
def __build_crc_tab(self, poly: int) -> None:
|
||||||
|
'''Build CRC-16 helper table, must be called exactly one time'''
|
||||||
|
for index in range(256):
|
||||||
|
data = index << 1
|
||||||
|
crc = 0
|
||||||
|
for _ in range(8, 0, -1):
|
||||||
|
data >>= 1
|
||||||
|
if (data ^ crc) & 1:
|
||||||
|
crc = (crc >> 1) ^ poly
|
||||||
|
else:
|
||||||
|
crc >>= 1
|
||||||
|
self.__crc_tab.append(crc)
|
||||||
110
app/src/mqtt.py
110
app/src/mqtt.py
@@ -1,22 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import aiomqtt
|
import aiomqtt
|
||||||
|
import traceback
|
||||||
|
from modbus import Modbus
|
||||||
|
from messages import Message
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from singleton import Singleton
|
||||||
|
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
class Singleton(type):
|
|
||||||
_instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
logger_mqtt.debug('singleton: __call__')
|
|
||||||
if cls not in cls._instances:
|
|
||||||
cls._instances[cls] = super(Singleton,
|
|
||||||
cls).__call__(*args, **kwargs)
|
|
||||||
return cls._instances[cls]
|
|
||||||
|
|
||||||
|
|
||||||
class Mqtt(metaclass=Singleton):
|
class Mqtt(metaclass=Singleton):
|
||||||
__client = None
|
__client = None
|
||||||
__cb_MqttIsUp = None
|
__cb_MqttIsUp = None
|
||||||
@@ -65,6 +58,12 @@ class Mqtt(metaclass=Singleton):
|
|||||||
password=mqtt['passwd'])
|
password=mqtt['passwd'])
|
||||||
|
|
||||||
interval = 5 # Seconds
|
interval = 5 # Seconds
|
||||||
|
ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||||
|
mb_rated_topic = "tsun/+/rated_load" # fixme
|
||||||
|
mb_reads_topic = "tsun/+/modbus_read_regs" # fixme
|
||||||
|
mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme
|
||||||
|
mb_at_cmd_topic = "tsun/+/at_cmd" # fixme
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with self.__client:
|
async with self.__client:
|
||||||
@@ -74,16 +73,36 @@ class Mqtt(metaclass=Singleton):
|
|||||||
await self.__cb_MqttIsUp()
|
await self.__cb_MqttIsUp()
|
||||||
|
|
||||||
# async with self.__client.messages() as messages:
|
# async with self.__client.messages() as messages:
|
||||||
await self.__client.subscribe(
|
await self.__client.subscribe(ha_status_topic)
|
||||||
f"{ha['auto_conf_prefix']}"
|
await self.__client.subscribe(mb_rated_topic)
|
||||||
"/status")
|
await self.__client.subscribe(mb_reads_topic)
|
||||||
|
await self.__client.subscribe(mb_inputs_topic)
|
||||||
|
await self.__client.subscribe(mb_at_cmd_topic)
|
||||||
|
|
||||||
async for message in self.__client.messages:
|
async for message in self.__client.messages:
|
||||||
status = message.payload.decode("UTF-8")
|
if message.topic.matches(ha_status_topic):
|
||||||
logger_mqtt.info('Home-Assistant Status:'
|
status = message.payload.decode("UTF-8")
|
||||||
f' {status}')
|
logger_mqtt.info('Home-Assistant Status:'
|
||||||
if status == 'online':
|
f' {status}')
|
||||||
self.ha_restarts += 1
|
if status == 'online':
|
||||||
await self.__cb_MqttIsUp()
|
self.ha_restarts += 1
|
||||||
|
await self.__cb_MqttIsUp()
|
||||||
|
|
||||||
|
if message.topic.matches(mb_rated_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.WRITE_SINGLE_REG,
|
||||||
|
1, 0x2008)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_reads_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.READ_REGS, 2)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_inputs_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.READ_INPUTS, 2)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_at_cmd_topic):
|
||||||
|
await self.at_cmd(message)
|
||||||
|
|
||||||
except aiomqtt.MqttError:
|
except aiomqtt.MqttError:
|
||||||
if Config.is_default('mqtt'):
|
if Config.is_default('mqtt'):
|
||||||
@@ -101,3 +120,54 @@ class Mqtt(metaclass=Singleton):
|
|||||||
logger_mqtt.debug("MQTT task cancelled")
|
logger_mqtt.debug("MQTT task cancelled")
|
||||||
self.__client = None
|
self.__client = None
|
||||||
return
|
return
|
||||||
|
except Exception:
|
||||||
|
# self.inc_counter('SW_Exception') # fixme
|
||||||
|
logger_mqtt.error(
|
||||||
|
f"Exception:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def each_inverter(self, message, func_name: str):
|
||||||
|
topic = str(message.topic)
|
||||||
|
node_id = topic.split('/')[1] + '/'
|
||||||
|
found = False
|
||||||
|
for m in Message:
|
||||||
|
if m.server_side and (m.node_id == node_id):
|
||||||
|
found = True
|
||||||
|
logger_mqtt.debug(f'Found: {node_id}')
|
||||||
|
fnc = getattr(m, func_name, None)
|
||||||
|
if callable(fnc):
|
||||||
|
yield fnc
|
||||||
|
else:
|
||||||
|
logger_mqtt.warning(f'Cmd not supported by: {node_id}')
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
||||||
|
|
||||||
|
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||||
|
topic = str(message.topic)
|
||||||
|
node_id = topic.split('/')[1] + '/'
|
||||||
|
# refactor into a loop over a table
|
||||||
|
payload = message.payload.decode("UTF-8")
|
||||||
|
logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}')
|
||||||
|
for m in Message:
|
||||||
|
if m.server_side and (m.node_id == node_id):
|
||||||
|
logger_mqtt.debug(f'Found: {node_id}')
|
||||||
|
fnc = getattr(m, "send_modbus_cmd", None)
|
||||||
|
res = payload.split(',')
|
||||||
|
if params != len(res):
|
||||||
|
logger_mqtt.error(f'Parameter expected: {params}, '
|
||||||
|
f'got: {len(res)}')
|
||||||
|
return
|
||||||
|
|
||||||
|
if callable(fnc):
|
||||||
|
if params == 1:
|
||||||
|
val = int(payload)
|
||||||
|
elif params == 2:
|
||||||
|
addr = int(res[0], base=16)
|
||||||
|
val = int(res[1]) # lenght
|
||||||
|
await fnc(func, addr, val, logging.INFO)
|
||||||
|
|
||||||
|
async def at_cmd(self, message):
|
||||||
|
payload = message.payload.decode("UTF-8")
|
||||||
|
for fnc in self.each_inverter(message, "send_at_cmd"):
|
||||||
|
await fnc(payload)
|
||||||
|
|||||||
35
app/src/my_timer.py
Normal file
35
app/src/my_timer.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
|
||||||
|
class Timer:
|
||||||
|
def __init__(self, cb, id_str: str = ''):
|
||||||
|
self.__timeout_cb = cb
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
self.tim = None
|
||||||
|
self.id_str = id_str
|
||||||
|
self.exp_count = count(0)
|
||||||
|
|
||||||
|
def start(self, timeout: float) -> None:
|
||||||
|
'''Start timer with timeout seconds'''
|
||||||
|
if self.tim:
|
||||||
|
self.tim.cancel()
|
||||||
|
self.tim = self.loop.call_later(timeout, self.__timeout)
|
||||||
|
logging.debug(f'[{self.id_str}]Start timer')
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
'''Stop timer'''
|
||||||
|
logging.debug(f'[{self.id_str}]Stop timer')
|
||||||
|
if self.tim:
|
||||||
|
self.tim.cancel()
|
||||||
|
self.tim = None
|
||||||
|
|
||||||
|
def __timeout(self) -> None:
|
||||||
|
'''timer expired handler'''
|
||||||
|
logging.debug(f'[{self.id_str}]Timer expired')
|
||||||
|
self.__timeout_cb(next(self.exp_count))
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.stop()
|
||||||
|
self.__timeout_cb = None
|
||||||
@@ -9,15 +9,15 @@ logger_mqtt = logging.getLogger('mqtt')
|
|||||||
|
|
||||||
class Schedule:
|
class Schedule:
|
||||||
mqtt = None
|
mqtt = None
|
||||||
|
count = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls) -> None:
|
def start(cls) -> None:
|
||||||
'''Start the scheduler and schedule the tasks (cron jobs)'''
|
'''Start the scheduler and schedule the tasks (cron jobs)'''
|
||||||
logging.info("Scheduler init")
|
logging.debug("Scheduler init")
|
||||||
cls.mqtt = Mqtt(None)
|
cls.mqtt = Mqtt(None)
|
||||||
|
|
||||||
crontab('0 0 * * *', func=cls.atmidnight, start=True)
|
crontab('0 0 * * *', func=cls.atmidnight, start=True)
|
||||||
# crontab('*/5 * * * *', func=cls.atmidnight, start=True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def atmidnight(cls) -> None:
|
async def atmidnight(cls) -> None:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import signal
|
import signal
|
||||||
import functools
|
|
||||||
import os
|
import os
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from aiohttp import web
|
||||||
from logging import config # noqa F401
|
from logging import config # noqa F401
|
||||||
from messages import Message
|
from messages import Message
|
||||||
from inverter import Inverter
|
from inverter import Inverter
|
||||||
@@ -11,38 +12,114 @@ from gen3plus.inverter_g3p import InverterG3P
|
|||||||
from scheduler import Schedule
|
from scheduler import Schedule
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
routes = web.RouteTableDef()
|
||||||
|
proxy_is_up = False
|
||||||
|
|
||||||
async def handle_client(reader, writer):
|
|
||||||
|
@routes.get('/')
|
||||||
|
async def hello(request):
|
||||||
|
return web.Response(text="Hello, world")
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/-/ready')
|
||||||
|
async def ready(request):
|
||||||
|
if proxy_is_up:
|
||||||
|
status = 200
|
||||||
|
text = 'Is ready'
|
||||||
|
else:
|
||||||
|
status = 503
|
||||||
|
text = 'Not ready'
|
||||||
|
return web.Response(status=status, text=text)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/-/healthy')
|
||||||
|
async def healthy(request):
|
||||||
|
|
||||||
|
if proxy_is_up:
|
||||||
|
# logging.info('web reqeust healthy()')
|
||||||
|
for stream in Message:
|
||||||
|
try:
|
||||||
|
res = stream.healthy()
|
||||||
|
if not res:
|
||||||
|
return web.Response(status=503, text="I have a problem")
|
||||||
|
except Exception as err:
|
||||||
|
logging.info(f'Exception:{err}')
|
||||||
|
|
||||||
|
return web.Response(status=200, text="I'm fine")
|
||||||
|
|
||||||
|
|
||||||
|
async def webserver(addr, port):
|
||||||
|
'''coro running our webserver'''
|
||||||
|
app = web.Application()
|
||||||
|
app.add_routes(routes)
|
||||||
|
runner = web.AppRunner(app)
|
||||||
|
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, addr, port)
|
||||||
|
await site.start()
|
||||||
|
logging.info(f'HTTP server listen on port: {port}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Normal interaction with aiohttp
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600) # sleep forever
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logging.info('HTTP server cancelled')
|
||||||
|
await runner.cleanup()
|
||||||
|
logging.debug('HTTP cleanup done')
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_client(reader: StreamReader, writer: StreamWriter):
|
||||||
'''Handles a new incoming connection and starts an async loop'''
|
'''Handles a new incoming connection and starts an async loop'''
|
||||||
|
|
||||||
addr = writer.get_extra_info('peername')
|
addr = writer.get_extra_info('peername')
|
||||||
await InverterG3(reader, writer, addr).server_loop(addr)
|
await InverterG3(reader, writer, addr).server_loop(addr)
|
||||||
|
|
||||||
|
|
||||||
async def handle_client_v2(reader, writer):
|
async def handle_client_v2(reader: StreamReader, writer: StreamWriter):
|
||||||
'''Handles a new incoming connection and starts an async loop'''
|
'''Handles a new incoming connection and starts an async loop'''
|
||||||
|
|
||||||
addr = writer.get_extra_info('peername')
|
addr = writer.get_extra_info('peername')
|
||||||
await InverterG3P(reader, writer, addr).server_loop(addr)
|
await InverterG3P(reader, writer, addr).server_loop(addr)
|
||||||
|
|
||||||
|
|
||||||
def handle_SIGTERM(loop):
|
async def handle_shutdown(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')
|
||||||
|
global proxy_is_up
|
||||||
|
proxy_is_up = False
|
||||||
|
|
||||||
#
|
#
|
||||||
# first, close all open TCP connections
|
# first, disc all open TCP connections gracefully
|
||||||
|
#
|
||||||
|
for stream in Message:
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(stream.disc(), 2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logging.info('Proxy disconnecting done')
|
||||||
|
|
||||||
|
#
|
||||||
|
# second, close all open TCP connections
|
||||||
#
|
#
|
||||||
for stream in Message:
|
for stream in Message:
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
#
|
await asyncio.sleep(0.1) # give time for closing
|
||||||
# at last, we stop the loop
|
logging.info('Proxy closing done')
|
||||||
#
|
|
||||||
loop.stop()
|
|
||||||
|
|
||||||
logging.info('Shutdown complete')
|
#
|
||||||
|
# third, cancel the web server
|
||||||
|
#
|
||||||
|
web_task.cancel()
|
||||||
|
await web_task
|
||||||
|
|
||||||
|
#
|
||||||
|
# at last, start a coro for stopping the loop
|
||||||
|
#
|
||||||
|
logging.debug("Stop event loop")
|
||||||
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
def get_log_level() -> int:
|
def get_log_level() -> int:
|
||||||
@@ -74,39 +151,48 @@ if __name__ == "__main__":
|
|||||||
logging.getLogger('msg').setLevel(log_level)
|
logging.getLogger('msg').setLevel(log_level)
|
||||||
logging.getLogger('conn').setLevel(log_level)
|
logging.getLogger('conn').setLevel(log_level)
|
||||||
logging.getLogger('data').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)
|
# logging.getLogger('mqtt').setLevel(log_level)
|
||||||
|
|
||||||
# read config file
|
|
||||||
Config.read()
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# read config file
|
||||||
|
ConfigErr = Config.class_init()
|
||||||
|
if ConfigErr is not None:
|
||||||
|
logging.info(f'ConfigErr: {ConfigErr}')
|
||||||
Inverter.class_init()
|
Inverter.class_init()
|
||||||
Schedule.start()
|
Schedule.start()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Create tasks for our listening servers. These must be tasks! If we call
|
||||||
|
# start_server directly out of our main task, the eventloop will be blocked
|
||||||
|
# and we can't receive and handle the UNIX signals!
|
||||||
|
#
|
||||||
|
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
||||||
|
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
||||||
|
web_task = loop.create_task(webserver('0.0.0.0', 8127))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Register some UNIX Signal handler for a gracefully server shutdown
|
# Register some UNIX Signal handler for a gracefully server shutdown
|
||||||
# on Docker restart and stop
|
# on Docker restart and stop
|
||||||
#
|
#
|
||||||
for signame in ('SIGINT', 'SIGTERM'):
|
for signame in ('SIGINT', 'SIGTERM'):
|
||||||
loop.add_signal_handler(getattr(signal, signame),
|
loop.add_signal_handler(getattr(signal, signame),
|
||||||
functools.partial(handle_SIGTERM, loop))
|
lambda loop=loop: asyncio.create_task(
|
||||||
|
handle_shutdown(web_task)))
|
||||||
#
|
|
||||||
# Create taska for our listening servera. These must be tasks! If we call
|
|
||||||
# start_server directly out of our main task, the eventloop will be blocked
|
|
||||||
# and we can't receive and handle the UNIX signals!
|
|
||||||
#
|
|
||||||
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
|
||||||
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
|
||||||
|
|
||||||
|
loop.set_debug(log_level == logging.DEBUG)
|
||||||
try:
|
try:
|
||||||
|
if ConfigErr is None:
|
||||||
|
proxy_is_up = True
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
logging.info("Event loop is stopped")
|
||||||
Inverter.class_close(loop)
|
Inverter.class_close(loop)
|
||||||
logging.info('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}"')
|
||||||
|
|||||||
9
app/src/singleton.py
Normal file
9
app/src/singleton.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Singleton(type):
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
# logger_mqtt.debug('singleton: __call__')
|
||||||
|
if cls not in cls._instances:
|
||||||
|
cls._instances[cls] = super(Singleton,
|
||||||
|
cls).__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
||||||
142
app/tests/test_config.py
Normal file
142
app/tests/test_config.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# test_with_pytest.py
|
||||||
|
import tomllib
|
||||||
|
from schema import SchemaMissingKeyError
|
||||||
|
from app.src.config import Config
|
||||||
|
|
||||||
|
class TstConfig(Config):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set(cls, cnf):
|
||||||
|
cls.config = cnf
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_config_file(cls) -> dict:
|
||||||
|
return cls.config
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_config():
|
||||||
|
cnf = {}
|
||||||
|
try:
|
||||||
|
Config.conf_schema.validate(cnf)
|
||||||
|
assert False
|
||||||
|
except SchemaMissingKeyError:
|
||||||
|
assert True
|
||||||
|
|
||||||
|
def test_default_config():
|
||||||
|
with open("app/config/default_config.toml", "rb") as f:
|
||||||
|
cnf = tomllib.load(f)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validated = Config.conf_schema.validate(cnf)
|
||||||
|
assert True
|
||||||
|
except:
|
||||||
|
assert False
|
||||||
|
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
|
|
||||||
|
def test_full_config():
|
||||||
|
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||||
|
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []},
|
||||||
|
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}},
|
||||||
|
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||||
|
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||||
|
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'inverters': {'allow_all': True,
|
||||||
|
'R170000000000001': {'node_id': '', 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
|
||||||
|
'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
|
try:
|
||||||
|
validated = Config.conf_schema.validate(cnf)
|
||||||
|
assert True
|
||||||
|
except:
|
||||||
|
assert False
|
||||||
|
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
|
|
||||||
|
def test_mininum_config():
|
||||||
|
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||||
|
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+']},
|
||||||
|
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']}}},
|
||||||
|
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||||
|
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||||
|
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
|
'inverters': {'allow_all': True,
|
||||||
|
'R170000000000001': {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
validated = Config.conf_schema.validate(cnf)
|
||||||
|
assert True
|
||||||
|
except:
|
||||||
|
assert False
|
||||||
|
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}}}
|
||||||
|
|
||||||
|
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': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
|
|
||||||
|
defcnf = TstConfig.def_config.get('solarman')
|
||||||
|
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
|
assert True == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
|
def test_no_file():
|
||||||
|
cnf = {}
|
||||||
|
TstConfig.set(cnf)
|
||||||
|
err = TstConfig.read('')
|
||||||
|
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||||
|
cnf = TstConfig.get()
|
||||||
|
assert cnf == {}
|
||||||
|
defcnf = TstConfig.def_config.get('solarman')
|
||||||
|
assert defcnf == None
|
||||||
|
|
||||||
|
def test_read_cnf1():
|
||||||
|
cnf = {'solarman' : {'enabled': False}}
|
||||||
|
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': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
|
cnf = TstConfig.get('solarman')
|
||||||
|
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
|
defcnf = TstConfig.def_config.get('solarman')
|
||||||
|
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
|
assert False == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
|
def test_read_cnf2():
|
||||||
|
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
||||||
|
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': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
|
assert True == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
|
def test_read_cnf3():
|
||||||
|
cnf = {'solarman' : {'port': 'FALSE'}}
|
||||||
|
TstConfig.set(cnf)
|
||||||
|
err = TstConfig.read('app/config/')
|
||||||
|
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||||
|
cnf = TstConfig.get()
|
||||||
|
assert cnf == {'solarman': {'port': 'FALSE'}}
|
||||||
|
|
||||||
|
def test_read_cnf4():
|
||||||
|
cnf = {'solarman' : {'port': 5000}}
|
||||||
|
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': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
|
assert False == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
|
def test_read_cnf5():
|
||||||
|
cnf = {'solarman' : {'port': 1023}}
|
||||||
|
TstConfig.set(cnf)
|
||||||
|
err = TstConfig.read('app/config/')
|
||||||
|
assert err != None
|
||||||
|
|
||||||
|
def test_read_cnf6():
|
||||||
|
cnf = {'solarman' : {'port': 65536}}
|
||||||
|
TstConfig.set(cnf)
|
||||||
|
err = TstConfig.read('app/config/')
|
||||||
|
assert err != None
|
||||||
@@ -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}})
|
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}})
|
||||||
|
|
||||||
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}})
|
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}})
|
||||||
val = i.dev_value(Register.INVERTER_CNT)
|
val = i.dev_value(Register.INVERTER_CNT)
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,82 @@ def InvDataSeq2(): # Data indication from the controller
|
|||||||
msg += b'\x53\x00\x00'
|
msg += b'\x53\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InvDataNew(): # Data indication from DSP V5.0.17
|
||||||
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x91\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01'
|
||||||
|
msg += b'\x95\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\x80\x53\x00\x00\x00\x00\x01\x90\x41\x00\x00'
|
||||||
|
msg += b'\x01\x94\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x96'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\xa0\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\xf0\x41\x00\x00\x01\xf1\x53\x00\x00\x00\x00\x01'
|
||||||
|
msg += b'\xf4\x53\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf8\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x53\x00\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x00\x58'
|
||||||
|
msg += b'\x41\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02'
|
||||||
|
msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x41\x00\x00\x02\x59\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00\x02\x44'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x45\x53\x00\x00\x00\x00\x02\x60\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x60\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\x20\x41\x00\x00\x02\x24\x53\x00\x00\x00\x00\x02\x24\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x26\x53\x00\x00\x00\x00\x02\x40\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x80\x41\x00\x00\x02\x81'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x84\x53\x00\x00\x00\x00\x02\x85\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02'
|
||||||
|
msg += b'\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x80\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\xc8\x42\x00\x00\x00\x00\x48\x42\x00\x00\x00\x00\x80'
|
||||||
|
msg += b'\x42\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x01\x20\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x01\x84\x53\x00\x10\x00\x00\x02\x40\x46\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x04\x04\x46\x02\x00\x46\x02\x00\x00\x04\x00\x46\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x05\x04\x42\x00\x00\x00\x05\x50\x42\x00\x00\x00'
|
||||||
|
msg += b'\x00\x14\x42\x00\x00\x00\x00\x00\x46\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\xa4\x46\x00\x00\x00\x00\x00\x00\x01\x00\x46\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x44\x46\x00\x00\x00\x00\x00\x00\x02\x00\x46\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00\x08\x90\x46\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x08\x54\x46\x00\x00\x00\x00\x00\x00\x09\x20'
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x08\x00\x46\x00\x00\x00\x00\x00\x00\x08\x84\x46\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x08\x40\x46\x00\x00\x00\x00\x00\x00\x09\x04\x46\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x0a\x10\x46\x00\x00\x00\x00\x00\x00\x0c\x14\x46'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x00\x00\x00\x00\x00\x00\x0c'
|
||||||
|
msg += b'\x24\x42\x00\x00\x00\x0d\x00\x42\x00\x00\x00\x00\x04\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x42\x00\x00\x00\x00\x44\x42\x00\x00\x00\x00\x10\x42'
|
||||||
|
msg += b'\x00\x00\x00\x01\x14\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x40\x53\x00\x00\x00\x00\x10'
|
||||||
|
msg += b'\x04\x53\x00\x00\x00\x00\x11\x00\x53\x00\x00\x00\x00\x11\x84\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x10\x50\x53\xff\xff\x00\x00\x10\x14\x53\x03\x20'
|
||||||
|
msg += b'\x00\x00\x10\x00\x53\x00\x00\x00\x00\x11\x24\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x03\x00\x53\x00\x00\x00\x00\x03\x64\x53\x00\x00\x00\x00\x04\x50'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x00\x34\x53\x00\x00\x00\x00\x00\x00\x42\x02'
|
||||||
|
msg += b'\x00\x00\x01\x04\x42\x00\x00\x00\x21\x00\x42\x00\x00\x00\x21\x44'
|
||||||
|
msg += b'\x42\x00\x00\x00\x22\x10\x53\x00\x00\x00\x00\x28\x14\x42\x01\x00'
|
||||||
|
msg += b'\x00\x28\xa0\x46\x42\x48\x00\x00\x00\x00\x29\x04\x42\x00\x00\x00'
|
||||||
|
msg += b'\x29\x40\x42\x00\x00\x00\x28\x04\x46\x42\x10\x00\x00\x00\x00\x28'
|
||||||
|
msg += b'\x00\x42\x00\x00\x00\x28\x84\x42\x00\x00\x00\x28\x50\x42\x00\x00'
|
||||||
|
msg += b'\x00\x29\x14\x42\x00\x00\x00\x2a\x00\x42\x00\x00\x00\x2c\x24\x46'
|
||||||
|
msg += b'\x42\x10\x00\x00\x00\x00\x2c\x80\x42\x00\x00\x00\x2c\x44\x53\x00'
|
||||||
|
msg += b'\x02\x00\x00\x2d\x00\x42\x00\x00\x00\x20\x04\x46\x42\x4d\x00\x00'
|
||||||
|
msg += b'\x00\x00\x20\x10\x42\x00\x00\x00\x20\x54\x42\x00\x00\x00\x20\x20'
|
||||||
|
msg += b'\x42\x00\x00\x00\x21\x04\x53\x00\x01\x00\x00\x22\x00\x42\x00\x00'
|
||||||
|
msg += b'\x00\x30\x04\x42\x00\x00\x00\x30\x40\x53\x00\x00\x00\x00\x30\x04'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x31\x10\x42\x00\x00\x00\x31\x94\x53\x00\x04'
|
||||||
|
msg += b'\x00\x00\x30\x00\x53\x00\x00\x00\x00\x30\x24\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x30\x00\x53\x00\x00\x00\x00\x31\x04\x53\x00\x00\x00\x00\x31\x80'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x32\x44\x53\x00\x00\x00\x00\x30\x00\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x30\x80\x53\x00\x00\x00\x00\x30\x00\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x30\x80\x53\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def InvDataSeq2_Zero(): # Data indication from the controller
|
def InvDataSeq2_Zero(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||||
@@ -391,6 +467,25 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_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}})
|
||||||
|
|
||||||
|
def test_new_data_types(InvDataNew):
|
||||||
|
i = InfosG3()
|
||||||
|
tests = 0
|
||||||
|
for key, update in i.parse (InvDataNew):
|
||||||
|
if key == 'events':
|
||||||
|
tests +=1
|
||||||
|
elif key == 'inverter':
|
||||||
|
assert update == True
|
||||||
|
tests +=1
|
||||||
|
elif key == 'input':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
assert tests==15
|
||||||
|
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
||||||
|
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
||||||
|
|
||||||
def test_invalid_data_type(InvalidDataSeq):
|
def test_invalid_data_type(InvalidDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def test_parse_4110(DeviceData: bytes):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 60, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Adress": "192.168.80.49"},
|
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49"},
|
||||||
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
|
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ def test_parse_4210(InverterData: bytes):
|
|||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
"controller": {"Power_On_Time": 2051},
|
"controller": {"Power_On_Time": 2051},
|
||||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "v4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4},
|
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4},
|
||||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||||
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||||
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
||||||
|
|||||||
380
app/tests/test_modbus.py
Normal file
380
app/tests/test_modbus.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# test_with_pytest.py
|
||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
from app.src.modbus import Modbus
|
||||||
|
from app.src.infos import Infos, Register
|
||||||
|
|
||||||
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
# pytestmark = pytest.mark.asyncio(scope="module")
|
||||||
|
|
||||||
|
class ModbusTestHelper(Modbus):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(self.send_cb)
|
||||||
|
self.db = Infos()
|
||||||
|
self.pdu = None
|
||||||
|
self.send_calls = 0
|
||||||
|
self.recv_responses = 0
|
||||||
|
def send_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||||
|
self.pdu = pdu
|
||||||
|
self.send_calls += 1
|
||||||
|
def resp_handler(self):
|
||||||
|
self.recv_responses += 1
|
||||||
|
|
||||||
|
def test_modbus_crc():
|
||||||
|
'''Check CRC-16 calculation'''
|
||||||
|
mb = Modbus(None)
|
||||||
|
assert 0x0b02 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04')
|
||||||
|
assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b')
|
||||||
|
assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b')
|
||||||
|
|
||||||
|
assert 0xc803 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00')
|
||||||
|
assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8')
|
||||||
|
assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8')
|
||||||
|
|
||||||
|
assert 0x5c75 == mb._Modbus__calc_crc(b'\x01\x03\x08\x01\x2c\x00\x2c\x02\x2c\x2c\x46')
|
||||||
|
|
||||||
|
def test_build_modbus_pdu():
|
||||||
|
'''Check building and sending a MODBUS RTU'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,6,0x2000,0x12)
|
||||||
|
assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07'
|
||||||
|
assert mb._Modbus__check_crc(mb.pdu)
|
||||||
|
assert mb.last_addr == 1
|
||||||
|
assert mb.last_fcode == 6
|
||||||
|
assert mb.last_reg == 0x2000
|
||||||
|
assert mb.last_len == 18
|
||||||
|
assert mb.err == 0
|
||||||
|
|
||||||
|
def test_recv_req():
|
||||||
|
'''Receive a valid request, which must transmitted'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
assert mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07')
|
||||||
|
assert mb.last_fcode == 6
|
||||||
|
assert mb.last_reg == 0x2000
|
||||||
|
assert mb.last_len == 0x12
|
||||||
|
assert mb.err == 0
|
||||||
|
|
||||||
|
def test_recv_req_crc_err():
|
||||||
|
'''Receive a request with invalid CRC, which must be dropped'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
assert not mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08')
|
||||||
|
assert mb.pdu == None
|
||||||
|
assert mb.last_fcode == 0
|
||||||
|
assert mb.last_reg == 0
|
||||||
|
assert mb.last_len == 0
|
||||||
|
assert mb.err == 1
|
||||||
|
|
||||||
|
def test_recv_resp_crc_err():
|
||||||
|
'''Receive a response with invalid CRC, which must be dropped'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
# simulate a transmitted request
|
||||||
|
mb.req_pend = True
|
||||||
|
mb.last_addr = 1
|
||||||
|
mb.last_fcode = 3
|
||||||
|
mb.last_reg == 0x300e
|
||||||
|
mb.last_len == 2
|
||||||
|
# check matching response, but with CRC error
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
|
||||||
|
call += 1
|
||||||
|
assert mb.err == 1
|
||||||
|
assert 0 == call
|
||||||
|
assert mb.req_pend == True
|
||||||
|
# cleanup queue
|
||||||
|
mb._Modbus__stop_timer()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_recv_resp_invalid_addr():
|
||||||
|
'''Receive a response with wrong server addr, which must be dropped'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.req_pend = True
|
||||||
|
# simulate a transmitted request
|
||||||
|
mb.last_addr = 1
|
||||||
|
mb.last_fcode = 3
|
||||||
|
mb.last_reg == 0x300e
|
||||||
|
mb.last_len == 2
|
||||||
|
|
||||||
|
# check not matching response, with wrong server addr
|
||||||
|
call = 0
|
||||||
|
for key, update in mb.recv_resp(mb.db, b'\x02\x03\x04\x01\x2c\x00\x46\x88\xf4', 'test'):
|
||||||
|
call += 1
|
||||||
|
assert mb.err == 2
|
||||||
|
assert 0 == call
|
||||||
|
assert mb.req_pend == True
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
|
||||||
|
# cleanup queue
|
||||||
|
mb._Modbus__stop_timer()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_recv_recv_fcode():
|
||||||
|
'''Receive a response with wrong function code, which must be dropped'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,4,0x300e,2)
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.req_pend
|
||||||
|
|
||||||
|
# check not matching response, with wrong function code
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||||
|
call += 1
|
||||||
|
|
||||||
|
assert mb.err == 3
|
||||||
|
assert 0 == call
|
||||||
|
assert mb.req_pend == True
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
|
||||||
|
# cleanup queue
|
||||||
|
mb._Modbus__stop_timer()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_recv_resp_len():
|
||||||
|
'''Receive a response with wrong data length, which must be dropped'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,3,0x300e,3)
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.last_len == 3
|
||||||
|
|
||||||
|
# check not matching response, with wrong data length
|
||||||
|
call = 0
|
||||||
|
for key, update, _ in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||||
|
call += 1
|
||||||
|
|
||||||
|
assert mb.err == 4
|
||||||
|
assert 0 == call
|
||||||
|
assert mb.req_pend == True
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
|
||||||
|
# cleanup queue
|
||||||
|
mb._Modbus__stop_timer()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_recv_unexpect_resp():
|
||||||
|
'''Receive a response when we havb't sent a request'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
# check unexpected response, which must be dropped
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||||
|
call += 1
|
||||||
|
|
||||||
|
assert mb.err == 5
|
||||||
|
assert 0 == call
|
||||||
|
assert mb.req_pend == False
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
|
||||||
|
def test_parse_resp():
|
||||||
|
'''Receive matching response and parse the values'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,3,0x3007,6)
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.req_pend
|
||||||
|
|
||||||
|
call = 0
|
||||||
|
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||||
|
if key == 'grid':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'inverter':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == True
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
assert exp_result[call] == val
|
||||||
|
call += 1
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert 5 == call
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_queue():
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,3,0x3022,4)
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.req_pend
|
||||||
|
|
||||||
|
assert mb.send_calls == 1
|
||||||
|
assert mb.pdu == b'\x01\x030"\x00\x04\xeb\x03'
|
||||||
|
mb.pdu = None
|
||||||
|
assert mb.send_calls == 1
|
||||||
|
assert mb.pdu == None
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
|
||||||
|
# cleanup queue
|
||||||
|
mb._Modbus__stop_timer()
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_queue2():
|
||||||
|
'''Check queue handling for build_msg() calls'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.build_msg(1,3,0x3007,6)
|
||||||
|
mb.build_msg(1,6,0x2008,4)
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.req_pend
|
||||||
|
mb.build_msg(1,3,0x3007,6)
|
||||||
|
assert mb.que.qsize() == 2
|
||||||
|
assert mb.req_pend
|
||||||
|
|
||||||
|
assert mb.send_calls == 1
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
call = 0
|
||||||
|
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||||
|
if key == 'grid':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'inverter':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == True
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
assert exp_result[call] == val
|
||||||
|
call += 1
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert 5 == call
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.send_calls == 2
|
||||||
|
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||||
|
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.send_calls == 3
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||||
|
call += 1
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert 5 == call
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
def test_queue3():
|
||||||
|
'''Check queue handling for recv_req() calls'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
assert mb.recv_req(b'\x01\x03\x30\x07\x00\x06{\t', mb.resp_handler)
|
||||||
|
assert mb.recv_req(b'\x01\x06\x20\x08\x00\x04\x02\x0b', mb.resp_handler)
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.recv_req(b'\x01\x03\x30\x07\x00\x06{\t')
|
||||||
|
assert mb.que.qsize() == 2
|
||||||
|
assert mb.req_pend
|
||||||
|
|
||||||
|
assert mb.send_calls == 1
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
assert mb.recv_responses == 0
|
||||||
|
|
||||||
|
call = 0
|
||||||
|
exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||||
|
if key == 'grid':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'inverter':
|
||||||
|
assert update == True
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == True
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
assert exp_result[call] == val
|
||||||
|
call += 1
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert 5 == call
|
||||||
|
assert mb.recv_responses == 1
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.send_calls == 2
|
||||||
|
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||||
|
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||||
|
pass
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert mb.recv_responses == 2
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.send_calls == 3
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||||
|
call += 1
|
||||||
|
assert 0 == mb.err
|
||||||
|
assert mb.recv_responses == 2
|
||||||
|
assert 5 == call
|
||||||
|
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_timeout():
|
||||||
|
'''Test MODBUS response timeout and RTU retransmitting'''
|
||||||
|
assert asyncio.get_running_loop()
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
mb.max_retries = 2
|
||||||
|
mb.timeout = 0.1 # 100ms timeout for fast testing, expect a time resolution of at least 10ms
|
||||||
|
assert asyncio.get_running_loop() == mb.loop
|
||||||
|
mb.build_msg(1,3,0x3007,6)
|
||||||
|
mb.build_msg(1,6,0x2008,4)
|
||||||
|
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.retry_cnt == 0
|
||||||
|
assert mb.send_calls == 1
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
|
||||||
|
mb.pdu = None
|
||||||
|
await asyncio.sleep(0.11) # wait for first timeout and retransmittion
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.retry_cnt == 1
|
||||||
|
assert mb.send_calls == 2
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
|
||||||
|
mb.pdu = None
|
||||||
|
await asyncio.sleep(0.11) # wait for second timeout and retransmittion
|
||||||
|
assert mb.que.qsize() == 1
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.retry_cnt == 2
|
||||||
|
assert mb.send_calls == 3
|
||||||
|
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||||
|
|
||||||
|
mb.pdu = None
|
||||||
|
await asyncio.sleep(0.11) # wait for third timeout and next pdu
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert mb.req_pend
|
||||||
|
assert mb.retry_cnt == 0
|
||||||
|
assert mb.send_calls == 4
|
||||||
|
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||||
|
|
||||||
|
mb.max_retries = 0 # next pdu without retranmsission
|
||||||
|
await asyncio.sleep(0.11) # wait for fourth timout
|
||||||
|
assert mb.que.qsize() == 0
|
||||||
|
assert not mb.req_pend
|
||||||
|
assert mb.retry_cnt == 0
|
||||||
|
assert mb.send_calls == 4
|
||||||
|
|
||||||
|
# assert mb.counter == {}
|
||||||
|
|
||||||
|
def test_recv_unknown_data():
|
||||||
|
'''Receive a response with an unknwon register'''
|
||||||
|
mb = ModbusTestHelper()
|
||||||
|
assert 0x9000 not in mb.map
|
||||||
|
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
||||||
|
|
||||||
|
mb.build_msg(1,3,0x9000,2)
|
||||||
|
|
||||||
|
# check matching response, but with CRC error
|
||||||
|
call = 0
|
||||||
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||||
|
call += 1
|
||||||
|
assert mb.err == 0
|
||||||
|
assert 0 == call
|
||||||
|
assert not mb.req_pend
|
||||||
|
|
||||||
|
del mb.map[0x9000]
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||||
from app.src.config import Config
|
from app.src.config import Config
|
||||||
from app.src.infos import Infos, Register
|
from app.src.infos import Infos, Register
|
||||||
|
from app.src.modbus import Modbus
|
||||||
|
from app.src.messages import State
|
||||||
|
|
||||||
|
|
||||||
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
# initialize the proxy statistics
|
# initialize the proxy statistics
|
||||||
Infos.static_init()
|
Infos.static_init()
|
||||||
@@ -12,9 +18,31 @@ Infos.static_init()
|
|||||||
timestamp = int(time.time()) # 1712861197
|
timestamp = int(time.time()) # 1712861197
|
||||||
heartbeat = 60
|
heartbeat = 60
|
||||||
|
|
||||||
|
class Writer():
|
||||||
|
def __init__(self):
|
||||||
|
self.sent_pdu = b''
|
||||||
|
|
||||||
|
def write(self, pdu: bytearray):
|
||||||
|
self.sent_pdu = pdu
|
||||||
|
|
||||||
|
|
||||||
|
class Mqtt():
|
||||||
|
def __init__(self):
|
||||||
|
self.key = ''
|
||||||
|
self.data = ''
|
||||||
|
|
||||||
|
async def publish(self, key, data):
|
||||||
|
self.key = key
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
super().__init__(server_side)
|
super().__init__(server_side)
|
||||||
|
if server_side:
|
||||||
|
self.mb.timeout = 1 # overwrite for faster testing
|
||||||
|
self.writer = Writer()
|
||||||
|
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
|
||||||
@@ -24,18 +52,27 @@ class MemoryStream(SolarmanV5):
|
|||||||
self.addr = 'Test: SrvSide'
|
self.addr = 'Test: SrvSide'
|
||||||
self.db.stat['proxy']['Invalid_Msg_Format'] = 0
|
self.db.stat['proxy']['Invalid_Msg_Format'] = 0
|
||||||
self.db.stat['proxy']['AT_Command'] = 0
|
self.db.stat['proxy']['AT_Command'] = 0
|
||||||
|
self.db.stat['proxy']['AT_Command_Blocked'] = 0
|
||||||
|
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.key = ''
|
||||||
|
self.data = ''
|
||||||
|
|
||||||
def _timestamp(self):
|
def _timestamp(self):
|
||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
def _heartbeat(self) -> int:
|
def _heartbeat(self) -> int:
|
||||||
return heartbeat
|
return heartbeat
|
||||||
|
|
||||||
|
|
||||||
def append_msg(self, msg):
|
def append_msg(self, msg):
|
||||||
self.__msg += msg
|
self.__msg += msg
|
||||||
self.__msg_len += len(msg)
|
self.__msg_len += len(msg)
|
||||||
|
|
||||||
|
def publish_mqtt(self, key, data):
|
||||||
|
self.key = key
|
||||||
|
self.data = data
|
||||||
|
|
||||||
def _read(self) -> int:
|
def _read(self) -> int:
|
||||||
copied_bytes = 0
|
copied_bytes = 0
|
||||||
try:
|
try:
|
||||||
@@ -54,6 +91,16 @@ class MemoryStream(SolarmanV5):
|
|||||||
pass
|
pass
|
||||||
return copied_bytes
|
return copied_bytes
|
||||||
|
|
||||||
|
async def async_write(self, headline=''):
|
||||||
|
if self.test_exception_async_write:
|
||||||
|
raise RuntimeError("Peer closed.")
|
||||||
|
|
||||||
|
def createClientStream(self, msg, chunks = (0,)):
|
||||||
|
c = MemoryStream(msg, chunks, False)
|
||||||
|
self.remoteStream = c
|
||||||
|
c. remoteStream = self
|
||||||
|
return c
|
||||||
|
|
||||||
def _SolarmanV5__flush_recv_msg(self) -> None:
|
def _SolarmanV5__flush_recv_msg(self) -> None:
|
||||||
super()._SolarmanV5__flush_recv_msg()
|
super()._SolarmanV5__flush_recv_msg()
|
||||||
self.msg_count += 1
|
self.msg_count += 1
|
||||||
@@ -63,6 +110,9 @@ class MemoryStream(SolarmanV5):
|
|||||||
def get_sn() -> bytes:
|
def get_sn() -> bytes:
|
||||||
return b'\x21\x43\x65\x7b'
|
return b'\x21\x43\x65\x7b'
|
||||||
|
|
||||||
|
def get_sn_int() -> int:
|
||||||
|
return 2070233889
|
||||||
|
|
||||||
def get_inv_no() -> bytes:
|
def get_inv_no() -> bytes:
|
||||||
return b'T170000000000001'
|
return b'T170000000000001'
|
||||||
|
|
||||||
@@ -308,6 +358,72 @@ def InverterIndMsg2000(): # 0x4210 rated Power 2000W
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterIndMsg800(): # 0x4210 rated Power 800W
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
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\x36\x00\x00\x03\x20\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x03\x20\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'\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'\x04\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 InverterIndMsg_81(): # 0x4210 fcode 0x81
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\x02\x03' +get_sn() +b'\x81\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x07\x04\x03\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
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\x36\x00\x00\x02\x58\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
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'\x04\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
|
@pytest.fixture
|
||||||
def InverterRspMsg(): # 0x1210
|
def InverterRspMsg(): # 0x1210
|
||||||
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
|
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
|
||||||
@@ -317,6 +433,15 @@ def InverterRspMsg(): # 0x1210
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterRspMsg_81(): # 0x1210 fcode 0x81
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x12\x03\03' +get_sn() +b'\x81\x01'
|
||||||
|
msg += total()
|
||||||
|
msg += hb()
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def UnknownMsg(): # 0x5110
|
def UnknownMsg(): # 0x5110
|
||||||
msg = b'\xa5\x0a\x00\x10\x51\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
|
msg = b'\xa5\x0a\x00\x10\x51\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
|
||||||
@@ -346,7 +471,7 @@ def SyncStartRspMsg(): # 0x1310
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def SyncStartFwdMsg(): # 0x4310
|
def SyncStartFwdMsg(): # 0x4310
|
||||||
msg = b'\xa5\x2f\x00\x10\x43\x0e\x0d' +get_sn() +b'\x81\x7a\x0b\x2e\x32'
|
msg = b'\xa5\x2f\x00\x10\x43\x0d\x0e' +get_sn() +b'\x81\x7a\x0b\x2e\x32'
|
||||||
msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73'
|
msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73'
|
||||||
msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x61\x01'
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x61\x01'
|
||||||
@@ -357,18 +482,28 @@ def SyncStartFwdMsg(): # 0x4310
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def AtCommandIndMsg(): # 0x4510
|
def AtCommandIndMsg(): # 0x4510
|
||||||
msg = b'\xa5\x27\x00\x10\x45\x02\x01' +get_sn() +b'\x01\x02\x00'
|
msg = b'\xa5\x27\x00\x10\x45\x03\x02' +get_sn() +b'\x01\x02\x00'
|
||||||
msg += b'\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'
|
||||||
msg += b'AT+TIME=214028,1,60,120\r'
|
msg += b'AT+TIME=214028,1,60,120\r'
|
||||||
msg += correct_checksum(msg)
|
msg += correct_checksum(msg)
|
||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def AtCommandIndMsgBlock(): # 0x4510
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x01\x02\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'AT+WEBU\r'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def AtCommandRspMsg(): # 0x1510
|
def AtCommandRspMsg(): # 0x1510
|
||||||
msg = b'\xa5\x0a\x00\x10\x15\x02\x02' +get_sn() +b'\x01\x01'
|
msg = b'\xa5\x11\x00\x10\x15\x03\x03' +get_sn() +b'\x01\x01'
|
||||||
msg += total()
|
msg += total()
|
||||||
msg += hb()
|
msg += hb()
|
||||||
|
msg += b'\x00\x00\x00\x00+ok'
|
||||||
msg += correct_checksum(msg)
|
msg += correct_checksum(msg)
|
||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
@@ -410,6 +545,81 @@ def SyncEndRspMsg(): # 0x1810
|
|||||||
msg += b'\x15'
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusCmd():
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusCmdFwd():
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x01\x00' +get_sn() +b'\x02\xb0\x02'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusCmdCrcErr():
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x04\xc8'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusRsp(): # 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\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 MsgModbusInvalid(): # 0x1510
|
||||||
|
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00'
|
||||||
|
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 MsgUnknownCmd():
|
||||||
|
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x03\xb0\x02'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgUnknownCmdRsp(): # 0x1510
|
||||||
|
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x03\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
|
@pytest.fixture
|
||||||
def ConfigTsunAllowAll():
|
def ConfigTsunAllowAll():
|
||||||
Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
|
Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
|
||||||
@@ -642,8 +852,7 @@ def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
|
|||||||
assert m.data_len == 0xd4
|
assert m.data_len == 0xd4
|
||||||
assert m.msg_count == 1
|
assert m.msg_count == 1
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
while m.read(): # read rest of message
|
m.read() # read rest of message
|
||||||
pass
|
|
||||||
assert m.msg_count == 1
|
assert m.msg_count == 1
|
||||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
@@ -693,6 +902,52 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, Inver
|
|||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_read_two_messages2(ConfigTsunAllowAll, InverterIndMsg, InverterIndMsg_81, InverterRspMsg, InverterRspMsg_81):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg, (0,))
|
||||||
|
m.append_msg(InverterIndMsg_81)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert m.time_ofs == 0x33e447a0
|
||||||
|
assert str(m.seq) == '02:02'
|
||||||
|
assert m.data_len == 0x199
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m._forward_buffer==InverterIndMsg
|
||||||
|
assert m._send_buffer==InverterRspMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._recv_buffer==InverterIndMsg_81
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear forward buffer for next test
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert m.time_ofs == 0x33e447a0
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.data_len == 0x199
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m._forward_buffer==InverterIndMsg_81
|
||||||
|
assert m._send_buffer==InverterRspMsg_81
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_unkown_message(ConfigTsunInv1, UnknownMsg):
|
def test_unkown_message(ConfigTsunInv1, UnknownMsg):
|
||||||
ConfigTsunInv1
|
ConfigTsunInv1
|
||||||
m = MemoryStream(UnknownMsg, (0,))
|
m = MemoryStream(UnknownMsg, (0,))
|
||||||
@@ -725,7 +980,7 @@ def test_device_rsp(ConfigTsunInv1, DeviceRspMsg):
|
|||||||
assert m.data_len == 0x0a
|
assert m.data_len == 0x0a
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m._forward_buffer==b'' # DeviceRspMsg
|
assert m._forward_buffer==b''
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -743,7 +998,7 @@ def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg):
|
|||||||
assert m.data_len == 0x0a
|
assert m.data_len == 0x0a
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m._forward_buffer==b'' # InverterRspMsg
|
assert m._forward_buffer==b''
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -779,7 +1034,7 @@ def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg):
|
|||||||
assert m.data_len == 0x0a
|
assert m.data_len == 0x0a
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m._forward_buffer==b'' # HeartbeatRspMsg
|
assert m._forward_buffer==b''
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -800,6 +1055,7 @@ def test_sync_start_ind(ConfigTsunInv1, SyncStartIndMsg, SyncStartRspMsg, SyncSt
|
|||||||
assert m._forward_buffer==SyncStartIndMsg
|
assert m._forward_buffer==SyncStartIndMsg
|
||||||
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._update_header(m._forward_buffer)
|
m._update_header(m._forward_buffer)
|
||||||
assert str(m.seq) == '0d:0e' # value after forwarding indication
|
assert str(m.seq) == '0d:0e' # value after forwarding indication
|
||||||
assert m._forward_buffer==SyncStartFwdMsg
|
assert m._forward_buffer==SyncStartFwdMsg
|
||||||
@@ -820,7 +1076,7 @@ def test_sync_start_rsp(ConfigTsunInv1, SyncStartRspMsg):
|
|||||||
assert m.data_len == 0x0a
|
assert m.data_len == 0x0a
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m._forward_buffer==b'' # HeartbeatRspMsg
|
assert m._forward_buffer==b''
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -856,29 +1112,10 @@ def test_sync_end_rsp(ConfigTsunInv1, SyncEndRspMsg):
|
|||||||
assert m.data_len == 0x0a
|
assert m.data_len == 0x0a
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m._forward_buffer==b'' # HeartbeatRspMsg
|
assert m._forward_buffer==b''
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_at_command_ind(ConfigTsunInv1, AtCommandIndMsg, AtCommandRspMsg):
|
|
||||||
ConfigTsunInv1
|
|
||||||
m = MemoryStream(AtCommandIndMsg, (0,), False)
|
|
||||||
m.read() # read complete msg, and dispatch msg
|
|
||||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
|
||||||
assert m.msg_count == 1
|
|
||||||
assert m.header_len==11
|
|
||||||
assert m.snr == 2070233889
|
|
||||||
# assert m.unique_id == '2070233889'
|
|
||||||
assert m.control == 0x4510
|
|
||||||
assert str(m.seq) == '02:02'
|
|
||||||
assert m.data_len == 39
|
|
||||||
assert m._recv_buffer==b''
|
|
||||||
assert m._send_buffer==AtCommandRspMsg
|
|
||||||
assert m._forward_buffer==AtCommandIndMsg
|
|
||||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
|
||||||
assert m.db.stat['proxy']['AT_Command'] == 1
|
|
||||||
m.close()
|
|
||||||
|
|
||||||
def test_build_modell_600(ConfigTsunAllowAll, InverterIndMsg):
|
def test_build_modell_600(ConfigTsunAllowAll, InverterIndMsg):
|
||||||
ConfigTsunAllowAll
|
ConfigTsunAllowAll
|
||||||
m = MemoryStream(InverterIndMsg, (0,))
|
m = MemoryStream(InverterIndMsg, (0,))
|
||||||
@@ -931,6 +1168,18 @@ def test_build_modell_2000(ConfigTsunAllowAll, InverterIndMsg2000):
|
|||||||
assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_800(ConfigTsunAllowAll, InverterIndMsg800):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg800, (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 800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 800 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MSxx00' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
|
def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
|
||||||
ConfigTsunAllowAll
|
ConfigTsunAllowAll
|
||||||
m = MemoryStream(DeviceIndMsg, (0,))
|
m = MemoryStream(DeviceIndMsg, (0,))
|
||||||
@@ -942,21 +1191,531 @@ def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
|
|||||||
assert 'V1.1.00.0B' == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0).rstrip('\00')
|
assert 'V1.1.00.0B' == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0).rstrip('\00')
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, AtCommandIndMsg):
|
def test_msg_iterator():
|
||||||
ConfigTsunAllowAll
|
m1 = SolarmanV5(server_side=True)
|
||||||
|
m2 = SolarmanV5(server_side=True)
|
||||||
|
m3 = SolarmanV5(server_side=True)
|
||||||
|
m3.close()
|
||||||
|
del m3
|
||||||
|
test1 = 0
|
||||||
|
test2 = 0
|
||||||
|
for key in SolarmanV5:
|
||||||
|
if key == m1:
|
||||||
|
test1+=1
|
||||||
|
elif key == m2:
|
||||||
|
test2+=1
|
||||||
|
elif type(key) != SolarmanV5:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
assert test1 == 1
|
||||||
|
assert test2 == 1
|
||||||
|
|
||||||
|
def test_proxy_counter():
|
||||||
|
m = SolarmanV5(server_side=True)
|
||||||
|
assert m.new_data == {}
|
||||||
|
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
|
||||||
|
m.inc_counter('Unknown_Msg')
|
||||||
|
assert m.new_data == {}
|
||||||
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
|
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
|
|
||||||
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
m.dec_counter('Unknown_Msg')
|
||||||
|
assert m.new_data == {}
|
||||||
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
|
assert 0 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, MsgModbusCmd):
|
||||||
|
ConfigTsunInv1
|
||||||
m = MemoryStream(DeviceIndMsg, (0,), True)
|
m = MemoryStream(DeviceIndMsg, (0,), True)
|
||||||
|
m.append_msg(InverterIndMsg)
|
||||||
m.read()
|
m.read()
|
||||||
assert m.control == 0x4110
|
assert m.control == 0x4110
|
||||||
assert str(m.seq) == '01:01'
|
assert str(m.seq) == '01:01'
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==InverterIndMsg # unhandled next message
|
||||||
assert m._send_buffer==DeviceRspMsg
|
assert m._send_buffer==DeviceRspMsg
|
||||||
assert m._forward_buffer==DeviceIndMsg
|
assert m._forward_buffer==DeviceIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert m._recv_buffer==InverterIndMsg # unhandled next message
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m.writer.sent_pdu == b'' # modbus command must be ignore, cause connection is still not up
|
||||||
|
assert m._send_buffer == b'' # modbus command must be ignore, cause connection is still not up
|
||||||
|
|
||||||
|
m.read()
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert str(m.seq) == '02:02'
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==InverterRspMsg
|
||||||
|
assert m._forward_buffer==InverterIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m.writer.sent_pdu == MsgModbusCmd
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m.test_exception_async_write = True
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg, AtCommandRspMsg):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,), True)
|
||||||
|
m.append_msg(InverterIndMsg)
|
||||||
|
m.append_msg(AtCommandRspMsg)
|
||||||
|
m.read() # read device ind
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert str(m.seq) == '01:01'
|
||||||
|
assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
|
||||||
|
assert m._send_buffer==DeviceRspMsg
|
||||||
|
assert m._forward_buffer==DeviceIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
await m.send_at_cmd('AT+TIME=214028,1,60,120')
|
||||||
|
assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert str(m.seq) == '01:01'
|
||||||
|
assert m.mqtt.key == ''
|
||||||
|
assert m.mqtt.data == ""
|
||||||
|
|
||||||
|
m.read() # read inverter ind
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert str(m.seq) == '02:02'
|
||||||
|
assert m._recv_buffer==AtCommandRspMsg # unhandled next message
|
||||||
|
assert m._send_buffer==InverterRspMsg
|
||||||
|
assert m._forward_buffer==InverterIndMsg
|
||||||
|
|
||||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
m.send_at_cmd('AT+TIME=214028,1,60,120')
|
await m.send_at_cmd('AT+TIME=214028,1,60,120')
|
||||||
assert m._recv_buffer==b''
|
assert m._recv_buffer==AtCommandRspMsg # unhandled next message
|
||||||
assert m._send_buffer==AtCommandIndMsg
|
assert m._send_buffer==AtCommandIndMsg
|
||||||
assert m._forward_buffer==b''
|
assert m._forward_buffer==b''
|
||||||
assert str(m.seq) == '01:02'
|
assert str(m.seq) == '02:03'
|
||||||
|
assert m.mqtt.key == ''
|
||||||
|
assert m.mqtt.data == ""
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m.read() # read at resp
|
||||||
|
assert m.control == 0x1510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.key == 'at_resp'
|
||||||
|
assert m.data == "+ok"
|
||||||
|
|
||||||
|
m.test_exception_async_write = True
|
||||||
|
await m.send_at_cmd('AT+TIME=214028,1,60,120')
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert str(m.seq) == '03:04'
|
||||||
|
assert m.forward_at_cmd_resp == False
|
||||||
|
assert m.mqtt.key == ''
|
||||||
|
assert m.mqtt.data == ""
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,), True)
|
||||||
|
m.append_msg(InverterIndMsg)
|
||||||
|
m.read()
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert str(m.seq) == '01:01'
|
||||||
|
assert m._recv_buffer==InverterIndMsg # unhandled next message
|
||||||
|
assert m._send_buffer==DeviceRspMsg
|
||||||
|
assert m._forward_buffer==DeviceIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
await m.send_at_cmd('AT+WEBU')
|
||||||
|
assert m._recv_buffer==InverterIndMsg # unhandled next message
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert str(m.seq) == '01:01'
|
||||||
|
assert m.mqtt.key == ''
|
||||||
|
assert m.mqtt.data == ""
|
||||||
|
|
||||||
|
m.read()
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert str(m.seq) == '02:02'
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==InverterRspMsg
|
||||||
|
assert m._forward_buffer==InverterIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
await m.send_at_cmd('AT+WEBU')
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert str(m.seq) == '02:02'
|
||||||
|
assert m.forward_at_cmd_resp == False
|
||||||
|
assert m.mqtt.key == 'at_resp'
|
||||||
|
assert m.mqtt.data == "'AT+WEBU' is forbidden"
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(AtCommandIndMsg, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['AT_Command'] = 0
|
||||||
|
m.db.stat['proxy']['AT_Command_Blocked'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
# assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4510
|
||||||
|
assert str(m.seq) == '03:02'
|
||||||
|
assert m.data_len == 39
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==AtCommandIndMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command'] == 1
|
||||||
|
assert m.db.stat['proxy']['AT_Command_Blocked'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_AT_cmd_ind_block(ConfigTsunInv1, AtCommandIndMsgBlock):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(AtCommandIndMsgBlock, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['AT_Command'] = 0
|
||||||
|
m.db.stat['proxy']['AT_Command_Blocked'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
# assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4510
|
||||||
|
assert str(m.seq) == '03:02'
|
||||||
|
assert m.data_len == 23
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command_Blocked'] == 1
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_at_command_rsp1(ConfigTsunInv1, AtCommandRspMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(AtCommandRspMsg)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 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 == 0x1510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==17
|
||||||
|
assert m._forward_buffer==AtCommandRspMsg
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_at_command_rsp2(ConfigTsunInv1, AtCommandRspMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(AtCommandRspMsg)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.forward_at_cmd_resp = False
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.control == 0x1510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==17
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd, MsgModbusCmdFwd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.snr = get_sn_int()
|
||||||
|
m.state = State.up
|
||||||
|
c = m.createClientStream(MsgModbusCmd)
|
||||||
|
|
||||||
|
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._forward_buffer==b''
|
||||||
|
assert c._send_buffer==b''
|
||||||
|
assert m.writer.sent_pdu == MsgModbusCmdFwd
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.snr = get_sn_int()
|
||||||
|
m.state = State.up
|
||||||
|
c = m.createClientStream(MsgModbusCmdCrcErr)
|
||||||
|
|
||||||
|
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._forward_buffer==b''
|
||||||
|
assert c._send_buffer==b''
|
||||||
|
assert m.writer.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'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_unknown_cmd_req(ConfigTsunInv1, MsgUnknownCmd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgUnknownCmd, (0,), False)
|
||||||
|
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
|
||||||
|
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 == 0x4510
|
||||||
|
assert str(m.seq) == '03:02'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==23
|
||||||
|
assert m._forward_buffer==MsgUnknownCmd
|
||||||
|
assert m._send_buffer==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_rsp1(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
'''Modbus response without a valid Modbus request must be dropped'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusRsp)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.control == 0x1510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==59
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
'''Modbus response with a valid Modbus request must be forwarded'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusRsp)
|
||||||
|
m.append_msg(MsgModbusRsp)
|
||||||
|
|
||||||
|
m.mb.rsp_handler = m._SolarmanV5__forward_msg
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
# assert m.db.db == {'inverter': {'Manufacturer': 'TSUN', 'Equipment_Model': 'TSOL-MSxx00'}}
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m._forward_buffer==MsgModbusRsp
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
# assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
|
||||||
|
assert m.new_data['inverter'] == True
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.mb.req_pend = 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.mb.err == 0
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m._forward_buffer==MsgModbusRsp
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
# assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
|
||||||
|
assert m.new_data['inverter'] == False
|
||||||
|
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
'''Modbus response with a valid Modbus request must be forwarded'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusRsp)
|
||||||
|
m.append_msg(MsgModbusRsp)
|
||||||
|
|
||||||
|
m.mb.rsp_handler = m._SolarmanV5__forward_msg
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
# assert m.db.db == {'inverter': {'Manufacturer': 'TSUN', 'Equipment_Model': 'TSOL-MSxx00'}}
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m._forward_buffer==MsgModbusRsp
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
# assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
|
||||||
|
assert m.new_data['inverter'] == True
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 5
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m._forward_buffer==MsgModbusRsp
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
# assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
|
||||||
|
assert m.new_data['inverter'] == False
|
||||||
|
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_unknown_rsp(ConfigTsunInv1, MsgUnknownCmdRsp):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgUnknownCmdRsp)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.control == 0x1510
|
||||||
|
assert str(m.seq) == '03:03'
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.data_len==59
|
||||||
|
assert m._forward_buffer==MsgUnknownCmdRsp
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInvalid):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusInvalid, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
ConfigTsunInv1
|
||||||
|
# receive more bytes than expected (7 bytes from the next msg)
|
||||||
|
m = MemoryStream(MsgModbusRsp+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.mb.rsp_handler = m._SolarmanV5__forward_msg
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
|
||||||
|
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._forward_buffer==MsgModbusRsp
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.modbus_elms == 20-1 # register 0x300d is unknown, so one value can't be mapped
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
'''
|
||||||
|
def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
m1 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
m2 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
m3 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
assert m1.state == m1.State.init
|
||||||
|
assert m2.state == m2.State.init
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m1.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.init
|
||||||
|
assert m2.state == m2.State.init
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m2.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.closed
|
||||||
|
assert m2.state == m2.State.init
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m3.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.closed
|
||||||
|
assert m2.state == m2.State.closed
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m1.close()
|
||||||
|
m2.close()
|
||||||
|
m3.close()
|
||||||
|
'''
|
||||||
@@ -2,16 +2,32 @@
|
|||||||
import pytest, logging
|
import pytest, logging
|
||||||
from app.src.gen3.talent import Talent, Control
|
from app.src.gen3.talent import Talent, Control
|
||||||
from app.src.config import Config
|
from app.src.config import Config
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Infos, Register
|
||||||
|
from app.src.modbus import Modbus
|
||||||
|
from app.src.messages import State
|
||||||
|
|
||||||
|
|
||||||
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
# initialize the proxy statistics
|
# initialize the proxy statistics
|
||||||
Infos.static_init()
|
Infos.static_init()
|
||||||
|
|
||||||
tracer = logging.getLogger('tracer')
|
tracer = logging.getLogger('tracer')
|
||||||
|
|
||||||
|
|
||||||
|
class Writer():
|
||||||
|
def __init__(self):
|
||||||
|
self.sent_pdu = b''
|
||||||
|
|
||||||
|
def write(self, pdu: bytearray):
|
||||||
|
self.sent_pdu = pdu
|
||||||
|
|
||||||
class MemoryStream(Talent):
|
class MemoryStream(Talent):
|
||||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||||
super().__init__(server_side)
|
super().__init__(server_side)
|
||||||
|
if server_side:
|
||||||
|
self.mb.timeout = 1 # overwrite for faster testing
|
||||||
|
self.writer = Writer()
|
||||||
self.__msg = msg
|
self.__msg = msg
|
||||||
self.__msg_len = len(msg)
|
self.__msg_len = len(msg)
|
||||||
self.__chunks = chunks
|
self.__chunks = chunks
|
||||||
@@ -19,6 +35,8 @@ class MemoryStream(Talent):
|
|||||||
self.__chunk_idx = 0
|
self.__chunk_idx = 0
|
||||||
self.msg_count = 0
|
self.msg_count = 0
|
||||||
self.addr = 'Test: SrvSide'
|
self.addr = 'Test: SrvSide'
|
||||||
|
self.send_msg_ofs = 0
|
||||||
|
self.test_exception_async_write = False
|
||||||
|
|
||||||
def append_msg(self, msg):
|
def append_msg(self, msg):
|
||||||
self.__msg += msg
|
self.__msg += msg
|
||||||
@@ -43,13 +61,25 @@ class MemoryStream(Talent):
|
|||||||
return copied_bytes
|
return copied_bytes
|
||||||
|
|
||||||
def _timestamp(self):
|
def _timestamp(self):
|
||||||
return 1700260990000
|
# return 1700260990000
|
||||||
|
return 1691246944000
|
||||||
|
|
||||||
|
def createClientStream(self, msg, chunks = (0,)):
|
||||||
|
c = MemoryStream(msg, chunks, False)
|
||||||
|
self.remoteStream = c
|
||||||
|
c. remoteStream = self
|
||||||
|
return c
|
||||||
|
|
||||||
def _Talent__flush_recv_msg(self) -> None:
|
def _Talent__flush_recv_msg(self) -> None:
|
||||||
super()._Talent__flush_recv_msg()
|
super()._Talent__flush_recv_msg()
|
||||||
self.msg_count += 1
|
self.msg_count += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def async_write(self, headline=''):
|
||||||
|
if self.test_exception_async_write:
|
||||||
|
raise RuntimeError("Peer closed.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def MsgContactInfo(): # Contact Info message
|
def MsgContactInfo(): # Contact Info message
|
||||||
@@ -85,6 +115,10 @@ def MsgGetTime(): # Get Time Request message
|
|||||||
def MsgTimeResp(): # Get Time Resonse message
|
def MsgTimeResp(): # Get Time Resonse message
|
||||||
return b'\x00\x00\x00\x1b\x10R170000000000001\x91\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
|
return b'\x00\x00\x00\x1b\x10R170000000000001\x91\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgTimeRespInv(): # Get Time Resonse message
|
||||||
|
return b'\x00\x00\x00\x17\x10R170000000000001\x91\x22\x00\x00\x01\x89'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def MsgTimeInvalid(): # Get Time Request message
|
def MsgTimeInvalid(): # Get Time Request message
|
||||||
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22'
|
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22'
|
||||||
@@ -101,6 +135,18 @@ def MsgControllerInd(): # Data indication from the controller
|
|||||||
msg += b'\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50\x59\x49\x00\x00\x00\x4c\x00\x0d\x1f\x60\x49\x00\x00\x00\x00'
|
msg += b'\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50\x59\x49\x00\x00\x00\x4c\x00\x0d\x1f\x60\x49\x00\x00\x00\x00'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgControllerIndTsOffs(): # Data indication from the controller - offset 0x1000
|
||||||
|
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
|
||||||
|
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x45\x50'
|
||||||
|
msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
||||||
|
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
|
||||||
|
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
|
||||||
|
msg += b'\x00\x00\x64\x00\x0c\x96\xa8\x49\x00\x00\x00\x1d\x00\x0c\x7f\x38\x49\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00\x00\x13\x8d\x00\x09\x5b\x50'
|
||||||
|
msg += b'\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50\x59\x49\x00\x00\x00\x4c\x00\x0d\x1f\x60\x49\x00\x00\x00\x00'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def MsgControllerAck(): # Get Time Request message
|
def MsgControllerAck(): # Get Time Request message
|
||||||
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x71\x01'
|
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x71\x01'
|
||||||
@@ -117,6 +163,92 @@ def MsgInverterInd(): # Data indication from the controller
|
|||||||
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgInverterIndTsOffs(): # Data indication from the controller + offset 256
|
||||||
|
msg = b'\x00\x00\x00\x8b\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||||
|
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x62\x08'
|
||||||
|
msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||||
|
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgInverterIndNew(): # Data indication from DSP V5.0.17
|
||||||
|
msg = b'\x00\x00\x04\xa0\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||||
|
msg += b'\x01\x00\x00\x01'
|
||||||
|
msg += b'\x90\x31\x4d\x68\x78\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x91\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01'
|
||||||
|
msg += b'\x95\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\x80\x53\x00\x00\x00\x00\x01\x90\x41\x00\x00'
|
||||||
|
msg += b'\x01\x94\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x96'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\xa0\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\xf0\x41\x00\x00\x01\xf1\x53\x00\x00\x00\x00\x01'
|
||||||
|
msg += b'\xf4\x53\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf8\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x01\x53\x00\x00\x00\x00\x00\x00\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x53\x00\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x00\x58'
|
||||||
|
msg += b'\x41\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02'
|
||||||
|
msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x41\x00\x00\x02\x59\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x02\x40\x53\x00\x00\x00\x00\x02\x41\x53\x00\x00\x00\x00\x02\x44'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x45\x53\x00\x00\x00\x00\x02\x60\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x60\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\x20\x41\x00\x00\x02\x24\x53\x00\x00\x00\x00\x02\x24\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x26\x53\x00\x00\x00\x00\x02\x40\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\x40\x53\x00\x00\x00\x00\x02\x80\x41\x00\x00\x02\x81'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x02\x84\x53\x00\x00\x00\x00\x02\x85\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02'
|
||||||
|
msg += b'\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x80\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x02\xc8\x42\x00\x00\x00\x00\x48\x42\x00\x00\x00\x00\x80'
|
||||||
|
msg += b'\x42\x00\x00\x00\x00\x04\x53\x00\x00\x00\x00\x01\x20\x53\x00\x00'
|
||||||
|
msg += b'\x00\x00\x01\x84\x53\x00\x10\x00\x00\x02\x40\x46\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x04\x04\x46\x02\x00\x46\x02\x00\x00\x04\x00\x46\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x05\x04\x42\x00\x00\x00\x05\x50\x42\x00\x00\x00'
|
||||||
|
msg += b'\x00\x14\x42\x00\x00\x00\x00\x00\x46\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\xa4\x46\x00\x00\x00\x00\x00\x00\x01\x00\x46\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x44\x46\x00\x00\x00\x00\x00\x00\x02\x00\x46\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00\x08\x90\x46\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x08\x54\x46\x00\x00\x00\x00\x00\x00\x09\x20'
|
||||||
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x08\x04\x46\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x08\x00\x46\x00\x00\x00\x00\x00\x00\x08\x84\x46\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x08\x40\x46\x00\x00\x00\x00\x00\x00\x09\x04\x46\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x0a\x10\x46\x00\x00\x00\x00\x00\x00\x0c\x14\x46'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x00\x00\x00\x00\x00\x00\x0c'
|
||||||
|
msg += b'\x24\x42\x00\x00\x00\x0d\x00\x42\x00\x00\x00\x00\x04\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x42\x00\x00\x00\x00\x44\x42\x00\x00\x00\x00\x10\x42'
|
||||||
|
msg += b'\x00\x00\x00\x01\x14\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x40\x53\x00\x00\x00\x00\x10'
|
||||||
|
msg += b'\x04\x53\x00\x00\x00\x00\x11\x00\x53\x00\x00\x00\x00\x11\x84\x53'
|
||||||
|
msg += b'\x00\x00\x00\x00\x10\x50\x53\xff\xff\x00\x00\x10\x14\x53\x03\x20'
|
||||||
|
msg += b'\x00\x00\x10\x00\x53\x00\x00\x00\x00\x11\x24\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x03\x00\x53\x00\x00\x00\x00\x03\x64\x53\x00\x00\x00\x00\x04\x50'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x00\x34\x53\x00\x00\x00\x00\x00\x00\x42\x02'
|
||||||
|
msg += b'\x00\x00\x01\x04\x42\x00\x00\x00\x21\x00\x42\x00\x00\x00\x21\x44'
|
||||||
|
msg += b'\x42\x00\x00\x00\x22\x10\x53\x00\x00\x00\x00\x28\x14\x42\x01\x00'
|
||||||
|
msg += b'\x00\x28\xa0\x46\x42\x48\x00\x00\x00\x00\x29\x04\x42\x00\x00\x00'
|
||||||
|
msg += b'\x29\x40\x42\x00\x00\x00\x28\x04\x46\x42\x10\x00\x00\x00\x00\x28'
|
||||||
|
msg += b'\x00\x42\x00\x00\x00\x28\x84\x42\x00\x00\x00\x28\x50\x42\x00\x00'
|
||||||
|
msg += b'\x00\x29\x14\x42\x00\x00\x00\x2a\x00\x42\x00\x00\x00\x2c\x24\x46'
|
||||||
|
msg += b'\x42\x10\x00\x00\x00\x00\x2c\x80\x42\x00\x00\x00\x2c\x44\x53\x00'
|
||||||
|
msg += b'\x02\x00\x00\x2d\x00\x42\x00\x00\x00\x20\x04\x46\x42\x4d\x00\x00'
|
||||||
|
msg += b'\x00\x00\x20\x10\x42\x00\x00\x00\x20\x54\x42\x00\x00\x00\x20\x20'
|
||||||
|
msg += b'\x42\x00\x00\x00\x21\x04\x53\x00\x01\x00\x00\x22\x00\x42\x00\x00'
|
||||||
|
msg += b'\x00\x30\x04\x42\x00\x00\x00\x30\x40\x53\x00\x00\x00\x00\x30\x04'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x31\x10\x42\x00\x00\x00\x31\x94\x53\x00\x04'
|
||||||
|
msg += b'\x00\x00\x30\x00\x53\x00\x00\x00\x00\x30\x24\x53\x00\x00\x00\x00'
|
||||||
|
msg += b'\x30\x00\x53\x00\x00\x00\x00\x31\x04\x53\x00\x00\x00\x00\x31\x80'
|
||||||
|
msg += b'\x53\x00\x00\x00\x00\x32\x44\x53\x00\x00\x00\x00\x30\x00\x53\x00'
|
||||||
|
msg += b'\x00\x00\x00\x30\x80\x53\x00\x00\x00\x00\x30\x00\x53\x00\x00\x00'
|
||||||
|
msg += b'\x00\x30\x80\x53\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def MsgInverterAck(): # Get Time Request message
|
def MsgInverterAck(): # Get Time Request message
|
||||||
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
|
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
|
||||||
@@ -170,6 +302,42 @@ def MsgOtaAck(): # Over the air update rewuest from tsun cloud
|
|||||||
def MsgOtaInvalid(): # Get Time Request message
|
def MsgOtaInvalid(): # Get Time Request message
|
||||||
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x13\x01'
|
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x13\x01'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusCmd():
|
||||||
|
msg = b'\x00\x00\x00\x20\x10R170000000000001'
|
||||||
|
msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusCmdCrcErr():
|
||||||
|
msg = b'\x00\x00\x00\x20\x10R170000000000001'
|
||||||
|
msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x04\xc8'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusRsp():
|
||||||
|
msg = b'\x00\x00\x00\x20\x10R170000000000001'
|
||||||
|
msg += b'\x91\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusInv():
|
||||||
|
msg = b'\x00\x00\x00\x20\x10R170000000000001'
|
||||||
|
msg += b'\x99\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08'
|
||||||
|
msg += b'\x00\x00\x03\xc8'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgModbusResp20():
|
||||||
|
msg = b'\x00\x00\x00\x45\x10R170000000000001'
|
||||||
|
msg += b'\x91\x77\x17\x18\x19\x1a\x2d\x01\x03\x28\x51'
|
||||||
|
msg += b'\x09\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00'
|
||||||
|
msg += b'\x08\x00\x00\x00\x00\x01\x59\x01\x21\x03\xe6\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\xdb\x6b'
|
||||||
|
return msg
|
||||||
|
|
||||||
def test_read_message(MsgContactInfo):
|
def test_read_message(MsgContactInfo):
|
||||||
m = MemoryStream(MsgContactInfo, (0,))
|
m = MemoryStream(MsgContactInfo, (0,))
|
||||||
@@ -267,8 +435,7 @@ def test_read_message_in_chunks2(MsgContactInfo):
|
|||||||
assert int(m.ctrl)==145
|
assert int(m.ctrl)==145
|
||||||
assert m.msg_id==0
|
assert m.msg_id==0
|
||||||
assert m.msg_count == 1
|
assert m.msg_count == 1
|
||||||
while m.read(): # read rest of message
|
m.read() # read rest of message
|
||||||
pass
|
|
||||||
assert m.msg_count == 1
|
assert m.msg_count == 1
|
||||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
m.close()
|
m.close()
|
||||||
@@ -407,9 +574,10 @@ def test_msg_get_time(ConfigTsunInv1, MsgGetTime):
|
|||||||
assert int(m.ctrl)==145
|
assert int(m.ctrl)==145
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==0
|
||||||
assert m.data_len==0
|
assert m.data_len==0
|
||||||
assert m._forward_buffer==MsgGetTime
|
assert m._forward_buffer==MsgGetTime
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -425,9 +593,10 @@ def test_msg_get_time_autark(ConfigNoTsunInv1, MsgGetTime):
|
|||||||
assert int(m.ctrl)==145
|
assert int(m.ctrl)==145
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==0
|
||||||
assert m.data_len==0
|
assert m.data_len==0
|
||||||
assert m._forward_buffer==b''
|
assert m._forward_buffer==b''
|
||||||
assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x8b\xdfs\xcc0'
|
assert m._send_buffer==bytearray(b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00')
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -443,8 +612,9 @@ def test_msg_time_resp(ConfigTsunInv1, MsgTimeResp):
|
|||||||
assert int(m.ctrl)==145
|
assert int(m.ctrl)==145
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==3600000
|
||||||
assert m.data_len==8
|
assert m.data_len==8
|
||||||
assert m._forward_buffer==MsgTimeResp
|
assert m._forward_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
@@ -461,12 +631,32 @@ def test_msg_time_resp_autark(ConfigNoTsunInv1, MsgTimeResp):
|
|||||||
assert int(m.ctrl)==145
|
assert int(m.ctrl)==145
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==3600000
|
||||||
assert m.data_len==8
|
assert m.data_len==8
|
||||||
assert m._forward_buffer==b''
|
assert m._forward_buffer==b''
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_time_inv_resp(ConfigTsunInv1, MsgTimeRespInv):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgTimeRespInv, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==145
|
||||||
|
assert m.msg_id==34
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==0
|
||||||
|
assert m.data_len==4
|
||||||
|
assert m._forward_buffer==MsgTimeRespInv
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
|
def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
|
||||||
ConfigTsunInv1
|
ConfigTsunInv1
|
||||||
m = MemoryStream(MsgTimeInvalid, (0,), False)
|
m = MemoryStream(MsgTimeInvalid, (0,), False)
|
||||||
@@ -479,6 +669,7 @@ def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
|
|||||||
assert int(m.ctrl)==148
|
assert int(m.ctrl)==148
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
|
assert m.ts_offset==0
|
||||||
assert m.data_len==0
|
assert m.data_len==0
|
||||||
assert m._forward_buffer==MsgTimeInvalid
|
assert m._forward_buffer==MsgTimeInvalid
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
@@ -496,6 +687,7 @@ def test_msg_time_invalid_autark(ConfigNoTsunInv1, MsgTimeInvalid):
|
|||||||
assert m.unique_id == 'R170000000000001'
|
assert m.unique_id == 'R170000000000001'
|
||||||
assert int(m.ctrl)==148
|
assert int(m.ctrl)==148
|
||||||
assert m.msg_id==34
|
assert m.msg_id==34
|
||||||
|
assert m.ts_offset==0
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==0
|
assert m.data_len==0
|
||||||
assert m._forward_buffer==b''
|
assert m._forward_buffer==b''
|
||||||
@@ -503,7 +695,7 @@ def test_msg_time_invalid_autark(ConfigNoTsunInv1, MsgTimeInvalid):
|
|||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerAck):
|
def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerIndTsOffs, MsgControllerAck):
|
||||||
ConfigTsunInv1
|
ConfigTsunInv1
|
||||||
m = MemoryStream(MsgControllerInd, (0,))
|
m = MemoryStream(MsgControllerInd, (0,))
|
||||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
@@ -516,7 +708,12 @@ def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerAck):
|
|||||||
assert m.msg_id==113
|
assert m.msg_id==113
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==284
|
assert m.data_len==284
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgControllerInd
|
assert m._forward_buffer==MsgControllerInd
|
||||||
|
m.ts_offset = -4096
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgControllerIndTsOffs
|
||||||
assert m._send_buffer==MsgControllerAck
|
assert m._send_buffer==MsgControllerAck
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
@@ -552,12 +749,17 @@ def test_msg_cntrl_invalid(ConfigTsunInv1, MsgControllerInvalid):
|
|||||||
assert m.msg_id==113
|
assert m.msg_id==113
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==1
|
assert m.data_len==1
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgControllerInvalid
|
||||||
|
m.ts_offset = -4096
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgControllerInvalid
|
assert m._forward_buffer==MsgControllerInvalid
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterAck):
|
def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterIndTsOffs, MsgInverterAck):
|
||||||
ConfigTsunInv1
|
ConfigTsunInv1
|
||||||
tracer.setLevel(logging.DEBUG)
|
tracer.setLevel(logging.DEBUG)
|
||||||
m = MemoryStream(MsgInverterInd, (0,))
|
m = MemoryStream(MsgInverterInd, (0,))
|
||||||
@@ -571,11 +773,62 @@ def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterAck):
|
|||||||
assert m.msg_id==4
|
assert m.msg_id==4
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==120
|
assert m.data_len==120
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgInverterInd
|
assert m._forward_buffer==MsgInverterInd
|
||||||
|
m.ts_offset = +256
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgInverterIndTsOffs
|
||||||
assert m._send_buffer==MsgInverterAck
|
assert m._send_buffer==MsgInverterAck
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck):
|
||||||
|
ConfigTsunInv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
m = MemoryStream(MsgInverterIndNew, (0,))
|
||||||
|
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 == 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==1165
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgInverterIndNew
|
||||||
|
assert m._send_buffer==MsgInverterAck
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck):
|
||||||
|
ConfigTsunInv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
m = MemoryStream(MsgInverterIndNew, (0,))
|
||||||
|
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 == 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==1165
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgInverterIndNew
|
||||||
|
assert m._send_buffer==MsgInverterAck
|
||||||
|
m.close()
|
||||||
|
|
||||||
def test_msg_inv_ack(ConfigTsunInv1, MsgInverterAck):
|
def test_msg_inv_ack(ConfigTsunInv1, MsgInverterAck):
|
||||||
ConfigTsunInv1
|
ConfigTsunInv1
|
||||||
tracer.setLevel(logging.ERROR)
|
tracer.setLevel(logging.ERROR)
|
||||||
@@ -609,6 +862,11 @@ def test_msg_inv_invalid(ConfigTsunInv1, MsgInverterInvalid):
|
|||||||
assert m.msg_id==4
|
assert m.msg_id==4
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==1
|
assert m.data_len==1
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgInverterInvalid
|
||||||
|
m.ts_offset = 256
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgInverterInvalid
|
assert m._forward_buffer==MsgInverterInvalid
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
@@ -628,6 +886,11 @@ def test_msg_ota_req(ConfigTsunInv1, MsgOtaReq):
|
|||||||
assert m.msg_id==19
|
assert m.msg_id==19
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==259
|
assert m.data_len==259
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgOtaReq
|
||||||
|
m.ts_offset = 4096
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgOtaReq
|
assert m._forward_buffer==MsgOtaReq
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
@@ -650,6 +913,11 @@ def test_msg_ota_ack(ConfigTsunInv1, MsgOtaAck):
|
|||||||
assert m.msg_id==19
|
assert m.msg_id==19
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==1
|
assert m.data_len==1
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
|
assert m._forward_buffer==MsgOtaAck
|
||||||
|
m.ts_offset = 256
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgOtaAck
|
assert m._forward_buffer==MsgOtaAck
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
@@ -670,7 +938,12 @@ def test_msg_ota_invalid(ConfigTsunInv1, MsgOtaInvalid):
|
|||||||
assert m.msg_id==19
|
assert m.msg_id==19
|
||||||
assert m.header_len==23
|
assert m.header_len==23
|
||||||
assert m.data_len==1
|
assert m.data_len==1
|
||||||
|
m.ts_offset = 0
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._forward_buffer==MsgOtaInvalid
|
assert m._forward_buffer==MsgOtaInvalid
|
||||||
|
m.ts_offset = 4096
|
||||||
|
assert m._forward_buffer==MsgOtaInvalid
|
||||||
|
m._update_header(m._forward_buffer)
|
||||||
assert m._send_buffer==b''
|
assert m._send_buffer==b''
|
||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
assert m.db.stat['proxy']['OTA_Start_Msg'] == 0
|
assert m.db.stat['proxy']['OTA_Start_Msg'] == 0
|
||||||
@@ -695,10 +968,16 @@ def test_msg_unknown(ConfigTsunInv1, MsgUnknown):
|
|||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
def test_ctrl_byte():
|
def test_ctrl_byte():
|
||||||
|
c = Control(0x70)
|
||||||
|
assert not c.is_ind()
|
||||||
|
assert not c.is_resp()
|
||||||
|
assert c.is_req()
|
||||||
c = Control(0x91)
|
c = Control(0x91)
|
||||||
|
assert not c.is_req()
|
||||||
assert c.is_ind()
|
assert c.is_ind()
|
||||||
assert not c.is_resp()
|
assert not c.is_resp()
|
||||||
c = Control(0x99)
|
c = Control(0x99)
|
||||||
|
assert not c.is_req()
|
||||||
assert not c.is_ind()
|
assert not c.is_ind()
|
||||||
assert c.is_resp()
|
assert c.is_resp()
|
||||||
|
|
||||||
@@ -724,19 +1003,378 @@ def test_msg_iterator():
|
|||||||
assert test2 == 1
|
assert test2 == 1
|
||||||
|
|
||||||
def test_proxy_counter():
|
def test_proxy_counter():
|
||||||
m = Talent(server_side=True)
|
# m = MemoryStream(b'')
|
||||||
|
# m.close()
|
||||||
|
Infos.stat['proxy']['Modbus_Command'] = 1
|
||||||
|
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.id_str = b"R170000000000001"
|
||||||
|
c = m.createClientStream(b'')
|
||||||
|
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
m.db.stat['proxy']['Unknown_Msg'] = 0
|
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
|
c.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
Infos.new_stat_data['proxy'] = False
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
|
||||||
m.inc_counter('Unknown_Msg')
|
m.inc_counter('Unknown_Msg')
|
||||||
|
m.close()
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
assert Infos.new_stat_data == {'proxy': True}
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
|
assert m.db.new_stat_data == {'proxy': True}
|
||||||
|
assert c.db.new_stat_data == {'proxy': True}
|
||||||
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
|
assert 1 == c.db.stat['proxy']['Unknown_Msg']
|
||||||
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
|
||||||
|
c.inc_counter('Unknown_Msg')
|
||||||
|
assert m.new_data == {}
|
||||||
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
|
assert m.db.new_stat_data == {'proxy': True}
|
||||||
|
assert c.db.new_stat_data == {'proxy': True}
|
||||||
|
assert 2 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
|
assert 2 == c.db.stat['proxy']['Unknown_Msg']
|
||||||
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
|
||||||
|
c.inc_counter('Modbus_Command')
|
||||||
|
assert m.new_data == {}
|
||||||
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
|
assert m.db.new_stat_data == {'proxy': True}
|
||||||
|
assert c.db.new_stat_data == {'proxy': True}
|
||||||
|
assert 2 == m.db.stat['proxy']['Modbus_Command']
|
||||||
|
assert 2 == c.db.stat['proxy']['Modbus_Command']
|
||||||
|
|
||||||
Infos.new_stat_data['proxy'] = False
|
Infos.new_stat_data['proxy'] = False
|
||||||
m.dec_counter('Unknown_Msg')
|
m.dec_counter('Unknown_Msg')
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
assert Infos.new_stat_data == {'proxy': True}
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
assert 0 == m.db.stat['proxy']['Unknown_Msg']
|
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.id_str = b"R170000000000001"
|
||||||
|
m.state = State.up
|
||||||
|
|
||||||
|
c = m.createClientStream(MsgModbusCmd)
|
||||||
|
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 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.id_str == b"R170000000000001"
|
||||||
|
assert c.unique_id == 'R170000000000001'
|
||||||
|
assert int(c.ctrl)==112
|
||||||
|
assert c.msg_id==119
|
||||||
|
assert c.header_len==23
|
||||||
|
assert c.data_len==13
|
||||||
|
assert c._forward_buffer==b''
|
||||||
|
assert c._send_buffer==b''
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.writer.sent_pdu == MsgModbusCmd
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.id_str = b"R170000000000001"
|
||||||
|
|
||||||
|
c = m.createClientStream(MsgModbusCmd)
|
||||||
|
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 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.id_str == b"R170000000000001"
|
||||||
|
assert c.unique_id == 'R170000000000001'
|
||||||
|
assert int(c.ctrl)==112
|
||||||
|
assert c.msg_id==119
|
||||||
|
assert c.header_len==23
|
||||||
|
assert c.data_len==13
|
||||||
|
assert c._forward_buffer==b''
|
||||||
|
assert c._send_buffer==b''
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.writer.sent_pdu == b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_req3(ConfigTsunInv1, MsgModbusCmdCrcErr):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'')
|
||||||
|
m.id_str = b"R170000000000001"
|
||||||
|
c = m.createClientStream(MsgModbusCmdCrcErr)
|
||||||
|
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 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.id_str == b"R170000000000001"
|
||||||
|
assert c.unique_id == 'R170000000000001'
|
||||||
|
assert int(c.ctrl)==112
|
||||||
|
assert c.msg_id==119
|
||||||
|
assert c.header_len==23
|
||||||
|
assert c.data_len==13
|
||||||
|
assert c._forward_buffer==b''
|
||||||
|
assert c._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.writer.sent_pdu ==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'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
'''Modbus response without a valid Modbus request must be dropped'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusRsp)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==145
|
||||||
|
assert m.msg_id==119
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.data_len==13
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_cloud_rsp(ConfigTsunInv1, MsgModbusRsp):
|
||||||
|
'''Modbus response from TSUN without a valid Modbus request must be dropped'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusRsp, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==145
|
||||||
|
assert m.msg_id==119
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.data_len==13
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Msg'] == 1
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20):
|
||||||
|
'''Modbus response with a valid Modbus request must be forwarded'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusResp20)
|
||||||
|
m.append_msg(MsgModbusResp20)
|
||||||
|
|
||||||
|
m.mb.rsp_handler = m.msg_forward
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
|
||||||
|
assert m.db.db == {}
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m._forward_buffer==MsgModbusResp20
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
|
||||||
|
assert m.new_data['inverter'] == True
|
||||||
|
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
m.mb.req_pend = 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.mb.err == 0
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m._forward_buffer==MsgModbusResp20
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
|
||||||
|
assert m.new_data['inverter'] == False
|
||||||
|
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusResp20):
|
||||||
|
'''Modbus response with a valid Modbus request must be forwarded'''
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusResp20)
|
||||||
|
m.append_msg(MsgModbusResp20)
|
||||||
|
|
||||||
|
m.mb.rsp_handler = m.msg_forward
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
|
||||||
|
assert m.db.db == {}
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m._forward_buffer==MsgModbusResp20
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
|
||||||
|
assert m.new_data['inverter'] == True
|
||||||
|
m.new_data['inverter'] = False
|
||||||
|
assert m.mb.req_pend == False
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.mb.err == 5
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m._forward_buffer==MsgModbusResp20
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
|
||||||
|
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
|
||||||
|
assert m.new_data['inverter'] == False
|
||||||
|
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInv):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(MsgModbusInv, (0,), False)
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl)==153
|
||||||
|
assert m.msg_id==119
|
||||||
|
assert m.header_len==23
|
||||||
|
assert m.data_len==13
|
||||||
|
assert m._forward_buffer==MsgModbusInv
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusResp20):
|
||||||
|
ConfigTsunInv1
|
||||||
|
# receive more bytes than expected (7 bytes from the next msg)
|
||||||
|
m = MemoryStream(MsgModbusResp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
|
||||||
|
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||||
|
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||||
|
m.mb.rsp_handler = m.msg_forward
|
||||||
|
m.mb.last_addr = 1
|
||||||
|
m.mb.last_fcode = 3
|
||||||
|
m.mb.last_len = 20
|
||||||
|
m.mb.last_reg = 0x3008
|
||||||
|
m.mb.req_pend = True
|
||||||
|
m.mb.err = 0
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.id_str == b"R170000000000001"
|
||||||
|
assert m.unique_id == 'R170000000000001'
|
||||||
|
assert int(m.ctrl) == 0x91
|
||||||
|
assert m.msg_id == 119
|
||||||
|
assert m.header_len == 23
|
||||||
|
assert m.data_len == 50
|
||||||
|
assert m._forward_buffer==MsgModbusResp20
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
assert m.mb.err == 0
|
||||||
|
assert m.modbus_elms == 20-1 # register 0x300d is unknown, so one value can't be mapped
|
||||||
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(b'', (0,), True)
|
||||||
|
m.id_str = b"R170000000000001"
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
assert m.writer.sent_pdu == b''
|
||||||
|
|
||||||
|
m.state = State.up
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
assert m.writer.sent_pdu == MsgModbusCmd
|
||||||
|
|
||||||
|
m.writer.sent_pdu = bytearray(0) # clear send buffer for next test
|
||||||
|
m.test_exception_async_write = True
|
||||||
|
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
|
||||||
|
assert 0 == m.send_msg_ofs
|
||||||
|
assert m._forward_buffer == b''
|
||||||
|
assert m._send_buffer == b''
|
||||||
|
assert m.writer.sent_pdu == b''
|
||||||
|
m.close()
|
||||||
|
'''
|
||||||
|
def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
|
||||||
|
ConfigTsunInv1
|
||||||
|
tracer.setLevel(logging.DEBUG)
|
||||||
|
start_val = MemoryStream._RefNo
|
||||||
|
|
||||||
|
m1 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
assert MemoryStream._RefNo == 1 + start_val
|
||||||
|
assert m1.RefNo == 1 + start_val
|
||||||
|
m2 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
assert MemoryStream._RefNo == 2 + start_val
|
||||||
|
assert m2.RefNo == 2 + start_val
|
||||||
|
m3 = MemoryStream(MsgInverterInd, (0,))
|
||||||
|
assert MemoryStream._RefNo == 3 + start_val
|
||||||
|
assert m3.RefNo == 3 + start_val
|
||||||
|
assert m1.state == m1.State.init
|
||||||
|
assert m2.state == m2.State.init
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m1.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.up
|
||||||
|
assert m2.state == m2.State.init
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m2.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.closed
|
||||||
|
assert m2.state == m2.State.up
|
||||||
|
assert m3.state == m3.State.init
|
||||||
|
m3.read() # read complete msg, and set unique_id
|
||||||
|
assert m1.state == m1.State.closed
|
||||||
|
assert m2.state == m2.State.closed
|
||||||
|
assert m3.state == m3.State.up
|
||||||
|
m1.close()
|
||||||
|
m2.close()
|
||||||
|
m3.close()
|
||||||
|
'''
|
||||||
2
app/tests/timestamp_old.svg
Normal file
2
app/tests/timestamp_old.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
26
app/tests/timestamp_old.yuml
Normal file
26
app/tests/timestamp_old.yuml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// {type:sequence}
|
||||||
|
// {generate:true}
|
||||||
|
|
||||||
|
[Inverter]ContactInd>[Proxy]
|
||||||
|
[Proxy]-[note: store Contact Info in proxy{bg:cornsilk}]
|
||||||
|
[Proxy]ContactRsp (Ok).>[Inverter]
|
||||||
|
|
||||||
|
[Inverter]getTimeReq>[Proxy]
|
||||||
|
[Proxy]ContactInd>[Cloud]
|
||||||
|
[Cloud]ContactRsp (Ok).>[Proxy]
|
||||||
|
[Proxy]getTimeReq>[Cloud]
|
||||||
|
[Cloud]TimeRsp (time).>[Proxy]
|
||||||
|
[Proxy]TimeRsp (time).>[Inverter]
|
||||||
|
[Inverter]-[note: set clock in inverter{bg:cornsilk}]
|
||||||
|
|
||||||
|
[Inverter]DataInd (ts:=time)>[Proxy]
|
||||||
|
[Proxy]DataRsp>[Inverter]
|
||||||
|
[Proxy]DataInd (ts)>>[Cloud]
|
||||||
|
[Proxy]DataInd>>[MQTT-Broker]
|
||||||
|
[Cloud]DataRsp>>[Proxy]
|
||||||
|
|
||||||
|
[Inverter]DataInd (ts:=time)>[Proxy]
|
||||||
|
[Proxy]DataRsp>[Inverter]
|
||||||
|
[Proxy]DataInd (ts)>>[Cloud]
|
||||||
|
[Proxy]DataInd>>[MQTT-Broker]
|
||||||
|
[Cloud]DataRsp>>[Proxy]
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
version: '3.0'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
####### H O M E - A S S I S T A N T #####
|
####### H O M E - A S S I S T A N T #####
|
||||||
home-assistant:
|
home-assistant:
|
||||||
@@ -34,7 +31,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8123:8123
|
- 8123:8123
|
||||||
volumes:
|
volumes:
|
||||||
- ${PROJECT_DIR}./homeassistant/config:/config
|
- ${PROJECT_DIR:-./}homeassistant/config:/config
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: curl --fail http://0.0.0.0:8123/auth/providers || exit 1
|
test: curl --fail http://0.0.0.0:8123/auth/providers || exit 1
|
||||||
@@ -56,18 +53,18 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- 1883
|
- 1883
|
||||||
volumes:
|
volumes:
|
||||||
- ${PROJECT_DIR}./mosquitto/config:/mosquitto/config
|
- ${PROJECT_DIR:-./}mosquitto/config:/mosquitto/config
|
||||||
- ${PROJECT_DIR}./mosquitto/data:/mosquitto/data
|
- ${PROJECT_DIR:-./}mosquitto/data:/mosquitto/data
|
||||||
networks:
|
networks:
|
||||||
outside:
|
- outside
|
||||||
ipv4_address: 172.28.1.5 # static IP required to receive mDNS traffic
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
####### T S U N - P R O X Y ######
|
####### T S U N - P R O X Y ######
|
||||||
tsun-proxy:
|
tsun-proxy:
|
||||||
container_name: tsun-proxy
|
container_name: tsun-proxy
|
||||||
image: ghcr.io/s-allius/tsun-gen3-proxy:latest
|
image: ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||||
|
# image: ghcr.io/s-allius/tsun-gen3-proxy:rc
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- mqtt
|
- mqtt
|
||||||
@@ -77,13 +74,18 @@ services:
|
|||||||
- GID=${GID:-1000}
|
- GID=${GID:-1000}
|
||||||
dns:
|
dns:
|
||||||
- ${DNS1:-8.8.8.8}
|
- ${DNS1:-8.8.8.8}
|
||||||
- $(DNS2:-4.4.4.4}
|
- ${DNS2:-4.4.4.4}
|
||||||
ports:
|
ports:
|
||||||
- 5005:5005
|
- 5005:5005
|
||||||
|
- 8127:8127
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
volumes:
|
volumes:
|
||||||
- ${PROJECT_DIR}./tsun-proxy/log:/home/tsun-proxy/log
|
- ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log
|
||||||
- ${PROJECT_DIR}./tsun-proxy/config:/home/tsun-proxy/config
|
- ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config
|
||||||
|
healthcheck:
|
||||||
|
test: wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
networks:
|
networks:
|
||||||
- outside
|
- outside
|
||||||
|
|
||||||
@@ -93,11 +95,4 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
outside:
|
outside:
|
||||||
name: home-assistant
|
name: home-assistant
|
||||||
external: true
|
|
||||||
ipam:
|
|
||||||
driver: default
|
|
||||||
config:
|
|
||||||
- subnet: 172.28.1.0/26
|
|
||||||
ip_range: 172.28.1.32/27
|
|
||||||
gateway: 172.28.1.62
|
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgI
|
|||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
# time.sleep(32.5)
|
# time.sleep(32.5)
|
||||||
# assert data == MsgTimeStampResp
|
# assert data == MsgTimeStampResp
|
||||||
try:
|
try:
|
||||||
s.sendall(MsgInvData)
|
s.sendall(MsgInvData)
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ def get_inv_no() -> bytes:
|
|||||||
def get_invalid_sn():
|
def get_invalid_sn():
|
||||||
return b'R170000000000002'
|
return b'R170000000000002'
|
||||||
|
|
||||||
|
def correct_checksum(buf):
|
||||||
|
checksum = sum(buf[1:]) & 0xff
|
||||||
|
return checksum.to_bytes(length=1)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def MsgContactInfo(): # Contact Info message
|
def MsgContactInfo(): # Contact Info message
|
||||||
@@ -61,10 +64,11 @@ def MsgDataInd():
|
|||||||
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\x12\x02\x12\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
msg += b'\x00\x01\x12\x02\x12\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
msg += b'\x40\x10\x08\xd8\x00\x09\x13\x84\x00\x35\x00\x00\x02\x58\x00\xd8'
|
msg += b'\x40\x10\x08\xd8\x00\x09\x13\x84\x00\x35\x00\x00\x02\x58\x00\xd8'
|
||||||
msg += b'\x01\x3f\x00\x17\x00\x4d\x01\x44\x00\x14\x00\x43\x01\x45\x00\x18'
|
msg += b'\x01\x3f\x00\x17\x00\x4d\x01\x44\x00\x14\x00\x43\x01\x45\x00\x18'
|
||||||
msg += b'\x00\x52\x00\x12\x00\x01\x00\x00\x00\x7c\x00\x00\x24\xed\x00\x2c'
|
msg += b'\x00\x52\x00\x12\x00\x01\x00\x00\x00\x7c\x00\x00\x24\xed\x00\x2c'
|
||||||
msg += b'\x00\x00\x0b\x10\x00\x26\x00\x00\x0a\x0f\x00\x30\x00\x00\x0b\x76'
|
msg += b'\x00\x00\x0b\x10\x00\x26\x00\x00\x0a\x0f\x00\x30\x00\x00\x0b\x76'
|
||||||
msg += b'\x00\x00\x00\x00\x06\x16\x00\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x00\x00\x00\x01\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'
|
||||||
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
@@ -73,7 +77,9 @@ def MsgDataInd():
|
|||||||
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
msg += b'\x04\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'\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\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x00\x24\x15'
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -83,6 +89,24 @@ def MsgDataResp(): # Contact Response message
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgInvalidInfo(): # Contact Info message wrong start byte
|
||||||
|
msg = b'\x47\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c'
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -147,4 +171,24 @@ def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
# time.sleep(2.5)
|
# time.sleep(2.5)
|
||||||
checkResponse(data, MsgDataResp)
|
checkResponse(data, MsgDataResp)
|
||||||
|
|
||||||
|
def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactResp):
|
||||||
|
s = ClientConnection
|
||||||
|
try:
|
||||||
|
s.sendall(MsgInvalidInfo)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
try:
|
||||||
|
s.sendall(MsgContactInfo)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
checkResponse(data, MsgContactResp)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user