Compare commits

...

19 Commits

Author SHA1 Message Date
Stefan Allius
e4ff17e600 S allius/issue117 (#118)
* add shutdown flag

* add more register definitions

* add start commando for client side connections

* add first support for port 8899

* fix shutdown

* add client_mode configuration

* read client_mode config to setup inverter connections

* add client_mode connections over port 8899

* add preview build
2024-07-08 19:08:58 +02:00
Stefan Allius
a42ba8a8c6 Dev 0.9 (#115)
* make timestamp handling stateless

* adapt tests for stateless timestamp handling

* initial version

* add more type annotations

* add more type annotations

* fix Generator annotation for ha_proxy_confs

* fix names of issue branches

* add more type annotations

* don't use depricated varn anymore

* don't mark all test as async

* fix imports

* fix solarman unit tests

- fake Mqtt class

* print image build time during proxy start

* update changelog

* fix pytest collect warning

* cleanup msg_get_time handler

* addapt unit test

* label debug images with debug

* dump droped packages

* fix warnings

* add systemtest with invalid start byte

* update changelog

* update changelog

* add exposed ports and healthcheck

* add wget for healthcheck

* add aiohttp

* use config validation for healthcheck

* add http server for healthcheck

* calculate msg prossesing time

* add healthy check methods

* fix typo

* log ConfigErr with DEBUG level

* Update async_stream.py

- check if processing time is < 5 sec

* add a close handler to release internal resources

* call modbus close hanlder on a close call

* add exception handling for forward handler

* update changelog

* isolate Modbus fix

* cleanup

* update changelog

* add heaithy handler

* log unrelease references

* add healtcheck

* complete exposed port list

* add wget for healtcheck

* add aiohttp

* use Enum class for State

* calc processing time for healthcheck

* add HTTP server for healthcheck

* cleanup

* Update CHANGELOG.md

* updat changelog

* add docstrings to state enum

* set new state State.received

* add healthy method

* log healthcheck infos with DEBUG level

* update changelog

* S allius/issue100 (#101)

* detect dead connections

- disconnect connection on Msg receive timeout
- improve connection trace (add connection id)

* update changelog

* fix merge conflict

* fix unittests

* S allius/issue108 (#109)

* add more data types

* adapt unittests

* improve test coverage

* fix linter warning

* update changelog

* S allius/issue102 (#110)

* hotfix: don't send two MODBUS commands together

* fix unit tests

* remove read loop

* optional sleep between msg read and sending rsp

* wait after read 0.5s before sending a response

* add pending state

* fix state definitions

* determine the connection timeout by the conn state

* avoid sending MODBUS cmds in the inverter's reporting phase

* update changelog

* S allius/issue111 (#112)

Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to unexpected packets.

* inital checkin

* remove crontab entry for regular MODBUS cmds

* add timer for regular MODBUS polling

* fix Stop method call for already stopped timer

* optimize MB_START_TIMEOUT value

* cleanup

* update changelog

* fix buildx warnings

* fix timer cleanup

* fix Config.class_init()

- return error string or None
- release Schema structure after building thr config

* add quit flag to docker push

* fix timout calculation

* rename python to debugpy

* add asyncio log

* cleanup shutdown
- stop webserver on shutdown
- enable asyncio debug mode for debug versions

* update changelog

* update changelog

* fix exception in MODBUS timeout callback

* update changelog
2024-07-01 23:41:56 +02:00
Stefan Allius
f3e69ff217 Dev 0.8 (#107)
* S allius/issue102 (#103)

* hotfix: don't send two MODBUS commands together

* Update README.md

Exchange logger fw version with the real inverter fw version in the compatibility table

* Update python-app.yml

run also on pushes to issue branches
fix name for issues branches

* S allius/issue104 (#105)

* Update README.md

Exchange logger fw version with the real inverter fw version in the compatibility table

* Update python-app.yml

run also on pushes to issue branches
fix name for issues branches

* fix forwarding of MODBUS responses

* fix unit tests

* update changelog

* update changelog
2024-06-21 18:41:54 +02:00
Stefan Allius
a3c054d2b1 Dev 0.8 (#106)
* S allius/issue102 (#103)

* hotfix: don't send two MODBUS commands together

* Update README.md

Exchange logger fw version with the real inverter fw version in the compatibility table

* Update python-app.yml

run also on pushes to issue branches
fix name for issues branches

* S allius/issue104 (#105)

* Update README.md

Exchange logger fw version with the real inverter fw version in the compatibility table

* Update python-app.yml

run also on pushes to issue branches
fix name for issues branches

* fix forwarding of MODBUS responses

* fix unit tests

* update changelog
2024-06-21 18:12:48 +02:00
Stefan Allius
c34b33ed5f Update python-app.yml
fix name for issues branches
2024-06-08 23:39:28 +02:00
Stefan Allius
0a18918326 Update python-app.yml
run also on pushes to issue branches
2024-06-08 23:23:56 +02:00
Stefan Allius
aa3bb4a1fa Merge pull request #86 from s-allius/dev-0.8.0
Dev 0.8.0
2024-06-07 19:51:55 +02:00
Stefan Allius
a62864218d update for version 0.8.0 2024-06-07 19:48:41 +02:00
Stefan Allius
0b2631c162 beautify some traces 2024-06-07 19:27:36 +02:00
Stefan Allius
c59bd16664 change log level for some traces 2024-06-05 22:01:48 +02:00
Stefan Allius
039a021cda cleanup trace output 2024-06-04 21:55:57 +02:00
Stefan Allius
49e2dfbd86 optimize docker-compose.yaml file 2024-06-04 20:27:15 +02:00
Stefan Allius
e6ecf5911b remove the external network expectation 2024-06-04 20:00:39 +02:00
Stefan Allius
6e1ed5d1e7 check the docker-compose.yaml file as last step 2024-06-03 20:59:21 +02:00
Stefan Allius
ad885e9644 add Y47 serial numbers 2024-06-03 20:40:35 +02:00
Stefan Allius
8f81ceda98 fix warnings and remove obsolete version 2024-06-03 20:28:14 +02:00
Stefan Allius
8204cae2b1 improve logging output 2024-06-03 19:52:37 +02:00
Stefan Allius
8baa68e615 fix typo (wrong bracket) 2024-06-02 14:08:06 +02:00
Stefan Allius
7b4ed406a1 Update README.md
Exchange logger fw version with the real inverter fw version in the compatibility table
2024-04-23 22:26:01 +02:00
39 changed files with 1454 additions and 496 deletions

View File

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

2
.vscode/launch.json vendored
View File

@@ -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",

View File

@@ -5,15 +5,54 @@ 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]
- GEN3PLUS: add client_mode connection on port 8899 [#117](https://github.com/s-allius/tsun-gen3-proxy/issues/117)
## [0.9.0] - 2024-07-01
- fix exception in MODBUS timeout callback
## [0.9.0-RC1] - 2024-06-29
- add asyncio log and debug mode
- stop the HTTP server on shutdown gracefully
- Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to
unexpected packets. [#111](https://github.com/s-allius/tsun-gen3-proxy/issues/111)
- GEN3: avoid sending MODBUS commands to the inverter during the inverter's reporting phase
- GEN3: determine the connection timeout based on the connection state
- GEN3: support more data encodings for DSP version V5.0.17 [#108](https://github.com/s-allius/tsun-gen3-proxy/issues/108)
- detect dead connections [#100](https://github.com/s-allius/tsun-gen3-proxy/issues/100)
- improve connection logging wirt a unique connection id
- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91)
- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93)
- add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94)
- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)
- GEN3PLUS: dump invalid packages with wrong start or stop byte
- label debug imagages als `debug`
- print imgae build time during proxy start
- add type annotations
- improve async unit test and fix pytest warnings
- run github tests even for pulls on issue branches
## [0.8.1] - 2024-06-21
- Fix MODBUS responses are dropped and not forwarded to the TSUN cloud [#104](https://github.com/s-allius/tsun-gen3-proxy/issues/104)
- GEN3: Fix connections losts due MODBUS requests [#102](https://github.com/s-allius/tsun-gen3-proxy/issues/102)
## [0.8.0] - 2024-06-07
- improve logging: add protocol or node_id to connection logs
- improve logging: log ignored AT+ or MODBUS commands
- improve tracelog: log level depends on message type and source
- fix typo in docker-compose.yaml and remove the external network definition
- trace heartbeat and regular modbus pakets witl log level DEBUG - trace heartbeat and regular modbus pakets witl log level DEBUG
- GEN3PLUS: don't forward ack paket from tsun to the inverter - GEN3PLUS: don't forward ack paket from tsun to the inverter
- add allow and block filter for AT+ commands - GEN3PLUS: add allow and block filter for AT+ commands
- catch all OSError errors in the read loop - catch all OSError errors in the read loop
- log Modbus traces with different log levels - log Modbus traces with different log levels
- add Modbus fifo and timeout handler - add Modbus fifo and timeout handler
- build version string in the same format as TSUN for GEN3 invterts - build version string in the same format as TSUN for GEN3 inverters
- add graceful shutdown - add graceful shutdown
- parse Modbus values and store them in the database - parse Modbus values and store them in the database
- add cron task to request the output power every minute - add cron task to request the output power every minute
@@ -24,7 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- change logging level to DEBUG for some logs - change logging level to DEBUG for some logs
- remove experimental value Register.VALUE_1 - remove experimental value Register.VALUE_1
- format Register.POWER_ON_TIME as integer - format Register.POWER_ON_TIME as integer
- ignore non realtime values for now - ignore catch-up values from the inverters for now
## [0.7.0] - 2024-04-20 ## [0.7.0] - 2024-04-20

View File

@@ -161,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
@@ -226,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>
@@ -241,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)

View File

@@ -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"

View File

@@ -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> in your .profile # export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .zprofile
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry # see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
@@ -21,32 +21,42 @@ IMAGE=tsun-gen3-proxy
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
IMAGE=docker.io/sallius/${IMAGE} IMAGE=docker.io/sallius/${IMAGE}
VERSION=${VERSION}-$1 VERSION=${VERSION}-$1
elif [[ $1 == rc ]] || [[ $1 == rel ]];then elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
IMAGE=ghcr.io/s-allius/${IMAGE} IMAGE=ghcr.io/s-allius/${IMAGE}
else else
echo argument missing! echo argument missing!
echo try: $0 '[debug|dev|rc|rel]' echo try: $0 '[debug|dev|preview|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 == debug ]];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}:dev 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 elif [[ $1 == dev ]];then
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app docker 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 == preview ]];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}:preview -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:preview
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
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}:rc -t ${IMAGE}:${VERSION} 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 '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:rc docker push -q ghcr.io/s-allius/tsun-gen3-proxy:rc
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION} 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

View File

@@ -44,6 +44,7 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
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 = '' # Optional, MQTT replacement for inverters serial number #node_id = '' # Optional, MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation place for home-assistant #suggested_area = '' # Optional, suggested installation place for home-assistant
#client_mode = {host = '192.168.0.1', port = 8899}
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr #pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr #pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr #pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr

View File

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

View File

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

View File

@@ -4,246 +4,254 @@
<!-- 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="673pt" height="1216pt" <svg width="691pt" height="1312pt"
viewBox="0.00 0.00 673.35 1216.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 1212)"> <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,-1212 669.348,-1212 669.348,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="108.5444,-1112 .1516,-1112 .1516,-1076 114.5444,-1076 114.5444,-1106 108.5444,-1112"/> <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="108.5444,-1112 108.5444,-1106 "/> <polyline fill="none" stroke="#000000" points="108.5444,-1208 108.5444,-1202 "/>
<polyline fill="none" stroke="#000000" points="114.5444,-1106 108.5444,-1106 "/> <polyline fill="none" stroke="#000000" points="114.5444,-1202 108.5444,-1202 "/>
<text text-anchor="middle" x="57.348" y="-1097" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text> <text text-anchor="middle" x="57.348" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
<text text-anchor="middle" x="57.348" y="-1085" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text> <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="639.0297,-816 569.6663,-816 569.6663,-780 639.0297,-780 639.0297,-816"/> <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="604.348" y="-795" 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="543.348,-524 543.348,-556 665.348,-556 665.348,-524 543.348,-524"/> <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="594.625" y="-537" 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="543.348,-468 543.348,-524 665.348,-524 665.348,-468 543.348,-468"/> <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="561.8355" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;ha_restarts</text> <text text-anchor="start" x="579.8355" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;ha_restarts</text>
<text text-anchor="start" x="569.6145" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__client</text> <text text-anchor="start" x="587.6145" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__client</text>
<text text-anchor="start" x="553.2215" y="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__cb_MqttIsUp</text> <text text-anchor="start" x="571.2215" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__cb_MqttIsUp</text>
<polygon fill="none" stroke="#000000" points="543.348,-424 543.348,-468 665.348,-468 665.348,-424 543.348,-424"/> <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="566.284" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish()</text> <text text-anchor="start" x="584.284" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish()</text>
<text text-anchor="start" x="570.4525" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;close()</text> <text text-anchor="start" x="588.4525" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;close()</text>
</g> </g>
<!-- A1&#45;&gt;A2 --> <!-- A1&#45;&gt;A2 -->
<g id="edge1" class="edge"> <g id="edge1" class="edge">
<title>A1&#45;&gt;A2</title> <title>A1&#45;&gt;A2</title>
<path fill="none" stroke="#000000" d="M604.348,-769.6429C604.348,-721.5141 604.348,-622.6159 604.348,-556.2865"/> <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="600.8481,-769.6555 604.348,-779.6556 607.8481,-769.6556 600.8481,-769.6555"/> <polygon fill="none" stroke="#000000" points="618.8481,-859.7608 622.348,-869.7608 625.8481,-859.7608 618.8481,-859.7608"/>
</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="550.348,-282 550.348,-314 658.348,-314 658.348,-282 550.348,-282"/> <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="587.4015" y="-295" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</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="550.348,-190 550.348,-282 658.348,-282 658.348,-190 550.348,-190"/> <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="580.452" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</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>
<text text-anchor="start" x="573.7885" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text> <text text-anchor="start" x="591.7885" y="-293" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
<text text-anchor="start" x="564.6235" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text> <text text-anchor="start" x="582.6235" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
<text text-anchor="start" x="564.0595" y="-227" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text> <text text-anchor="start" x="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="560.1705" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text> <text text-anchor="start" x="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="576.0135" y="-203" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</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="550.348,-170 550.348,-190 658.348,-190 658.348,-170 550.348,-170"/> <polygon fill="none" stroke="#000000" points="568.348,-212 568.348,-232 676.348,-232 676.348,-212 568.348,-212"/>
</g> </g>
<!-- A2&#45;&gt;A11 --> <!-- A2&#45;&gt;A11 -->
<g id="edge13" class="edge"> <g id="edge13" class="edge">
<title>A2&#45;&gt;A11</title> <title>A2&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M604.348,-423.8663C604.348,-390.0029 604.348,-348.7174 604.348,-314.0468"/> <path fill="none" stroke="#000000" d="M622.348,-507.8316C622.348,-462.6124 622.348,-402.6972 622.348,-356.2361"/>
</g> </g>
<!-- A3 --> <!-- A3 -->
<g id="node4" class="node"> <g id="node4" class="node">
<title>A3</title> <title>A3</title>
<polygon fill="none" stroke="#000000" points="274.348,-276 274.348,-308 346.348,-308 346.348,-276 274.348,-276"/> <polygon fill="none" stroke="#000000" points="257.348,-366 257.348,-398 364.348,-398 364.348,-366 257.348,-366"/>
<text text-anchor="start" x="292.5655" y="-289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text> <text text-anchor="start" x="293.0655" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
<polygon fill="none" stroke="#000000" points="274.348,-232 274.348,-276 346.348,-276 346.348,-232 274.348,-232"/> <polygon fill="none" stroke="#000000" points="257.348,-226 257.348,-366 364.348,-366 364.348,-226 257.348,-226"/>
<text text-anchor="start" x="304.2395" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text> <text text-anchor="start" x="302.5095" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
<text text-anchor="start" x="290.9015" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text> <text text-anchor="start" x="283.338" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
<polygon fill="none" stroke="#000000" points="274.348,-176 274.348,-232 346.348,-232 346.348,-176 274.348,-176"/> <text text-anchor="start" x="284.453" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
<text text-anchor="start" x="284.238" y="-213" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text> <text text-anchor="start" x="266.9565" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout:max_retires</text>
<text text-anchor="start" x="287.572" y="-201" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text> <text text-anchor="start" x="292.79" y="-287" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
<text text-anchor="start" x="285.072" y="-189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</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>
<!-- A4 --> <!-- A4 -->
<g id="node5" class="node"> <g id="node5" class="node">
<title>A4</title> <title>A4</title>
<polygon fill="none" stroke="#000000" points="263.348,-1104 263.348,-1136 334.348,-1136 334.348,-1104 263.348,-1104"/> <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="273.293" y="-1117" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</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="263.348,-1084 263.348,-1104 334.348,-1104 334.348,-1084 263.348,-1084"/> <polygon fill="none" stroke="#000000" points="263.348,-1180 263.348,-1200 334.348,-1200 334.348,-1180 263.348,-1180"/>
<polygon fill="none" stroke="#000000" points="263.348,-1052 263.348,-1084 334.348,-1084 334.348,-1052 263.348,-1052"/> <polygon fill="none" stroke="#000000" points="263.348,-1148 263.348,-1180 334.348,-1180 334.348,-1148 263.348,-1148"/>
<text text-anchor="start" x="280.787" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text> <text text-anchor="start" x="280.787" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
</g> </g>
<!-- A5 --> <!-- A5 -->
<g id="node6" class="node"> <g id="node6" class="node">
<title>A5</title> <title>A5</title>
<polygon fill="none" stroke="#000000" points="231.348,-898 231.348,-930 365.348,-930 365.348,-898 231.348,-898"/> <polygon fill="none" stroke="#000000" points="231.348,-994 231.348,-1026 365.348,-1026 365.348,-994 231.348,-994"/>
<text text-anchor="start" x="278.0655" y="-911" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text> <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,-734 231.348,-898 365.348,-898 365.348,-734 231.348,-734"/> <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="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text> <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="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid: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="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</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="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_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="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</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="-819" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_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="-807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</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="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</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="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_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="-771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_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="-759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</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="-747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</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>
<polygon fill="none" stroke="#000000" points="231.348,-666 231.348,-734 365.348,-734 365.348,-666 231.348,-666"/> <text text-anchor="start" x="287.51" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state</text>
<text text-anchor="start" x="248.0575" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void&lt;abstract&gt;</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="272.7925" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text> <text text-anchor="start" x="248.0575" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void&lt;abstract&gt;</text>
<text text-anchor="start" x="258.6205" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text> <text text-anchor="start" x="272.7925" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
<text text-anchor="start" x="256.9505" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text> <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> </g>
<!-- A4&#45;&gt;A5 --> <!-- A4&#45;&gt;A5 -->
<g id="edge2" class="edge"> <g id="edge2" class="edge">
<title>A4&#45;&gt;A5</title> <title>A4&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M298.348,-1041.7414C298.348,-1010.6043 298.348,-969.5621 298.348,-930.0536"/> <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,-1041.9047 298.348,-1051.9048 301.8481,-1041.9048 294.8481,-1041.9047"/> <polygon fill="none" stroke="#000000" points="294.8481,-1137.6902 298.348,-1147.6902 301.8481,-1137.6902 294.8481,-1137.6902"/>
</g> </g>
<!-- A6 --> <!-- A6 -->
<g id="node7" class="node"> <g id="node7" class="node">
<title>A6</title> <title>A6</title>
<polygon fill="none" stroke="#000000" points="370.348,-584 370.348,-616 484.348,-616 484.348,-584 370.348,-584"/> <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="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text> <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,-480 370.348,-584 484.348,-584 484.348,-480 370.348,-480"/> <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="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text> <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="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</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="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</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="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</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="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</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="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</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="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</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,-364 370.348,-480 484.348,-480 484.348,-364 370.348,-364"/> <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="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text> <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="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</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="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</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="-425" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</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="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_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="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</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="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text> <text text-anchor="start" x="412.3505" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g> </g>
<!-- A5&#45;&gt;A6 --> <!-- A5&#45;&gt;A6 -->
<g id="edge3" class="edge"> <g id="edge3" class="edge">
<title>A5&#45;&gt;A6</title> <title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M357.819,-656.0073C363.3477,-642.8069 368.9261,-629.488 374.3902,-616.442"/> <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="354.4392,-655.017 353.8043,-665.5928 360.8958,-657.7213 354.4392,-655.017"/> <polygon fill="none" stroke="#000000" points="355.6797,-739.2382 355.117,-749.8181 362.1546,-741.8983 355.6797,-739.2382"/>
</g> </g>
<!-- A7 --> <!-- A7 -->
<g id="node8" class="node"> <g id="node8" class="node">
<title>A7</title> <title>A7</title>
<polygon fill="none" stroke="#000000" points="127.348,-548 127.348,-580 218.348,-580 218.348,-548 127.348,-548"/> <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="-561" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text> <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,-456 127.348,-548 218.348,-548 218.348,-456 127.348,-456"/> <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="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text> <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="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</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="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</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="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</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="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</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="-469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</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,-400 127.348,-456 218.348,-456 218.348,-400 127.348,-400"/> <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="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text> <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="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text> <text text-anchor="start" x="157.8505" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g> </g>
<!-- A5&#45;&gt;A7 --> <!-- A5&#45;&gt;A7 -->
<g id="edge4" class="edge"> <g id="edge4" class="edge">
<title>A5&#45;&gt;A7</title> <title>A5&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M240.2947,-656.0919C229.716,-630.2328 218.9501,-603.9162 209.2,-580.0827"/> <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="237.1556,-657.6626 244.1814,-665.5928 243.6344,-655.0121 237.1556,-657.6626"/> <polygon fill="none" stroke="#000000" points="235.9268,-741.8409 242.8992,-749.8181 242.4233,-739.2339 235.9268,-741.8409"/>
</g> </g>
<!-- A6&#45;&gt;A3 --> <!-- A6&#45;&gt;A3 -->
<g id="edge6" class="edge"> <g id="edge6" class="edge">
<title>A6&#45;&gt;A3</title> <title>A6&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M370.219,-368.906C361.9756,-351.4328 353.6959,-333.8827 346.0351,-317.6444"/> <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="341.601,-308.2456 349.9376,-315.3696 343.7344,-312.7676 345.8678,-317.2897 345.8678,-317.2897 345.8678,-317.2897 343.7344,-312.7676 341.7979,-319.2097 341.601,-308.2456 341.601,-308.2456"/> <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="356.9793" y="-318.0326" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text> <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="354.8406" y="-353.119" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</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> </g>
<!-- A8 --> <!-- A8 -->
<g id="node9" class="node"> <g id="node9" class="node">
<title>A8</title> <title>A8</title>
<polygon fill="none" stroke="#000000" points="364.348,-258 364.348,-290 514.348,-290 514.348,-258 364.348,-258"/> <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="407.3935" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text> <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="364.348,-226 364.348,-258 514.348,-258 514.348,-226 364.348,-226"/> <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="374.335" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text> <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="364.348,-194 364.348,-226 514.348,-226 514.348,-194 364.348,-194"/> <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="424.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text> <text text-anchor="start" x="442.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g> </g>
<!-- A6&#45;&gt;A8 --> <!-- A6&#45;&gt;A8 -->
<g id="edge5" class="edge"> <g id="edge5" class="edge">
<title>A6&#45;&gt;A8</title> <title>A6&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M433.943,-353.7029C435.0429,-330.9727 436.1158,-308.7991 437.012,-290.2778"/> <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="430.441,-353.6629 433.4535,-363.8204 437.4328,-354.0013 430.441,-353.6629"/> <polygon fill="none" stroke="#000000" points="437.9668,-437.3383 440.4192,-447.6454 444.9297,-438.0587 437.9668,-437.3383"/>
</g> </g>
<!-- A7&#45;&gt;A3 --> <!-- A7&#45;&gt;A3 -->
<g id="edge8" class="edge"> <g id="edge8" class="edge">
<title>A7&#45;&gt;A3</title> <title>A7&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M208.5342,-399.8871C214.3752,-387.5945 220.7034,-375.3217 227.348,-364 241.4757,-339.9278 249.4905,-336.9696 265.348,-314 266.3556,-312.5405 267.3678,-311.0592 268.3821,-309.5614"/> <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="274.1947,-300.8381 272.3944,-311.6552 271.4221,-304.999 268.6496,-309.1599 268.6496,-309.1599 268.6496,-309.1599 271.4221,-304.999 264.9048,-306.6646 274.1947,-300.8381 274.1947,-300.8381"/> <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="271.1774" y="-317.6092" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text> <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="208.7462" y="-376.8883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</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="82.348,-258 82.348,-290 238.348,-290 238.348,-258 82.348,-258"/> <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="125.059" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</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="82.348,-226 82.348,-258 238.348,-258 238.348,-226 82.348,-226"/> <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="92.0005" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text> <text text-anchor="start" x="74.0005" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
<polygon fill="none" stroke="#000000" points="82.348,-194 82.348,-226 238.348,-226 238.348,-194 82.348,-194"/> <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="145.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text> <text text-anchor="start" x="127.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g> </g>
<!-- A7&#45;&gt;A9 --> <!-- A7&#45;&gt;A9 -->
<g id="edge7" class="edge"> <g id="edge7" class="edge">
<title>A7&#45;&gt;A9</title> <title>A7&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M167.4931,-389.6656C165.8292,-355.2775 164.0432,-318.3674 162.6731,-290.0512"/> <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="164.0025,-389.9456 167.9818,-399.7648 170.9943,-389.6073 164.0025,-389.9456"/> <polygon fill="none" stroke="#000000" points="158.5098,-474.2451 163.0203,-483.8319 165.4726,-473.5248 158.5098,-474.2451"/>
</g> </g>
<!-- A8&#45;&gt;A8 --> <!-- A8&#45;&gt;A8 -->
<g id="edge15" class="edge"> <g id="edge15" class="edge">
<title>A8&#45;&gt;A8</title> <title>A8&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M514.5164,-272.6238C525.1874,-267.6708 532.348,-257.4629 532.348,-242 532.348,-231.1277 528.8079,-222.8533 522.9966,-217.1769"/> <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="#000000" stroke="#000000" points="514.5164,-211.3762 525.3108,-213.3079 518.6433,-214.1991 522.7702,-217.0221 522.7702,-217.0221 522.7702,-217.0221 518.6433,-214.1991 520.2296,-220.7363 514.5164,-211.3762 514.5164,-211.3762"/> <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="534.2494" y="-211.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text> <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="526.5555" y="-253.6532" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</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>
<!-- A12 --> <!-- A12 -->
<g id="node13" class="node"> <g id="node13" class="node">
<title>A12</title> <title>A12</title>
<polygon fill="none" stroke="#000000" points="460.348,-88 460.348,-120 582.348,-120 582.348,-88 460.348,-88"/> <polygon fill="none" stroke="#000000" points="478.348,-88 478.348,-120 600.348,-120 600.348,-88 478.348,-88"/>
<text text-anchor="start" x="497.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text> <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="460.348,-56 460.348,-88 582.348,-88 582.348,-56 460.348,-56"/> <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="490.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text> <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="460.348,0 460.348,-56 582.348,-56 582.348,0 460.348,0"/> <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="469.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text> <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="506.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</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>
<!-- A8&#45;&gt;A12 --> <!-- A8&#45;&gt;A12 -->
<g id="edge14" class="edge"> <g id="edge14" class="edge">
<title>A8&#45;&gt;A12</title> <title>A8&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M465.2524,-184.5048C474.4734,-164.0387 484.8784,-140.9447 494.2007,-120.2539"/> <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="462.024,-183.1501 461.1072,-193.7052 468.4062,-186.0256 462.024,-183.1501"/> <polygon fill="none" stroke="#000000" points="475.09,-225.3526 474.9391,-235.9464 481.6634,-227.759 475.09,-225.3526"/>
</g> </g>
<!-- A9&#45;&gt;A9 --> <!-- A9&#45;&gt;A9 -->
<g id="edge17" class="edge"> <g id="edge17" class="edge">
<title>A9&#45;&gt;A9</title> <title>A9&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M238.6951,-272.2739C249.2923,-267.1987 256.348,-257.1074 256.348,-242 256.348,-231.3776 252.8598,-223.2351 247.1049,-217.5725"/> <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="238.6951,-211.7261 249.4746,-213.7393 242.8005,-214.5802 246.9059,-217.4342 246.9059,-217.4342 246.9059,-217.4342 242.8005,-214.5802 244.3373,-221.1291 238.6951,-211.7261 238.6951,-211.7261"/> <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="258.4028" y="-212.1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text> <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="250.5654" y="-253.1774" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</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">
@@ -259,118 +267,118 @@
<!-- A9&#45;&gt;A13 --> <!-- A9&#45;&gt;A13 -->
<g id="edge16" class="edge"> <g id="edge16" class="edge">
<title>A9&#45;&gt;A13</title> <title>A9&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M207.2144,-185.8836C224.5887,-165.0802 244.3566,-141.4107 262.0261,-120.2539"/> <path fill="none" stroke="#000000" d="M184.9859,-227.8183C209.8288,-195.0842 241.1576,-153.8039 266.5264,-120.3767"/>
<polygon fill="none" stroke="#000000" points="204.406,-183.7863 200.6821,-193.7052 209.7787,-188.2734 204.406,-183.7863"/> <polygon fill="none" stroke="#000000" points="182.0747,-225.8647 178.8173,-235.9464 187.6507,-230.0965 182.0747,-225.8647"/>
</g> </g>
<!-- A10 --> <!-- A10 -->
<g id="node11" class="node"> <g id="node11" class="node">
<title>A10</title> <title>A10</title>
<polygon fill="none" stroke="#000000" points="236.348,-578 236.348,-610 352.348,-610 352.348,-578 236.348,-578"/> <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="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text> <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,-498 236.348,-578 352.348,-578 352.348,-498 236.348,-498"/> <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="-559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text> <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="-547" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</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="-535" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</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="-523" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_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="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_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,-370 236.348,-498 352.348,-498 352.348,-370 236.348,-370"/> <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="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text> <text text-anchor="start" x="246.0055" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="248.226" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text> <text text-anchor="start" x="248.226" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="266.002" y="-455" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text> <text text-anchor="start" x="266.002" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="282.13" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text> <text text-anchor="start" x="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="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</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="-407" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</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="-395" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</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="-383" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</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> </g>
<!-- A10&#45;&gt;A8 --> <!-- A10&#45;&gt;A8 -->
<g id="edge9" class="edge"> <g id="edge9" class="edge">
<title>A10&#45;&gt;A8</title> <title>A10&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M357.4808,-370.672C358.7731,-368.4252 360.063,-366.1993 361.348,-364 375.806,-339.2552 392.855,-312.3558 407.3203,-290.1276"/> <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.4004,-369.0085 352.4898,-379.4297 360.4821,-372.4746 354.4004,-369.0085"/> <polygon fill="none" stroke="#000000" points="354.1241,-453.7956 352.3659,-464.2436 360.2557,-457.1724 354.1241,-453.7956"/>
</g> </g>
<!-- A10&#45;&gt;A9 --> <!-- A10&#45;&gt;A9 -->
<g id="edge10" class="edge"> <g id="edge10" class="edge">
<title>A10&#45;&gt;A9</title> <title>A10&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M231.527,-371.7451C230.1228,-369.1386 228.7284,-366.5542 227.348,-364 214.1359,-339.5527 199.2736,-312.4504 186.9099,-290.012"/> <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.5137,-373.5316 236.3333,-380.6803 234.6785,-370.2155 228.5137,-373.5316"/> <polygon fill="none" stroke="#000000" points="228.4903,-455.8898 236.2327,-463.1221 234.6903,-452.6401 228.4903,-455.8898"/>
</g> </g>
<!-- A11&#45;&gt;A12 --> <!-- A11&#45;&gt;A12 -->
<g id="edge11" class="edge"> <g id="edge11" class="edge">
<title>A11&#45;&gt;A12</title> <title>A11&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M567.1644,-160.4648C560.9703,-146.8826 554.6465,-133.016 548.7531,-120.0931"/> <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="564.0911,-162.1611 571.425,-169.8074 570.4601,-159.2566 564.0911,-162.1611"/> <polygon fill="none" stroke="#000000" points="588.8729,-203.7311 595.6294,-211.892 595.4368,-201.2989 588.8729,-203.7311"/>
</g> </g>
<!-- A11&#45;&gt;A13 --> <!-- A11&#45;&gt;A13 -->
<g id="edge12" class="edge"> <g id="edge12" class="edge">
<title>A11&#45;&gt;A13</title> <title>A11&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M542.4867,-170.8745C542.1078,-170.5805 541.7282,-170.2889 541.348,-170 513.3438,-148.7162 431.3406,-111.2534 373.497,-86.0342"/> <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="540.4079,-173.6974 550.3407,-177.3838 544.8747,-168.3078 540.4079,-173.6974"/> <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="133.348,-1176 133.348,-1208 236.348,-1208 236.348,-1176 133.348,-1176"/> <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="174.01" y="-1189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</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="133.348,-1120 133.348,-1176 236.348,-1176 236.348,-1120 133.348,-1120"/> <polygon fill="none" stroke="#000000" points="133.348,-1216 133.348,-1272 236.348,-1272 236.348,-1216 133.348,-1216"/>
<text text-anchor="start" x="176.7895" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text> <text text-anchor="start" x="176.7895" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="152.334" y="-1145" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text> <text text-anchor="start" x="152.334" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="165.9515" y="-1133" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text> <text text-anchor="start" x="165.9515" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="133.348,-980 133.348,-1120 236.348,-1120 236.348,-980 133.348,-980"/> <polygon fill="none" stroke="#000000" points="133.348,-1076 133.348,-1216 236.348,-1216 236.348,-1076 133.348,-1076"/>
<text text-anchor="start" x="160.6835" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text> <text text-anchor="start" x="160.6835" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="158.7325" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text> <text text-anchor="start" x="158.7325" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="155.6785" y="-1077" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text> <text text-anchor="start" x="155.6785" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="154.0085" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text> <text text-anchor="start" x="154.0085" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="152.058" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text> <text text-anchor="start" x="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="-1041" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_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="-1029" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</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="-1017" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</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="-1005" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_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="-993" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</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="386.348,-814 386.348,-846 453.348,-846 453.348,-814 386.348,-814"/> <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="402.341" y="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</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="386.348,-794 386.348,-814 453.348,-814 453.348,-794 386.348,-794"/> <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="386.348,-750 386.348,-794 453.348,-794 453.348,-750 386.348,-750"/> <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="396.232" y="-775" 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="404.016" y="-763" 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>
<!-- A14&#45;&gt;A15 --> <!-- A14&#45;&gt;A15 -->
<g id="edge18" class="edge"> <g id="edge18" class="edge">
<title>A14&#45;&gt;A15</title> <title>A14&#45;&gt;A15</title>
<path fill="none" stroke="#000000" d="M242.8857,-990.9876C246.5464,-987.0913 250.3682,-983.4032 254.348,-980 298.2601,-942.4501 334.8682,-972.1855 374.348,-930 395.7725,-907.1072 407.0366,-873.5975 412.9375,-846.0704"/> <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="240.0515,-988.9088 236.0452,-998.717 245.2936,-993.548 240.0515,-988.9088"/> <polygon fill="none" stroke="#000000" points="240.0515,-1084.9088 236.0452,-1094.717 245.2936,-1089.548 240.0515,-1084.9088"/>
</g> </g>
<!-- A16 --> <!-- A16 -->
<g id="node17" class="node"> <g id="node17" class="node">
<title>A16</title> <title>A16</title>
<polygon fill="none" stroke="#000000" points="142.348,-814 142.348,-846 209.348,-846 209.348,-814 142.348,-814"/> <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="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text> <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,-794 142.348,-814 209.348,-814 209.348,-794 142.348,-794"/> <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,-750 142.348,-794 209.348,-794 209.348,-750 142.348,-750"/> <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="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text> <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="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text> <text text-anchor="start" x="160.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
</g> </g>
<!-- A14&#45;&gt;A16 --> <!-- A14&#45;&gt;A16 -->
<g id="edge19" class="edge"> <g id="edge19" class="edge">
<title>A14&#45;&gt;A16</title> <title>A14&#45;&gt;A16</title>
<path fill="none" stroke="#000000" d="M180.5733,-969.853C179.2476,-926.2551 177.8358,-879.821 176.8143,-846.2247"/> <path fill="none" stroke="#000000" d="M180.6399,-1065.5724C179.2846,-1020.0932 177.8303,-971.2935 176.7899,-936.3828"/>
<polygon fill="none" stroke="#000000" points="177.0788,-970.0931 180.8812,-979.9821 184.0756,-969.8803 177.0788,-970.0931"/> <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&#45;&gt;A6 --> <!-- A15&#45;&gt;A6 -->
<g id="edge21" class="edge"> <g id="edge21" class="edge">
<title>A15&#45;&gt;A6</title> <title>A15&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M420.598,-749.875C421.4623,-716.5989 422.6586,-670.54 423.8044,-626.4296"/> <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="424.071,-616.1641 428.3097,-626.2776 423.9411,-621.1624 423.8113,-626.1607 423.8113,-626.1607 423.8113,-626.1607 423.9411,-621.1624 419.3128,-626.0438 424.071,-616.1641 424.071,-616.1641"/> <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> </g>
<!-- A16&#45;&gt;A7 --> <!-- A16&#45;&gt;A7 -->
<g id="edge20" class="edge"> <g id="edge20" class="edge">
<title>A16&#45;&gt;A7</title> <title>A16&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M174.8793,-749.875C174.4651,-707.3571 173.8477,-643.9701 173.3263,-590.4435"/> <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.2268,-580.2253 177.8241,-590.181 173.2756,-585.2251 173.3243,-590.2249 173.3243,-590.2249 173.3243,-590.2249 173.2756,-585.2251 168.8245,-590.2687 173.2268,-580.2253 173.2268,-580.2253"/> <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: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -4,8 +4,8 @@
[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|err;retry_cnt|build_msg();recv_req();recv_resp()] [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;db:InfosG3;mb:Modbus;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;db:InfosG3P;mb:Modbus;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()]

View File

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

View File

@@ -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 '
f'connection: [{self.remoteStream.node_id}:'
f'{self.remoteStream.conn_no}]')
await self.remoteStream.disc() 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,28 +87,45 @@ 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 asyncio.TimeoutError:
logger.warning(f'[{self.node_id}:{self.conn_no}] Dead '
f'connection timeout ({dead_conn_to}s) '
f'for {self.l_addr}')
await self.disc()
self.close()
return self
except OSError as error: except OSError as error:
logger.error(f'{error} for l{self.l_addr} | ' logger.error(f'[{self.node_id}:{self.conn_no}] '
f'{error} for l{self.l_addr} | '
f'r{self.r_addr}') f'r{self.r_addr}')
await self.disc() await self.disc()
self.close() self.close()
return self return self
except RuntimeError as error: except RuntimeError as error:
logger.warning(f"{error} for {self.l_addr}") logger.info(f'[{self.node_id}:{self.conn_no}] '
f'{error} for {self.l_addr}')
await self.disc() await self.disc()
self.close() self.close()
return self return self
@@ -84,31 +136,8 @@ class AsyncStream():
f"Exception for {self.addr}:\n" f"Exception for {self.addr}:\n"
f"{traceback.format_exc()}") f"{traceback.format_exc()}")
async def disc(self) -> None: async def async_write(self, headline: str = 'Transmit to ') -> None:
if self.writer.is_closing(): """Async write handler to transmit the send_buffer"""
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):
if self.writer.is_closing():
return
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, headline='Transmit to ') -> None:
if self._send_buffer: if self._send_buffer:
hex_dump_memory(logging.INFO, f'{headline}{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))
@@ -116,8 +145,57 @@ class AsyncStream():
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:
@@ -134,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}")

View File

@@ -53,7 +53,11 @@ class Config():
Use(lambda s: s + '/' Use(lambda s: s + '/'
if len(s) > 0 and if len(s) > 0 and
s[-1] != '/' else s)), s[-1] != '/' else s)),
Optional('client_mode'): {
'host': Use(str),
Optional('port', default=8899):
And(Use(int), lambda n: 1024 <= n <= 65535)
},
Optional('suggested_area', default=""): Use(str), Optional('suggested_area', default=""): Use(str),
Optional('pv1'): { Optional('pv1'): {
Optional('type'): Use(str), Optional('type'): Use(str),
@@ -84,7 +88,7 @@ class Config():
) )
@classmethod @classmethod
def class_init(cls): # pragma: no cover def class_init(cls) -> None | str: # pragma: no cover
try: try:
# make the default config transparaent by copying it # make the default config transparaent by copying it
# in the config.example file # in the config.example file
@@ -94,7 +98,9 @@ class Config():
"config/config.example.toml") "config/config.example.toml")
except Exception: except Exception:
pass pass
cls.read() err_str = cls.read()
del cls.conf_schema
return err_str
@classmethod @classmethod
def _read_config_file(cls) -> dict: # pragma: no cover def _read_config_file(cls) -> dict: # pragma: no cover

View File

@@ -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
''' '''

View File

@@ -132,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
@@ -144,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
@@ -164,7 +178,7 @@ class InfosG3(Infos):
name = str(f'info-id.0x{addr:x}') name = str(f'info-id.0x{addr:x}')
if update: if update:
self.tracer.log(level, f'[\'{node_id}\']GEN3: {name} :' self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
f' {result}{unit}') f' {result}{unit}')
i += 1 i += 1

View File

@@ -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__")

View File

@@ -4,13 +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.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 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
@@ -35,12 +37,16 @@ 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, self.send_modbus_cb, mb_timeout=11) 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,
@@ -63,18 +69,21 @@ class Talent(Message):
0x04: logging.INFO, 0x04: logging.INFO,
} }
self.modbus_elms = 0 # for unit tests 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.log_lvl.clear()
self.state = self.STATE_CLOSED self.state = State.closed
self.mb_timer.close()
super().close() super().close()
def __set_serial_no(self, serial_no: str): def __set_serial_no(self, serial_no: str):
@@ -103,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:
@@ -111,6 +120,9 @@ 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):
if self.state == State.init:
self.state = State.received # received 1st package
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING) log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
if callable(log_lvl): if callable(log_lvl):
log_lvl = log_lvl() log_lvl = log_lvl()
@@ -121,7 +133,7 @@ class Talent(Message):
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')
@@ -138,7 +150,9 @@ class Talent(Message):
return return
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str): def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
if self.state != self.STATE_UP: if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
return return
self.__build_header(0x70, 0x77) self.__build_header(0x70, 0x77)
@@ -152,11 +166,25 @@ class Talent(Message):
self.writer.write(self._send_buffer) self.writer.write(self._send_buffer)
self._send_buffer = bytearray(0) # self._send_buffer[sent:] self._send_buffer = bytearray(0) # self._send_buffer[sent:]
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None: def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
if self.state != self.STATE_UP: if self.state != State.up:
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
' as the state is not UP')
return return
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl) 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
@@ -198,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
@@ -250,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') +
@@ -299,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
@@ -350,7 +395,6 @@ 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 = self.STATE_UP
elif self.ctrl.is_resp(): elif self.ctrl.is_resp():
return # ignore received response return # ignore received response
@@ -366,7 +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 = self.STATE_UP 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
@@ -420,13 +464,19 @@ class Talent(Message):
if self.ctrl.is_req(): if self.ctrl.is_req():
if self.remoteStream.mb.recv_req(data[hdr_len:], if self.remoteStream.mb.recv_req(data[hdr_len:],
self.msg_forward): self.remoteStream.
msg_forward):
self.inc_counter('Modbus_Command') self.inc_counter('Modbus_Command')
else: else:
self.inc_counter('Invalid_Msg_Format') self.inc_counter('Invalid_Msg_Format')
elif self.ctrl.is_ind(): elif self.ctrl.is_ind():
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
self.modbus_elms = 0 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[ for key, update, _ in self.mb.recv_resp(self.db, data[
hdr_len:], hdr_len:],
self.node_id): self.node_id):

View File

@@ -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
''' '''

View File

@@ -123,5 +123,5 @@ class InfosG3P(Infos):
update = False update = False
if update: if update:
self.tracer.log(level, f'[\'{node_id}\']GEN3PLUS: {name}' self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
f' : {result}{unit}') f' : {result}{unit}')

View File

@@ -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:

View File

@@ -6,15 +6,17 @@ 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.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 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
@@ -51,6 +53,8 @@ class Sequence():
class SolarmanV5(Message): class SolarmanV5(Message):
AT_CMD = 1 AT_CMD = 1
MB_RTU_CMD = 2 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, self.send_modbus_cb, mb_timeout=5) super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
@@ -62,6 +66,8 @@ class SolarmanV5(Message):
self.db = InfosG3P() self.db = InfosG3P()
self.time_ofs = 0 self.time_ofs = 0
self.forward_at_cmd_resp = False self.forward_at_cmd_resp = False
self.no_forwarding = False
'''not allowed to connect to TSUN cloud by connection type'''
self.switch = { self.switch = {
0x4210: self.msg_data_ind, # real time data 0x4210: self.msg_data_ind, # real time data
@@ -122,19 +128,36 @@ class SolarmanV5(Message):
if 'at_acl' in g3p_cnf: # pragma: no cover if 'at_acl' in g3p_cnf: # pragma: no cover
self.at_acl = g3p_cnf['at_acl'] 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.log_lvl.clear()
self.state = self.STATE_CLOSED self.state = State.closed
self.mb_timer.close()
super().close() super().close()
async def send_start_cmd(self, snr: int):
self.no_forwarding = True
self.snr = snr
self.__set_serial_no(snr)
self.__send_ack_rsp(0x1710, ftype=0)
await self.async_write('Send Start Command:')
self._send_buffer = bytearray(0)
self.state = State.up
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 64, logging.INFO)
self.mb_timer.start(self.MB_START_TIMEOUT)
def __set_serial_no(self, snr: int): def __set_serial_no(self, snr: int):
serial_no = str(snr) serial_no = str(snr)
if self.unique_id == serial_no: if self.unique_id == serial_no:
@@ -166,7 +189,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:
@@ -181,12 +204,17 @@ class SolarmanV5(Message):
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:
if self.no_forwarding:
return
tsun = Config.get('solarman') tsun = Config.get('solarman')
if tsun['enabled']: if tsun['enabled']:
self._forward_buffer = buffer[:buflen] self._forward_buffer = buffer[:buflen]
@@ -250,6 +278,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()
@@ -261,6 +293,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]
@@ -335,7 +370,9 @@ class SolarmanV5(Message):
self.__finish_send_msg() self.__finish_send_msg()
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str): def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
if self.state != self.STATE_UP: if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
return return
self.__build_header(0x4510) self.__build_header(0x4510)
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD, self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
@@ -347,17 +384,33 @@ class SolarmanV5(Message):
self.writer.write(self._send_buffer) self.writer.write(self._send_buffer)
self._send_buffer = bytearray(0) # self._send_buffer[sent:] self._send_buffer = bytearray(0) # self._send_buffer[sent:]
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None: def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
if self.state != self.STATE_UP: if self.state != State.up:
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
' as the state is not UP')
return return
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl) 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, 0x3000, 48, logging.DEBUG)
if 0 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 64, logging.DEBUG)
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool: def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \ return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
cmd.startswith(tuple(self.at_acl[connection]['block'])) cmd.startswith(tuple(self.at_acl[connection]['block']))
async def send_at_cmd(self, AT_cmd: str) -> None: async def send_at_cmd(self, AT_cmd: str) -> None:
if self.state != self.STATE_UP: if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
' as the state is not UP')
return return
AT_cmd = AT_cmd.strip() AT_cmd = AT_cmd.strip()
@@ -366,8 +419,7 @@ class SolarmanV5(Message):
node_id = self.node_id node_id = self.node_id
key = 'at_resp' key = 'at_resp'
logger.info(f'{key}: {data_json}') logger.info(f'{key}: {data_json}')
asyncio.ensure_future( await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
return return
self.forward_at_cmd_resp = False self.forward_at_cmd_resp = False
@@ -456,7 +508,9 @@ class SolarmanV5(Message):
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)
self.state = self.STATE_UP 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:]
@@ -486,16 +540,19 @@ class SolarmanV5(Message):
elif ftype == self.MB_RTU_CMD: elif ftype == self.MB_RTU_CMD:
if self.remoteStream.mb.recv_req(data[15:], if self.remoteStream.mb.recv_req(data[15:],
self.__forward_msg()): self.remoteStream.
__forward_msg):
self.inc_counter('Modbus_Command') self.inc_counter('Modbus_Command')
else: else:
logger.error('Invalid Modbus Msg')
self.inc_counter('Invalid_Msg_Format') self.inc_counter('Invalid_Msg_Format')
return return
self.__forward_msg() self.__forward_msg()
async def publish_mqtt(self, key, data): def publish_mqtt(self, key, data):
await self.mqtt.publish(key, data) # pragma: no cover asyncio.ensure_future(
self.mqtt.publish(key, data))
def get_cmd_rsp_log_lvl(self) -> int: def get_cmd_rsp_log_lvl(self) -> int:
ftype = self._recv_buffer[self.header_len] ftype = self._recv_buffer[self.header_len]
@@ -519,8 +576,7 @@ class SolarmanV5(Message):
node_id = self.node_id node_id = self.node_id
key = 'at_resp' key = 'at_resp'
logger.info(f'{key}: {data_json}') logger.info(f'{key}: {data_json}')
asyncio.ensure_future( self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
return return
elif ftype == self.MB_RTU_CMD: elif ftype == self.MB_RTU_CMD:
valid = data[1] valid = data[1]
@@ -551,7 +607,9 @@ class SolarmanV5(Message):
self.__forward_msg() self.__forward_msg()
self.__send_ack_rsp(0x1710, ftype) self.__send_ack_rsp(0x1710, ftype)
self.state = self.STATE_UP 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:]

View File

@@ -343,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

View File

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

View File

@@ -1,6 +1,7 @@
import logging import logging
import weakref import weakref
from typing import Callable from typing import Callable, Generator
from enum import Enum
if __name__ == "app.src.messages": if __name__ == "app.src.messages":
@@ -45,21 +46,32 @@ 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 = []
STATE_INIT = 0
STATE_UP = 2
STATE_CLOSED = 3
def __init__(self, server_side: bool, send_modbus_cb: def __init__(self, server_side: bool, send_modbus_cb:
Callable[[bytes, int, str], None], mb_timeout): Callable[[bytes, int, str], None], mb_timeout: int):
self._registry.append(weakref.ref(self)) self._registry.append(weakref.ref(self))
self.server_side = server_side self.server_side = server_side
@@ -72,13 +84,14 @@ class Message(metaclass=IterRegistry):
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 = self.STATE_INIT self.state = State.init
self.shutdown_started = False
''' '''
Empty methods, that have to be implemented in any child class which Empty methods, that have to be implemented in any child class which
@@ -97,7 +110,7 @@ class Message(metaclass=IterRegistry):
''' '''
def close(self) -> None: def close(self) -> None:
if self.mb: if self.mb:
del self.mb self.mb.close()
self.mb = None self.mb = None
pass # pragma: no cover pass # pragma: no cover

View File

@@ -41,7 +41,9 @@ class Modbus():
__crc_tab = [] __crc_tab = []
map = { map = {
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
# 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 0x203e: {'reg': Register.NO_INPUTS, 'fmt': '!H', 'ratio': 1/256}, # noqa: E501
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # 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 0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
@@ -105,7 +107,17 @@ class Modbus():
self.req_pend = False self.req_pend = False
self.tim = None 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): def __del__(self):
"""log statistics on the deleting of a MODBUS instance"""
logging.debug(f'Modbus __del__:\n {self.counter}') logging.debug(f'Modbus __del__:\n {self.counter}')
def build_msg(self, addr: int, func: int, reg: int, val: int, def build_msg(self, addr: int, func: int, reg: int, val: int,
@@ -172,23 +184,25 @@ class Modbus():
self.err = 5 self.err = 5
return return
if not self.__check_crc(buf): if not self.__check_crc(buf):
logger.error('Modbus resp: CRC error') logger.error(f'[{node_id}] Modbus resp: CRC error')
self.err = 1 self.err = 1
return return
if buf[0] != self.last_addr: if buf[0] != self.last_addr:
logger.info(f'Modbus resp: Wrong addr {buf[0]}') logger.info(f'[{node_id}] Modbus resp: Wrong addr {buf[0]}')
self.err = 2 self.err = 2
return return
fcode = buf[1] fcode = buf[1]
if fcode != self.last_fcode: if fcode != self.last_fcode:
logger.info(f'Modbus: Wrong fcode {fcode} != {self.last_fcode}') logger.info(f'[{node_id}] Modbus: Wrong fcode {fcode}'
f' != {self.last_fcode}')
self.err = 3 self.err = 3
return return
if self.last_addr == self.INV_ADDR and \ if self.last_addr == self.INV_ADDR and \
(fcode == 3 or fcode == 4): (fcode == 3 or fcode == 4):
elmlen = buf[2] >> 1 elmlen = buf[2] >> 1
if elmlen != self.last_len: if elmlen != self.last_len:
logger.info(f'Modbus: len error {elmlen} != {self.last_len}') logger.info(f'[{node_id}] Modbus: len error {elmlen}'
f' != {self.last_len}')
self.err = 4 self.err = 4
return return
first_reg = self.last_reg # save last_reg before sending next pdu first_reg = self.last_reg # save last_reg before sending next pdu
@@ -216,7 +230,7 @@ class Modbus():
yield keys[0], update, result yield keys[0], update, result
if update: if update:
info_db.tracer.log(level, info_db.tracer.log(level,
f'[\'{node_id}\']MODBUS: {name}' f'[{node_id}] MODBUS: {name}'
f' : {result}{unit}') f' : {result}{unit}')
else: else:
self.__stop_timer() self.__stop_timer()
@@ -241,6 +255,7 @@ class Modbus():
# logging.debug(f'Modbus stop timer {self}') # logging.debug(f'Modbus stop timer {self}')
if self.tim: if self.tim:
self.tim.cancel() self.tim.cancel()
self.tim = None
def __timeout_cb(self) -> None: def __timeout_cb(self) -> None:
'''Rsponse timeout handler retransmit pdu or send next pdu''' '''Rsponse timeout handler retransmit pdu or send next pdu'''

70
app/src/modbus_tcp.py Normal file
View File

@@ -0,0 +1,70 @@
import logging
import traceback
import asyncio
from config import Config
# import gc
from gen3plus.inverter_g3p import InverterG3P
logger = logging.getLogger('conn')
class ModbusConn():
def __init__(self, host, port):
self.host = host
self.port = port
self.addr = (host, port)
self.stream = None
async def __aenter__(self) -> 'InverterG3P':
'''Establish a client connection to the TSUN cloud'''
connection = asyncio.open_connection(self.host, self.port)
reader, writer = await connection
self.stream = InverterG3P(reader, writer, self.addr)
logging.info(f'[{self.stream.node_id}:{self.stream.conn_no}] '
f'Connected to {self.addr}')
self.stream.inc_counter('Inverter_Cnt')
return self.stream
async def __aexit__(self, exc_type, exc, tb):
self.stream.dec_counter('Inverter_Cnt')
class ModbusTcp():
def __init__(self, loop) -> None:
inverters = Config.get('inverters')
# logging.info(f'Inverters: {inverters}')
for inv in inverters.values():
if (type(inv) is dict
and 'monitor_sn' in inv
and 'client_mode' in inv):
client = inv['client_mode']
# logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
loop.create_task(self.modbus_loop(client['host'],
client['port'],
inv['monitor_sn']))
async def modbus_loop(self, host, port, snr: int) -> None:
'''Loop for receiving messages from the TSUN cloud (client-side)'''
while True:
try:
async with ModbusConn(host, port) as stream:
await stream.send_start_cmd(snr)
await stream.loop()
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
f'Connection closed - Shutdown: '
f'{stream.shutdown_started}')
if stream.shutdown_started:
return
except (ConnectionRefusedError, TimeoutError) as error:
logging.info(f'{error}')
except Exception:
logging.error(
f"ModbusTcpCreate: Exception for {(host,port)}:\n"
f"{traceback.format_exc()}")
await asyncio.sleep(10)

View File

@@ -148,10 +148,10 @@ class Mqtt(metaclass=Singleton):
node_id = topic.split('/')[1] + '/' node_id = topic.split('/')[1] + '/'
# refactor into a loop over a table # refactor into a loop over a table
payload = message.payload.decode("UTF-8") payload = message.payload.decode("UTF-8")
logger_mqtt.info(f'InvCnf: {node_id}:{payload}') logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}')
for m in Message: for m in Message:
if m.server_side and (m.node_id == node_id): if m.server_side and (m.node_id == node_id):
logger_mqtt.info(f'Found: {node_id}') logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, "send_modbus_cmd", None) fnc = getattr(m, "send_modbus_cmd", None)
res = payload.split(',') res = payload.split(',')
if params != len(res): if params != len(res):

35
app/src/my_timer.py Normal file
View 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

View File

@@ -3,8 +3,6 @@ import json
from mqtt import Mqtt from mqtt import Mqtt
from aiocron import crontab from aiocron import crontab
from infos import ClrAtMidnight from infos import ClrAtMidnight
from modbus import Modbus
from messages import Message
logger_mqtt = logging.getLogger('mqtt') logger_mqtt = logging.getLogger('mqtt')
@@ -21,9 +19,6 @@ class Schedule:
crontab('0 0 * * *', func=cls.atmidnight, start=True) crontab('0 0 * * *', func=cls.atmidnight, start=True)
# every minute
crontab('* * * * *', func=cls.regular_modbus_cmds, start=True)
@classmethod @classmethod
async def atmidnight(cls) -> None: async def atmidnight(cls) -> None:
'''Clear daily counters at midnight''' '''Clear daily counters at midnight'''
@@ -33,15 +28,3 @@ class Schedule:
logger_mqtt.debug(f'{key}: {data}') logger_mqtt.debug(f'{key}: {data}')
data_json = json.dumps(data) data_json = json.dumps(data)
await cls.mqtt.publish(f"{key}", data_json) await cls.mqtt.publish(f"{key}", data_json)
@classmethod
async def regular_modbus_cmds(cls):
for m in Message:
if m.server_side:
fnc = getattr(m, "send_modbus_cmd", None)
if callable(fnc):
await fnc(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
if 0 == (cls.count % 30):
# logging.info("Regular Modbus Status request")
await fnc(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
cls.count += 1

View File

@@ -2,6 +2,8 @@ import logging
import asyncio import asyncio
import signal import signal
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
@@ -9,36 +11,96 @@ from gen3.inverter_g3 import InverterG3
from gen3plus.inverter_g3p import InverterG3P from gen3plus.inverter_g3p import InverterG3P
from scheduler import Schedule from scheduler import Schedule
from config import Config from config import Config
from modbus_tcp import ModbusTcp
routes = web.RouteTableDef()
proxy_is_up = False
async def handle_client(reader, writer): @routes.get('/')
async def hello(request):
return web.Response(text="Hello, world")
@routes.get('/-/ready')
async def ready(request):
if proxy_is_up:
status = 200
text = 'Is ready'
else:
status = 503
text = 'Not ready'
return web.Response(status=status, text=text)
@routes.get('/-/healthy')
async def healthy(request):
if proxy_is_up:
# logging.info('web reqeust healthy()')
for 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)
async def handle_shutdown(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, disc all open TCP connections gracefully # first, disc all open TCP connections gracefully
# #
for stream in Message: for stream in Message:
stream.shutdown_started = True
try: try:
await asyncio.wait_for(stream.disc(), 2) await asyncio.wait_for(stream.disc(), 2)
except Exception: except Exception:
pass pass
logging.info('Disconnecting done') logging.info('Proxy disconnecting done')
# #
# second, close all open TCP connections # second, close all open TCP connections
@@ -46,12 +108,27 @@ async def handle_shutdown(loop):
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
#
# now cancel all remaining (pending) tasks
#
pending = asyncio.all_tasks()
for task in pending:
task.cancel()
#
# at last, start a coro for stopping the loop
#
logging.debug("Stop event loop")
loop.stop()
def get_log_level() -> int: def get_log_level() -> int:
@@ -84,16 +161,28 @@ if __name__ == "__main__":
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('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.class_init()
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()
mb_tcp = ModbusTcp(loop)
#
# Create tasks for our listening servers. These must be tasks! If we call
# start_server directly out of our main task, the eventloop will be blocked
# and we can't receive and handle the UNIX signals!
#
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
@@ -102,22 +191,18 @@ if __name__ == "__main__":
for signame in ('SIGINT', 'SIGTERM'): for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), loop.add_signal_handler(getattr(signal, signame),
lambda loop=loop: asyncio.create_task( lambda loop=loop: asyncio.create_task(
handle_shutdown(loop))) 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}"')

View File

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

View File

@@ -5,9 +5,9 @@ from app.src.modbus import Modbus
from app.src.infos import Infos, Register from app.src.infos import Infos, Register
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
pytestmark = pytest.mark.asyncio(scope="module") # pytestmark = pytest.mark.asyncio(scope="module")
class TestHelper(Modbus): class ModbusTestHelper(Modbus):
def __init__(self): def __init__(self):
super().__init__(self.send_cb) super().__init__(self.send_cb)
self.db = Infos() self.db = Infos()
@@ -35,7 +35,7 @@ def test_modbus_crc():
def test_build_modbus_pdu(): def test_build_modbus_pdu():
'''Check building and sending a MODBUS RTU''' '''Check building and sending a MODBUS RTU'''
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,6,0x2000,0x12) mb.build_msg(1,6,0x2000,0x12)
assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07' assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07'
assert mb._Modbus__check_crc(mb.pdu) assert mb._Modbus__check_crc(mb.pdu)
@@ -47,7 +47,7 @@ def test_build_modbus_pdu():
def test_recv_req(): def test_recv_req():
'''Receive a valid request, which must transmitted''' '''Receive a valid request, which must transmitted'''
mb = TestHelper() mb = ModbusTestHelper()
assert mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07') assert mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07')
assert mb.last_fcode == 6 assert mb.last_fcode == 6
assert mb.last_reg == 0x2000 assert mb.last_reg == 0x2000
@@ -56,7 +56,7 @@ def test_recv_req():
def test_recv_req_crc_err(): def test_recv_req_crc_err():
'''Receive a request with invalid CRC, which must be dropped''' '''Receive a request with invalid CRC, which must be dropped'''
mb = TestHelper() mb = ModbusTestHelper()
assert not mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08') assert not mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08')
assert mb.pdu == None assert mb.pdu == None
assert mb.last_fcode == 0 assert mb.last_fcode == 0
@@ -66,7 +66,7 @@ def test_recv_req_crc_err():
def test_recv_resp_crc_err(): def test_recv_resp_crc_err():
'''Receive a response with invalid CRC, which must be dropped''' '''Receive a response with invalid CRC, which must be dropped'''
mb = TestHelper() mb = ModbusTestHelper()
# simulate a transmitted request # simulate a transmitted request
mb.req_pend = True mb.req_pend = True
mb.last_addr = 1 mb.last_addr = 1
@@ -86,7 +86,7 @@ def test_recv_resp_crc_err():
def test_recv_resp_invalid_addr(): def test_recv_resp_invalid_addr():
'''Receive a response with wrong server addr, which must be dropped''' '''Receive a response with wrong server addr, which must be dropped'''
mb = TestHelper() mb = ModbusTestHelper()
mb.req_pend = True mb.req_pend = True
# simulate a transmitted request # simulate a transmitted request
mb.last_addr = 1 mb.last_addr = 1
@@ -109,7 +109,7 @@ def test_recv_resp_invalid_addr():
def test_recv_recv_fcode(): def test_recv_recv_fcode():
'''Receive a response with wrong function code, which must be dropped''' '''Receive a response with wrong function code, which must be dropped'''
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,4,0x300e,2) mb.build_msg(1,4,0x300e,2)
assert mb.que.qsize() == 0 assert mb.que.qsize() == 0
assert mb.req_pend assert mb.req_pend
@@ -130,7 +130,7 @@ def test_recv_recv_fcode():
def test_recv_resp_len(): def test_recv_resp_len():
'''Receive a response with wrong data length, which must be dropped''' '''Receive a response with wrong data length, which must be dropped'''
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,3,0x300e,3) mb.build_msg(1,3,0x300e,3)
assert mb.que.qsize() == 0 assert mb.que.qsize() == 0
assert mb.req_pend assert mb.req_pend
@@ -152,7 +152,7 @@ def test_recv_resp_len():
def test_recv_unexpect_resp(): def test_recv_unexpect_resp():
'''Receive a response when we havb't sent a request''' '''Receive a response when we havb't sent a request'''
mb = TestHelper() mb = ModbusTestHelper()
assert not mb.req_pend assert not mb.req_pend
# check unexpected response, which must be dropped # check unexpected response, which must be dropped
@@ -167,7 +167,7 @@ def test_recv_unexpect_resp():
def test_parse_resp(): def test_parse_resp():
'''Receive matching response and parse the values''' '''Receive matching response and parse the values'''
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,3,0x3007,6) mb.build_msg(1,3,0x3007,6)
assert mb.que.qsize() == 0 assert mb.que.qsize() == 0
assert mb.req_pend assert mb.req_pend
@@ -191,7 +191,7 @@ def test_parse_resp():
assert not mb.req_pend assert not mb.req_pend
def test_queue(): def test_queue():
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,3,0x3022,4) mb.build_msg(1,3,0x3022,4)
assert mb.que.qsize() == 0 assert mb.que.qsize() == 0
assert mb.req_pend assert mb.req_pend
@@ -210,7 +210,7 @@ def test_queue():
def test_queue2(): def test_queue2():
'''Check queue handling for build_msg() calls''' '''Check queue handling for build_msg() calls'''
mb = TestHelper() mb = ModbusTestHelper()
mb.build_msg(1,3,0x3007,6) mb.build_msg(1,3,0x3007,6)
mb.build_msg(1,6,0x2008,4) mb.build_msg(1,6,0x2008,4)
assert mb.que.qsize() == 1 assert mb.que.qsize() == 1
@@ -258,7 +258,7 @@ def test_queue2():
def test_queue3(): def test_queue3():
'''Check queue handling for recv_req() calls''' '''Check queue handling for recv_req() calls'''
mb = TestHelper() mb = ModbusTestHelper()
assert mb.recv_req(b'\x01\x03\x30\x07\x00\x06{\t', mb.resp_handler) assert mb.recv_req(b'\x01\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.recv_req(b'\x01\x06\x20\x08\x00\x04\x02\x0b', mb.resp_handler)
assert mb.que.qsize() == 1 assert mb.que.qsize() == 1
@@ -315,7 +315,7 @@ def test_queue3():
async def test_timeout(): async def test_timeout():
'''Test MODBUS response timeout and RTU retransmitting''' '''Test MODBUS response timeout and RTU retransmitting'''
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
mb = TestHelper() mb = ModbusTestHelper()
mb.max_retries = 2 mb.max_retries = 2
mb.timeout = 0.1 # 100ms timeout for fast testing, expect a time resolution of at least 10ms mb.timeout = 0.1 # 100ms timeout for fast testing, expect a time resolution of at least 10ms
assert asyncio.get_running_loop() == mb.loop assert asyncio.get_running_loop() == mb.loop
@@ -363,7 +363,7 @@ async def test_timeout():
def test_recv_unknown_data(): def test_recv_unknown_data():
'''Receive a response with an unknwon register''' '''Receive a response with an unknwon register'''
mb = TestHelper() mb = ModbusTestHelper()
assert 0x9000 not in mb.map assert 0x9000 not in mb.map
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1} mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}

View File

@@ -7,6 +7,7 @@ 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.modbus import Modbus
from app.src.messages import State
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@@ -24,12 +25,24 @@ class Writer():
def write(self, pdu: bytearray): def write(self, pdu: bytearray):
self.sent_pdu = pdu 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: if server_side:
self.mb.timeout = 1 # overwrite for faster testing self.mb.timeout = 1 # overwrite for faster testing
self.writer = Writer() 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
@@ -43,6 +56,8 @@ class MemoryStream(SolarmanV5):
self.test_exception_async_write = False self.test_exception_async_write = False
self.entity_prfx = '' self.entity_prfx = ''
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}} self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
self.key = ''
self.data = ''
def _timestamp(self): def _timestamp(self):
return timestamp return timestamp
@@ -54,6 +69,10 @@ class MemoryStream(SolarmanV5):
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:
@@ -91,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'
@@ -478,9 +500,10 @@ def AtCommandIndMsgBlock(): # 0x4510
@pytest.fixture @pytest.fixture
def AtCommandRspMsg(): # 0x1510 def AtCommandRspMsg(): # 0x1510
msg = b'\xa5\x0a\x00\x10\x15\x03\x03' +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
@@ -531,6 +554,15 @@ def MsgModbusCmd():
msg += b'\x15' msg += b'\x15'
return msg 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 @pytest.fixture
def MsgModbusCmdCrcErr(): def MsgModbusCmdCrcErr():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02' msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
@@ -820,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
@@ -1243,48 +1274,64 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg,
m.close() m.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg): async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg, AtCommandRspMsg):
ConfigTsunAllowAll ConfigTsunAllowAll
m = MemoryStream(DeviceIndMsg, (0,), True) m = MemoryStream(DeviceIndMsg, (0,), True)
m.append_msg(InverterIndMsg) m.append_msg(InverterIndMsg)
m.read() m.append_msg(AtCommandRspMsg)
m.read() # read device ind
assert m.control == 0x4110 assert m.control == 0x4110
assert str(m.seq) == '01:01' assert str(m.seq) == '01:01'
assert m._recv_buffer==InverterIndMsg # unhandled next message assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # 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._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
await 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==InverterIndMsg # unhandled next message assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
assert m._send_buffer==b'' assert m._send_buffer==b''
assert m._forward_buffer==b'' assert m._forward_buffer==b''
assert str(m.seq) == '01:01' assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
m.read() m.read() # read inverter ind
assert m.control == 0x4210 assert m.control == 0x4210
assert str(m.seq) == '02:02' assert str(m.seq) == '02:02'
assert m._recv_buffer==b'' assert m._recv_buffer==AtCommandRspMsg # unhandled next message
assert m._send_buffer==InverterRspMsg assert m._send_buffer==InverterRspMsg
assert m._forward_buffer==InverterIndMsg 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
await 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) == '02:03' 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._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 m.test_exception_async_write = True
await 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==b''
assert m._send_buffer==b'' assert m._send_buffer==b''
assert m._forward_buffer==b'' assert m._forward_buffer==b''
assert str(m.seq) == '02:04' assert str(m.seq) == '03:04'
assert m.forward_at_cmd_resp == False assert m.forward_at_cmd_resp == False
assert m.mqtt.key == ''
assert m.mqtt.data == ""
m.close() m.close()
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1306,6 +1353,8 @@ async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, In
assert m._send_buffer==b'' assert m._send_buffer==b''
assert m._forward_buffer==b'' assert m._forward_buffer==b''
assert str(m.seq) == '01:01' assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
m.read() m.read()
assert m.control == 0x4210 assert m.control == 0x4210
@@ -1322,6 +1371,8 @@ async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, In
assert m._forward_buffer==b'' assert m._forward_buffer==b''
assert str(m.seq) == '02:02' assert str(m.seq) == '02:02'
assert m.forward_at_cmd_resp == False assert m.forward_at_cmd_resp == False
assert m.mqtt.key == 'at_resp'
assert m.mqtt.data == "'AT+WEBU' is forbidden"
m.close() m.close()
def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg): def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg):
@@ -1386,7 +1437,7 @@ def test_msg_at_command_rsp1(ConfigTsunInv1, AtCommandRspMsg):
assert m.control == 0x1510 assert m.control == 0x1510
assert str(m.seq) == '03:03' assert str(m.seq) == '03:03'
assert m.header_len==11 assert m.header_len==11
assert m.data_len==10 assert m.data_len==17
assert m._forward_buffer==AtCommandRspMsg assert m._forward_buffer==AtCommandRspMsg
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
@@ -1405,16 +1456,18 @@ def test_msg_at_command_rsp2(ConfigTsunInv1, AtCommandRspMsg):
assert m.control == 0x1510 assert m.control == 0x1510
assert str(m.seq) == '03:03' assert str(m.seq) == '03:03'
assert m.header_len==11 assert m.header_len==11
assert m.data_len==10 assert m.data_len==17
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
assert m.db.stat['proxy']['Modbus_Command'] == 0 assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close() m.close()
def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd): def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd, MsgModbusCmdFwd):
ConfigTsunInv1 ConfigTsunInv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.snr = get_sn_int()
m.state = State.up
c = m.createClientStream(MsgModbusCmd) c = m.createClientStream(MsgModbusCmd)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1428,8 +1481,9 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
assert str(c.seq) == '03:02' assert str(c.seq) == '03:02'
assert c.header_len==11 assert c.header_len==11
assert c.data_len==23 assert c.data_len==23
assert c._forward_buffer==MsgModbusCmd assert c._forward_buffer==b''
assert c._send_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']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['AT_Command'] == 0 assert m.db.stat['proxy']['AT_Command'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 1 assert m.db.stat['proxy']['Modbus_Command'] == 1
@@ -1439,6 +1493,8 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr): def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
ConfigTsunInv1 ConfigTsunInv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.snr = get_sn_int()
m.state = State.up
c = m.createClientStream(MsgModbusCmdCrcErr) c = m.createClientStream(MsgModbusCmdCrcErr)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1452,8 +1508,9 @@ def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
assert str(c.seq) == '03:02' assert str(c.seq) == '03:02'
assert c.header_len==11 assert c.header_len==11
assert c.data_len==23 assert c.data_len==23
assert c._forward_buffer==MsgModbusCmdCrcErr assert c._forward_buffer==b''
assert c._send_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']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['AT_Command'] == 0 assert m.db.stat['proxy']['AT_Command'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0 assert m.db.stat['proxy']['Modbus_Command'] == 0
@@ -1643,21 +1700,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
m1 = MemoryStream(MsgInverterInd, (0,)) m1 = MemoryStream(MsgInverterInd, (0,))
m2 = MemoryStream(MsgInverterInd, (0,)) m2 = MemoryStream(MsgInverterInd, (0,))
m3 = MemoryStream(MsgInverterInd, (0,)) m3 = MemoryStream(MsgInverterInd, (0,))
assert m1.state == m1.STATE_INIT assert m1.state == m1.State.init
assert m2.state == m2.STATE_INIT assert m2.state == m2.State.init
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m1.read() # read complete msg, and set unique_id m1.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_INIT assert m1.state == m1.State.init
assert m2.state == m2.STATE_INIT assert m2.state == m2.State.init
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m2.read() # read complete msg, and set unique_id m2.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_CLOSED assert m1.state == m1.State.closed
assert m2.state == m2.STATE_INIT assert m2.state == m2.State.init
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m3.read() # read complete msg, and set unique_id m3.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_CLOSED assert m1.state == m1.State.closed
assert m2.state == m2.STATE_CLOSED assert m2.state == m2.State.closed
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m1.close() m1.close()
m2.close() m2.close()
m3.close() m3.close()

View File

@@ -4,6 +4,7 @@ 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, Register from app.src.infos import Infos, Register
from app.src.modbus import Modbus from app.src.modbus import Modbus
from app.src.messages import State
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@@ -60,7 +61,8 @@ 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,)): def createClientStream(self, msg, chunks = (0,)):
c = MemoryStream(msg, chunks, False) c = MemoryStream(msg, chunks, False)
@@ -113,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'
@@ -129,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'
@@ -145,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'
@@ -331,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()
@@ -471,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()
@@ -489,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()
@@ -507,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()
@@ -525,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)
@@ -543,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''
@@ -560,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''
@@ -567,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
@@ -580,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()
@@ -616,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,))
@@ -635,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)
@@ -673,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
@@ -692,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
@@ -714,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
@@ -734,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
@@ -847,7 +1056,7 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
ConfigTsunInv1 ConfigTsunInv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.id_str = b"R170000000000001" m.id_str = b"R170000000000001"
m.state = m.STATE_UP m.state = State.up
c = m.createClientStream(MsgModbusCmd) c = m.createClientStream(MsgModbusCmd)
@@ -953,6 +1162,29 @@ def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
assert m.db.stat['proxy']['Modbus_Command'] == 0 assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close() 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): def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20):
'''Modbus response with a valid Modbus request must be forwarded''' '''Modbus response with a valid Modbus request must be forwarded'''
ConfigTsunInv1 ConfigTsunInv1
@@ -1097,7 +1329,7 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
assert m._send_buffer == b'' assert m._send_buffer == b''
assert m.writer.sent_pdu == b'' assert m.writer.sent_pdu == b''
m.state = m.STATE_UP m.state = State.up
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG) await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
assert 0 == m.send_msg_ofs assert 0 == m.send_msg_ofs
assert m._forward_buffer == b'' assert m._forward_buffer == b''
@@ -1127,21 +1359,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
m3 = MemoryStream(MsgInverterInd, (0,)) m3 = MemoryStream(MsgInverterInd, (0,))
assert MemoryStream._RefNo == 3 + start_val assert MemoryStream._RefNo == 3 + start_val
assert m3.RefNo == 3 + start_val assert m3.RefNo == 3 + start_val
assert m1.state == m1.STATE_INIT assert m1.state == m1.State.init
assert m2.state == m2.STATE_INIT assert m2.state == m2.State.init
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m1.read() # read complete msg, and set unique_id m1.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_UP assert m1.state == m1.State.up
assert m2.state == m2.STATE_INIT assert m2.state == m2.State.init
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m2.read() # read complete msg, and set unique_id m2.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_CLOSED assert m1.state == m1.State.closed
assert m2.state == m2.STATE_UP assert m2.state == m2.State.up
assert m3.state == m3.STATE_INIT assert m3.state == m3.State.init
m3.read() # read complete msg, and set unique_id m3.read() # read complete msg, and set unique_id
assert m1.state == m1.STATE_CLOSED assert m1.state == m1.State.closed
assert m2.state == m2.STATE_CLOSED assert m2.state == m2.State.closed
assert m3.state == m3.STATE_UP assert m3.state == m3.State.up
m1.close() m1.close()
m2.close() m2.close()
m3.close() m3.close()

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View 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]

View File

@@ -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,13 +53,12 @@ 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:
@@ -78,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
@@ -94,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

View File

@@ -89,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")
@@ -155,4 +173,22 @@ def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
# 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)