Compare commits
101 Commits
v0.8.0-rc.
...
ssl-connec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2632698008 | ||
|
|
a42ba8a8c6 | ||
|
|
210c02f0b9 | ||
|
|
a51ac03021 | ||
|
|
e6b726912a | ||
|
|
7c48ee4065 | ||
|
|
4e89abd2c9 | ||
|
|
f304aa009e | ||
|
|
9e218fdf41 | ||
|
|
18f6332784 | ||
|
|
26aebbcab8 | ||
|
|
a9c7ea386e | ||
|
|
6332976c4a | ||
|
|
cc233dcb17 | ||
|
|
9a9cf79aac | ||
|
|
3ce29d4a96 | ||
|
|
a09d489c94 | ||
|
|
f3e69ff217 | ||
|
|
a3c054d2b1 | ||
|
|
2d4679a361 | ||
|
|
9ff1453922 | ||
|
|
5b36efc5e9 | ||
|
|
c71994c839 | ||
|
|
7d058e74fe | ||
|
|
373916bead | ||
|
|
f4b434cfef | ||
|
|
d14cbe87a2 | ||
|
|
8aa1ef59ce | ||
|
|
3d55ac57a8 | ||
|
|
8088e6ab3c | ||
|
|
4372e49a1e | ||
|
|
da832232bb | ||
|
|
e0568291f6 | ||
|
|
f5e7aa4292 | ||
|
|
5e360e1139 | ||
|
|
94f7f5faa2 | ||
|
|
4600fc9577 | ||
|
|
fa7bfe9e16 | ||
|
|
3cebab40c8 | ||
|
|
4649beb075 | ||
|
|
9138affdb9 | ||
|
|
80183598ca | ||
|
|
b688d04836 | ||
|
|
377c09bc66 | ||
|
|
abb9e7c280 | ||
|
|
d78e32dd12 | ||
|
|
30a6f75430 | ||
|
|
e22ad78dcd | ||
|
|
453d8b2aa2 | ||
|
|
f9b02f3486 | ||
|
|
b053c7e576 | ||
|
|
10346e888f | ||
|
|
f629246dbd | ||
|
|
dbff66affd | ||
|
|
ac534c20ed | ||
|
|
ff3ed83b49 | ||
|
|
ae94cd62fc | ||
|
|
a16a19cc2c | ||
|
|
dd351176bd | ||
|
|
cc8674d108 | ||
|
|
d7767cb5ea | ||
|
|
1e3bb31ef8 | ||
|
|
d6a44d9173 | ||
|
|
43a2ef5712 | ||
|
|
3209ebabde | ||
|
|
aac6cfd629 | ||
|
|
e8d32b45a5 | ||
|
|
06b63f554d | ||
|
|
53f6a5447d | ||
|
|
d6093e6b11 | ||
|
|
c8113e2f60 | ||
|
|
57d6785f15 | ||
|
|
ff8adb5632 | ||
|
|
1deab4be6a | ||
|
|
730229cfb0 | ||
|
|
7b9550773d | ||
|
|
3bc2b262b5 | ||
|
|
37c2246132 | ||
|
|
d0bd599420 | ||
|
|
c34b33ed5f | ||
|
|
661f699444 | ||
|
|
a499c5e6b0 | ||
|
|
0a18918326 | ||
|
|
9985917ad2 | ||
|
|
851bd54d8f | ||
|
|
aa3bb4a1fa | ||
|
|
a62864218d | ||
|
|
0b2631c162 | ||
|
|
c59bd16664 | ||
|
|
039a021cda | ||
|
|
49e2dfbd86 | ||
|
|
e6ecf5911b | ||
|
|
6e1ed5d1e7 | ||
|
|
ad885e9644 | ||
|
|
8f81ceda98 | ||
|
|
8204cae2b1 | ||
|
|
8baa68e615 | ||
|
|
81d551e47f | ||
|
|
63547bb51f | ||
|
|
6eebd0c852 | ||
|
|
7b4ed406a1 |
2
.github/workflows/python-app.yml
vendored
2
.github/workflows/python-app.yml
vendored
@@ -5,7 +5,7 @@ name: Python application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev-*" ]
|
||||
branches: [ "main", "dev-*", "*/issue*" ]
|
||||
paths-ignore:
|
||||
- '**.md' # Do no build on *.md changes
|
||||
- '**.yml' # Do no build on *.yml changes
|
||||
|
||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Aktuelle Datei",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -5,15 +5,52 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [unreleased]
|
||||
|
||||
## [0.9.0] - 2024-07-01
|
||||
|
||||
- fix exception in MODBUS timeout callback
|
||||
|
||||
## [0.9.0-RC1] - 2024-06-29
|
||||
|
||||
- add asyncio log and debug mode
|
||||
- stop the HTTP server on shutdown gracefully
|
||||
- Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to
|
||||
unexpected packets. [#111](https://github.com/s-allius/tsun-gen3-proxy/issues/111)
|
||||
- GEN3: avoid sending MODBUS commands to the inverter during the inverter's reporting phase
|
||||
- GEN3: determine the connection timeout based on the connection state
|
||||
- GEN3: support more data encodings for DSP version V5.0.17 [#108](https://github.com/s-allius/tsun-gen3-proxy/issues/108)
|
||||
- detect dead connections [#100](https://github.com/s-allius/tsun-gen3-proxy/issues/100)
|
||||
- improve connection logging wirt a unique connection id
|
||||
- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91)
|
||||
- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93)
|
||||
- add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94)
|
||||
- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)
|
||||
- GEN3PLUS: dump invalid packages with wrong start or stop byte
|
||||
- label debug imagages als `debug`
|
||||
- print imgae build time during proxy start
|
||||
- add type annotations
|
||||
- improve async unit test and fix pytest warnings
|
||||
- run github tests even for pulls on issue branches
|
||||
|
||||
## [0.8.1] - 2024-06-21
|
||||
|
||||
- Fix MODBUS responses are dropped and not forwarded to the TSUN cloud [#104](https://github.com/s-allius/tsun-gen3-proxy/issues/104)
|
||||
- GEN3: Fix connections losts due MODBUS requests [#102](https://github.com/s-allius/tsun-gen3-proxy/issues/102)
|
||||
|
||||
## [0.8.0] - 2024-06-07
|
||||
|
||||
- improve logging: add protocol or node_id to connection logs
|
||||
- improve logging: log ignored AT+ or MODBUS commands
|
||||
- improve tracelog: log level depends on message type and source
|
||||
- fix typo in docker-compose.yaml and remove the external network definition
|
||||
- trace heartbeat and regular modbus pakets witl log level DEBUG
|
||||
- GEN3PLUS: don't forward ack paket from tsun to the inverter
|
||||
- add allow and block filter for AT+ commands
|
||||
- GEN3PLUS: add allow and block filter for AT+ commands
|
||||
- catch all OSError errors in the read loop
|
||||
- log Modbus traces with different log levels
|
||||
- add Modbus fifo and timeout handler
|
||||
- build version string in the same format as TSUN for GEN3 invterts
|
||||
- build version string in the same format as TSUN for GEN3 inverters
|
||||
- add graceful shutdown
|
||||
- parse Modbus values and store them in the database
|
||||
- add cron task to request the output power every minute
|
||||
@@ -24,7 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- change logging level to DEBUG for some logs
|
||||
- remove experimental value Register.VALUE_1
|
||||
- format Register.POWER_ON_TIME as integer
|
||||
- ignore non realtime values for now
|
||||
- ignore catch-up values from the inverters for now
|
||||
|
||||
## [0.7.0] - 2024-04-20
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||
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.
|
||||
|
||||
<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 (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>
|
||||
@@ -241,7 +241,7 @@ Legend
|
||||
🚧: Proxy support in preparation
|
||||
```
|
||||
|
||||
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E`
|
||||
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` or `Y47E` instead of `R17E`
|
||||
|
||||
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN apk upgrade --no-cache && \
|
||||
|
||||
#
|
||||
# second stage for building wheels packages
|
||||
FROM base as builder
|
||||
FROM base AS builder
|
||||
|
||||
# copy the dependencies file to the root dir and install requirements
|
||||
COPY ./requirements.txt /root/
|
||||
@@ -26,7 +26,7 @@ RUN apk add --no-cache build-base && \
|
||||
|
||||
#
|
||||
# third stage for our runtime image
|
||||
FROM base as runtime
|
||||
FROM base AS runtime
|
||||
ARG SERVICE_NAME
|
||||
ARG VERSION
|
||||
ARG UID
|
||||
@@ -45,7 +45,7 @@ ENV HOME=/home/$SERVICE_NAME
|
||||
# set the working directory in the container
|
||||
WORKDIR /home/$SERVICE_NAME
|
||||
|
||||
VOLUME ["/home/$SERVICE_NAME/log", "/home/$SERVICE_NAME/config"]
|
||||
VOLUME ["/home/$SERVICE_NAME/log", "/home/$SERVICE_NAME/config", "/home/$SERVICE_NAME/cert"]
|
||||
|
||||
# install the requirements from the wheels packages from the builder stage
|
||||
# and unistall python packages and alpine package manger to reduce attack surface
|
||||
@@ -63,8 +63,8 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||
COPY config .
|
||||
COPY src .
|
||||
|
||||
EXPOSE 5005
|
||||
RUN date > /build-date.txt
|
||||
EXPOSE 5005 8127 10000 10443
|
||||
|
||||
# command to run on container start
|
||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||
@@ -73,7 +73,7 @@ CMD [ "python3", "./server.py" ]
|
||||
|
||||
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
|
||||
LABEL org.opencontainers.image.authors="Stefan Allius"
|
||||
LABEL org.opencontainers.image.source https://github.com/s-allius/tsun-gen3-proxy
|
||||
LABEL org.opencontainers.image.description 'This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
||||
LABEL org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy
|
||||
LABEL org.opencontainers.image.description='This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
||||
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
|
||||
LABEL org.opencontainers.image.vendor="Stefan Allius"
|
||||
|
||||
19
app/build.sh
19
app/build.sh
@@ -4,7 +4,7 @@
|
||||
# rc: release candidate build
|
||||
# rel: release build and push to ghcr.io
|
||||
# Note: for release build, you need to set GHCR_TOKEN
|
||||
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .profile
|
||||
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .zprofile
|
||||
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ fi
|
||||
|
||||
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||
if [[ $1 == debug ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:debug app
|
||||
elif [[ $1 == dev ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||
|
||||
@@ -39,14 +39,17 @@ elif [[ $1 == rc ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:rc
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:rc
|
||||
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
|
||||
elif [[ $1 == rel ]];then
|
||||
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
fi
|
||||
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
|
||||
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||
fi
|
||||
|
||||
echo 'check docker-compose.yaml file'
|
||||
docker-compose config -q
|
||||
@@ -5,6 +5,7 @@ user="$(id -u)"
|
||||
echo "######################################################"
|
||||
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
||||
echo "# for running with UserID:$UID, GroupID:$GID"
|
||||
echo "# Image built: $(cat /build-date.txt) "
|
||||
echo "#"
|
||||
|
||||
if [ "$user" = '0' ]; then
|
||||
|
||||
@@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \
|
||||
-name od -o \
|
||||
-name strings -o \
|
||||
-name su -o \
|
||||
-name wget -o \
|
||||
\) -delete \
|
||||
; fi
|
||||
|
||||
424
app/proxy.svg
424
app/proxy.svg
@@ -4,246 +4,254 @@
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="673pt" height="1216pt"
|
||||
viewBox="0.00 0.00 673.35 1216.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1212)">
|
||||
<svg width="691pt" height="1312pt"
|
||||
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 1308)">
|
||||
<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 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1112 .1516,-1112 .1516,-1076 114.5444,-1076 114.5444,-1106 108.5444,-1112"/>
|
||||
<polyline fill="none" stroke="#000000" points="108.5444,-1112 108.5444,-1106 "/>
|
||||
<polyline fill="none" stroke="#000000" points="114.5444,-1106 108.5444,-1106 "/>
|
||||
<text text-anchor="middle" x="57.348" y="-1097" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||
<text text-anchor="middle" x="57.348" y="-1085" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1208 .1516,-1208 .1516,-1172 114.5444,-1172 114.5444,-1202 108.5444,-1208"/>
|
||||
<polyline fill="none" stroke="#000000" points="108.5444,-1208 108.5444,-1202 "/>
|
||||
<polyline fill="none" stroke="#000000" points="114.5444,-1202 108.5444,-1202 "/>
|
||||
<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="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="639.0297,-816 569.6663,-816 569.6663,-780 639.0297,-780 639.0297,-816"/>
|
||||
<text text-anchor="middle" x="604.348" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
||||
<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="622.348" y="-885" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-524 543.348,-556 665.348,-556 665.348,-524 543.348,-524"/>
|
||||
<text text-anchor="start" x="594.625" y="-537" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-468 543.348,-524 665.348,-524 665.348,-468 543.348,-468"/>
|
||||
<text text-anchor="start" x="561.8355" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||
<text text-anchor="start" x="569.6145" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||
<text text-anchor="start" x="553.2215" y="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||
<polygon fill="none" stroke="#000000" points="543.348,-424 543.348,-468 665.348,-468 665.348,-424 543.348,-424"/>
|
||||
<text text-anchor="start" x="566.284" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||
<text text-anchor="start" x="570.4525" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||
<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="612.625" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||
<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="579.8355" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||
<text text-anchor="start" x="587.6145" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||
<text text-anchor="start" x="571.2215" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||
<polygon fill="none" stroke="#000000" points="561.348,-508 561.348,-552 683.348,-552 683.348,-508 561.348,-508"/>
|
||||
<text text-anchor="start" x="584.284" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||
<text text-anchor="start" x="588.4525" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||
</g>
|
||||
<!-- A1->A2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A1->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M604.348,-769.6429C604.348,-721.5141 604.348,-622.6159 604.348,-556.2865"/>
|
||||
<polygon fill="none" stroke="#000000" points="600.8481,-769.6555 604.348,-779.6556 607.8481,-769.6556 600.8481,-769.6555"/>
|
||||
<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="618.8481,-859.7608 622.348,-869.7608 625.8481,-859.7608 618.8481,-859.7608"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-282 550.348,-314 658.348,-314 658.348,-282 550.348,-282"/>
|
||||
<text text-anchor="start" x="587.4015" y="-295" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-190 550.348,-282 658.348,-282 658.348,-190 550.348,-190"/>
|
||||
<text text-anchor="start" x="580.452" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
|
||||
<text text-anchor="start" x="573.7885" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
|
||||
<text text-anchor="start" x="564.6235" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
||||
<text text-anchor="start" x="564.0595" y="-227" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
||||
<text text-anchor="start" x="560.1705" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
||||
<text text-anchor="start" x="576.0135" y="-203" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
||||
<polygon fill="none" stroke="#000000" points="550.348,-170 550.348,-190 658.348,-190 658.348,-170 550.348,-170"/>
|
||||
<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="605.4015" y="-337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
||||
<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="598.452" y="-305" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</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="582.6235" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
||||
<text text-anchor="start" x="582.0595" y="-269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
||||
<text text-anchor="start" x="578.1705" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
||||
<text text-anchor="start" x="594.0135" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
||||
<polygon fill="none" stroke="#000000" points="568.348,-212 568.348,-232 676.348,-232 676.348,-212 568.348,-212"/>
|
||||
</g>
|
||||
<!-- A2->A11 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A2->A11</title>
|
||||
<path fill="none" stroke="#000000" d="M604.348,-423.8663C604.348,-390.0029 604.348,-348.7174 604.348,-314.0468"/>
|
||||
<path fill="none" stroke="#000000" d="M622.348,-507.8316C622.348,-462.6124 622.348,-402.6972 622.348,-356.2361"/>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-276 274.348,-308 346.348,-308 346.348,-276 274.348,-276"/>
|
||||
<text text-anchor="start" x="292.5655" y="-289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-232 274.348,-276 346.348,-276 346.348,-232 274.348,-232"/>
|
||||
<text text-anchor="start" x="304.2395" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="290.9015" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<polygon fill="none" stroke="#000000" points="274.348,-176 274.348,-232 346.348,-232 346.348,-176 274.348,-176"/>
|
||||
<text text-anchor="start" x="284.238" y="-213" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="287.572" y="-201" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="285.072" y="-189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<polygon fill="none" stroke="#000000" points="257.348,-366 257.348,-398 364.348,-398 364.348,-366 257.348,-366"/>
|
||||
<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="257.348,-226 257.348,-366 364.348,-366 364.348,-226 257.348,-226"/>
|
||||
<text text-anchor="start" x="302.5095" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="283.338" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="284.453" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="266.9565" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout:max_retires</text>
|
||||
<text text-anchor="start" x="292.79" y="-287" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="304.7395" y="-275" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="291.4015" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="289.727" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="304.1845" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="257.348,-170 257.348,-226 364.348,-226 364.348,-170 257.348,-170"/>
|
||||
<text text-anchor="start" x="284.738" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="288.072" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="285.572" y="-183" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1104 263.348,-1136 334.348,-1136 334.348,-1104 263.348,-1104"/>
|
||||
<text text-anchor="start" x="273.293" y="-1117" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1084 263.348,-1104 334.348,-1104 334.348,-1084 263.348,-1084"/>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1052 263.348,-1084 334.348,-1084 334.348,-1052 263.348,-1052"/>
|
||||
<text text-anchor="start" x="280.787" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
||||
<polygon fill="none" stroke="#000000" points="263.348,-1200 263.348,-1232 334.348,-1232 334.348,-1200 263.348,-1200"/>
|
||||
<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,-1180 263.348,-1200 334.348,-1200 334.348,-1180 263.348,-1180"/>
|
||||
<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="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-898 231.348,-930 365.348,-930 365.348,-898 231.348,-898"/>
|
||||
<text text-anchor="start" x="278.0655" y="-911" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-734 231.348,-898 365.348,-898 365.348,-734 231.348,-734"/>
|
||||
<text text-anchor="start" x="261.6745" y="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="258.891" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="251.662" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
||||
<text text-anchor="start" x="257.496" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
||||
<text text-anchor="start" x="276.6725" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="280.5615" y="-819" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="277.5065" y="-807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
||||
<text text-anchor="start" x="248.337" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="246.9425" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="241.1145" y="-771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="280.5615" y="-759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
||||
<text text-anchor="start" x="269.174" y="-747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-666 231.348,-734 365.348,-734 365.348,-666 231.348,-666"/>
|
||||
<text text-anchor="start" x="248.0575" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
||||
<text text-anchor="start" x="272.7925" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
||||
<text text-anchor="start" x="258.6205" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
||||
<text text-anchor="start" x="256.9505" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-994 231.348,-1026 365.348,-1026 365.348,-994 231.348,-994"/>
|
||||
<text text-anchor="start" x="278.0655" y="-1007" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-818 231.348,-994 365.348,-994 365.348,-818 231.348,-818"/>
|
||||
<text text-anchor="start" x="261.6745" y="-975" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="258.891" y="-963" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="251.662" y="-951" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
||||
<text text-anchor="start" x="257.496" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
||||
<text text-anchor="start" x="276.6725" y="-927" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="280.5615" y="-915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="277.5065" y="-903" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
||||
<text text-anchor="start" x="248.337" y="-891" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="246.9425" y="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="241.1145" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
||||
<text text-anchor="start" x="280.5615" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
||||
<text text-anchor="start" x="269.174" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
||||
<text text-anchor="start" x="287.51" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state</text>
|
||||
<polygon fill="none" stroke="#000000" points="231.348,-750 231.348,-818 365.348,-818 365.348,-750 231.348,-750"/>
|
||||
<text text-anchor="start" x="248.0575" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
||||
<text text-anchor="start" x="272.7925" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
||||
<text text-anchor="start" x="258.6205" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
||||
<text text-anchor="start" x="256.9505" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
||||
</g>
|
||||
<!-- A4->A5 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A4->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M298.348,-1041.7414C298.348,-1010.6043 298.348,-969.5621 298.348,-930.0536"/>
|
||||
<polygon fill="none" stroke="#000000" points="294.8481,-1041.9047 298.348,-1051.9048 301.8481,-1041.9048 294.8481,-1041.9047"/>
|
||||
<path fill="none" stroke="#000000" d="M298.348,-1137.5879C298.348,-1106.6429 298.348,-1065.8843 298.348,-1026.2983"/>
|
||||
<polygon fill="none" stroke="#000000" points="294.8481,-1137.6902 298.348,-1147.6902 301.8481,-1137.6902 294.8481,-1137.6902"/>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-584 370.348,-616 484.348,-616 484.348,-584 370.348,-584"/>
|
||||
<text text-anchor="start" x="413.456" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-480 370.348,-584 484.348,-584 484.348,-480 370.348,-480"/>
|
||||
<text text-anchor="start" x="380.111" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="415.1255" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="395.948" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="399.288" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="402.8925" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="401.232" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="413.46" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-364 370.348,-480 484.348,-480 484.348,-364 370.348,-364"/>
|
||||
<text text-anchor="start" x="384.8405" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="386.7805" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="392.6245" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="380.6765" y="-425" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="382.6215" y="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="391.7885" y="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="412.3505" y="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-668 370.348,-700 484.348,-700 484.348,-668 370.348,-668"/>
|
||||
<text text-anchor="start" x="413.456" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-564 370.348,-668 484.348,-668 484.348,-564 370.348,-564"/>
|
||||
<text text-anchor="start" x="380.111" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="415.1255" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="395.948" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="399.288" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="402.8925" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="401.232" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="413.46" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="370.348,-448 370.348,-564 484.348,-564 484.348,-448 370.348,-448"/>
|
||||
<text text-anchor="start" x="384.8405" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="386.7805" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="392.6245" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="380.6765" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="382.6215" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="391.7885" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="412.3505" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M357.819,-656.0073C363.3477,-642.8069 368.9261,-629.488 374.3902,-616.442"/>
|
||||
<polygon fill="none" stroke="#000000" points="354.4392,-655.017 353.8043,-665.5928 360.8958,-657.7213 354.4392,-655.017"/>
|
||||
<path fill="none" stroke="#000000" d="M358.9294,-740.5383C364.4479,-727.1056 370.0049,-713.5794 375.4378,-700.355"/>
|
||||
<polygon fill="none" stroke="#000000" points="355.6797,-739.2382 355.117,-749.8181 362.1546,-741.8983 355.6797,-739.2382"/>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-548 127.348,-580 218.348,-580 218.348,-548 127.348,-548"/>
|
||||
<text text-anchor="start" x="145.343" y="-561" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-456 127.348,-548 218.348,-548 218.348,-456 127.348,-456"/>
|
||||
<text text-anchor="start" x="157.846" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="160.9055" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="165.904" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="145.058" y="-493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="146.732" y="-481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="158.96" y="-469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-400 127.348,-456 218.348,-456 218.348,-400 127.348,-400"/>
|
||||
<text text-anchor="start" x="137.2885" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="157.8505" y="-413" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-632 127.348,-664 218.348,-664 218.348,-632 127.348,-632"/>
|
||||
<text text-anchor="start" x="145.343" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-540 127.348,-632 218.348,-632 218.348,-540 127.348,-540"/>
|
||||
<text text-anchor="start" x="157.846" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="160.9055" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="165.904" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="145.058" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="146.732" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="158.96" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="127.348,-484 127.348,-540 218.348,-540 218.348,-484 127.348,-484"/>
|
||||
<text text-anchor="start" x="137.2885" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="157.8505" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A5->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A5->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M240.2947,-656.0919C229.716,-630.2328 218.9501,-603.9162 209.2,-580.0827"/>
|
||||
<polygon fill="none" stroke="#000000" points="237.1556,-657.6626 244.1814,-665.5928 243.6344,-655.0121 237.1556,-657.6626"/>
|
||||
<path fill="none" stroke="#000000" d="M239.076,-740.2903C228.6761,-714.3733 218.1403,-688.1174 208.6075,-664.3611"/>
|
||||
<polygon fill="none" stroke="#000000" points="235.9268,-741.8409 242.8992,-749.8181 242.4233,-739.2339 235.9268,-741.8409"/>
|
||||
</g>
|
||||
<!-- A6->A3 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A6->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M370.219,-368.906C361.9756,-351.4328 353.6959,-333.8827 346.0351,-317.6444"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="341.601,-308.2456 349.9376,-315.3696 343.7344,-312.7676 345.8678,-317.2897 345.8678,-317.2897 345.8678,-317.2897 343.7344,-312.7676 341.7979,-319.2097 341.601,-308.2456 341.601,-308.2456"/>
|
||||
<text text-anchor="middle" x="356.9793" y="-318.0326" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="354.8406" y="-353.119" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<path fill="none" stroke="#000000" d="M376.3705,-447.6454C371.0187,-434.3805 365.5816,-420.9039 360.2423,-407.6696"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="356.3743,-398.0824 364.289,-405.6724 358.2451,-402.7192 360.1158,-407.3561 360.1158,-407.3561 360.1158,-407.3561 358.2451,-402.7192 355.9427,-409.0397 356.3743,-398.0824 356.3743,-398.0824"/>
|
||||
<text text-anchor="middle" x="370.9946" y="-408.7296" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="361.7502" y="-430.9982" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-258 364.348,-290 514.348,-290 514.348,-258 364.348,-258"/>
|
||||
<text text-anchor="start" x="407.3935" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-226 364.348,-258 514.348,-258 514.348,-226 364.348,-226"/>
|
||||
<text text-anchor="start" x="374.335" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="364.348,-194 364.348,-226 514.348,-226 514.348,-194 364.348,-194"/>
|
||||
<text text-anchor="start" x="424.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="382.348,-300 382.348,-332 532.348,-332 532.348,-300 382.348,-300"/>
|
||||
<text text-anchor="start" x="425.3935" y="-313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="382.348,-268 382.348,-300 532.348,-300 532.348,-268 382.348,-268"/>
|
||||
<text text-anchor="start" x="392.335" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="382.348,-236 382.348,-268 532.348,-268 532.348,-236 382.348,-236"/>
|
||||
<text text-anchor="start" x="442.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A6->A8 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A6->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M433.943,-353.7029C435.0429,-330.9727 436.1158,-308.7991 437.012,-290.2778"/>
|
||||
<polygon fill="none" stroke="#000000" points="430.441,-353.6629 433.4535,-363.8204 437.4328,-354.0013 430.441,-353.6629"/>
|
||||
<path fill="none" stroke="#000000" d="M441.464,-437.5454C445.3714,-399.7739 449.3591,-361.2265 452.3615,-332.203"/>
|
||||
<polygon fill="none" stroke="#000000" points="437.9668,-437.3383 440.4192,-447.6454 444.9297,-438.0587 437.9668,-437.3383"/>
|
||||
</g>
|
||||
<!-- A7->A3 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A7->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M208.5342,-399.8871C214.3752,-387.5945 220.7034,-375.3217 227.348,-364 241.4757,-339.9278 249.4905,-336.9696 265.348,-314 266.3556,-312.5405 267.3678,-311.0592 268.3821,-309.5614"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="274.1947,-300.8381 272.3944,-311.6552 271.4221,-304.999 268.6496,-309.1599 268.6496,-309.1599 268.6496,-309.1599 271.4221,-304.999 264.9048,-306.6646 274.1947,-300.8381 274.1947,-300.8381"/>
|
||||
<text text-anchor="middle" x="271.1774" y="-317.6092" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="208.7462" y="-376.8883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<path fill="none" stroke="#000000" d="M210.935,-483.8952C216.3404,-471.7801 221.9084,-459.553 227.348,-448 235.1472,-431.4354 243.6196,-414.0579 252.0717,-397.0641"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="256.7608,-387.6701 256.3209,-398.6272 254.5277,-392.1437 252.2946,-396.6174 252.2946,-396.6174 252.2946,-396.6174 254.5277,-392.1437 248.2683,-394.6076 256.7608,-387.6701 256.7608,-387.6701"/>
|
||||
<text text-anchor="middle" x="256.228" y="-404.663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<text text-anchor="middle" x="210.6174" y="-460.8977" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-258 82.348,-290 238.348,-290 238.348,-258 82.348,-258"/>
|
||||
<text text-anchor="start" x="125.059" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-226 82.348,-258 238.348,-258 238.348,-226 82.348,-226"/>
|
||||
<text text-anchor="start" x="92.0005" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="82.348,-194 82.348,-226 238.348,-226 238.348,-194 82.348,-194"/>
|
||||
<text text-anchor="start" x="145.3505" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="64.348,-300 64.348,-332 220.348,-332 220.348,-300 64.348,-300"/>
|
||||
<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="64.348,-268 64.348,-300 220.348,-300 220.348,-268 64.348,-268"/>
|
||||
<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="64.348,-236 64.348,-268 220.348,-268 220.348,-236 64.348,-236"/>
|
||||
<text text-anchor="start" x="127.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M167.4931,-389.6656C165.8292,-355.2775 164.0432,-318.3674 162.6731,-290.0512"/>
|
||||
<polygon fill="none" stroke="#000000" points="164.0025,-389.9456 167.9818,-399.7648 170.9943,-389.6073 164.0025,-389.9456"/>
|
||||
<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="158.5098,-474.2451 163.0203,-483.8319 165.4726,-473.5248 158.5098,-474.2451"/>
|
||||
</g>
|
||||
<!-- A8->A8 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A8->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M514.5164,-272.6238C525.1874,-267.6708 532.348,-257.4629 532.348,-242 532.348,-231.1277 528.8079,-222.8533 522.9966,-217.1769"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="514.5164,-211.3762 525.3108,-213.3079 518.6433,-214.1991 522.7702,-217.0221 522.7702,-217.0221 522.7702,-217.0221 518.6433,-214.1991 520.2296,-220.7363 514.5164,-211.3762 514.5164,-211.3762"/>
|
||||
<text text-anchor="middle" x="534.2494" y="-211.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="526.5555" y="-253.6532" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<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="532.5164,-246.3092 543.0929,-249.2054 536.3722,-249.4924 540.228,-252.6756 540.228,-252.6756 540.228,-252.6756 536.3722,-249.4924 537.3632,-256.1459 532.5164,-246.3092 532.5164,-246.3092"/>
|
||||
<text text-anchor="middle" x="551.8757" y="-248.3308" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="543.0584" y="-301.6947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,-88 460.348,-120 582.348,-120 582.348,-88 460.348,-88"/>
|
||||
<text text-anchor="start" x="497.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,-56 460.348,-88 582.348,-88 582.348,-56 460.348,-56"/>
|
||||
<text text-anchor="start" x="490.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<polygon fill="none" stroke="#000000" points="460.348,0 460.348,-56 582.348,-56 582.348,0 460.348,0"/>
|
||||
<text text-anchor="start" x="469.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||
<text text-anchor="start" x="506.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="478.348,-88 478.348,-120 600.348,-120 600.348,-88 478.348,-88"/>
|
||||
<text text-anchor="start" x="515.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="478.348,-56 478.348,-88 600.348,-88 600.348,-56 478.348,-56"/>
|
||||
<text text-anchor="start" x="508.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||
<polygon fill="none" stroke="#000000" points="478.348,0 478.348,-56 600.348,-56 600.348,0 478.348,0"/>
|
||||
<text text-anchor="start" x="487.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||
<text text-anchor="start" x="524.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A8->A12 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A8->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M465.2524,-184.5048C474.4734,-164.0387 484.8784,-140.9447 494.2007,-120.2539"/>
|
||||
<polygon fill="none" stroke="#000000" points="462.024,-183.1501 461.1072,-193.7052 468.4062,-186.0256 462.024,-183.1501"/>
|
||||
<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="475.09,-225.3526 474.9391,-235.9464 481.6634,-227.759 475.09,-225.3526"/>
|
||||
</g>
|
||||
<!-- A9->A9 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A9->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M238.6951,-272.2739C249.2923,-267.1987 256.348,-257.1074 256.348,-242 256.348,-231.3776 252.8598,-223.2351 247.1049,-217.5725"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="238.6951,-211.7261 249.4746,-213.7393 242.8005,-214.5802 246.9059,-217.4342 246.9059,-217.4342 246.9059,-217.4342 242.8005,-214.5802 244.3373,-221.1291 238.6951,-211.7261 238.6951,-211.7261"/>
|
||||
<text text-anchor="middle" x="258.4028" y="-212.1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="250.5654" y="-253.1774" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<path fill="none" stroke="#000000" d="M220.6951,-321.2601C231.2923,-315.0138 238.348,-302.5938 238.348,-284 238.348,-270.6357 234.703,-260.4608 228.7179,-253.4753"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="220.6951,-246.7399 231.2473,-249.7232 224.5245,-249.9548 228.3539,-253.1697 228.3539,-253.1697 228.3539,-253.1697 224.5245,-249.9548 225.4605,-256.6162 220.6951,-246.7399 220.6951,-246.7399"/>
|
||||
<text text-anchor="middle" x="240.0123" y="-248.9211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
<text text-anchor="middle" x="231.039" y="-301.1428" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
@@ -259,118 +267,118 @@
|
||||
<!-- A9->A13 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A9->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M207.2144,-185.8836C224.5887,-165.0802 244.3566,-141.4107 262.0261,-120.2539"/>
|
||||
<polygon fill="none" stroke="#000000" points="204.406,-183.7863 200.6821,-193.7052 209.7787,-188.2734 204.406,-183.7863"/>
|
||||
<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="182.0747,-225.8647 178.8173,-235.9464 187.6507,-230.0965 182.0747,-225.8647"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-578 236.348,-610 352.348,-610 352.348,-578 236.348,-578"/>
|
||||
<text text-anchor="start" x="264.622" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-498 236.348,-578 352.348,-578 352.348,-498 236.348,-498"/>
|
||||
<text text-anchor="start" x="279.901" y="-559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="282.131" y="-547" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="284.345" y="-535" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="279.901" y="-523" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="280.456" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-370 236.348,-498 352.348,-498 352.348,-370 236.348,-370"/>
|
||||
<text text-anchor="start" x="246.0055" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="248.226" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="266.002" y="-455" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="282.13" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="279.3505" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="259.6185" y="-407" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="264.628" y="-395" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</text>
|
||||
<text text-anchor="start" x="252.955" y="-383" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-662 236.348,-694 352.348,-694 352.348,-662 236.348,-662"/>
|
||||
<text text-anchor="start" x="264.622" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-582 236.348,-662 352.348,-662 352.348,-582 236.348,-582"/>
|
||||
<text text-anchor="start" x="279.901" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="282.131" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="284.345" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="279.901" y="-607" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="280.456" y="-595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="236.348,-454 236.348,-582 352.348,-582 352.348,-454 236.348,-454"/>
|
||||
<text text-anchor="start" x="246.0055" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="248.226" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="266.002" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="282.13" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="279.3505" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="259.6185" y="-491" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="264.628" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</text>
|
||||
<text text-anchor="start" x="252.955" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A10->A8 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M357.4808,-370.672C358.7731,-368.4252 360.063,-366.1993 361.348,-364 375.806,-339.2552 392.855,-312.3558 407.3203,-290.1276"/>
|
||||
<polygon fill="none" stroke="#000000" points="354.4004,-369.0085 352.4898,-379.4297 360.4821,-372.4746 354.4004,-369.0085"/>
|
||||
<path fill="none" stroke="#000000" d="M357.3002,-455.2837C358.6569,-452.8318 360.0073,-450.4016 361.348,-448 383.2991,-408.6787 409.1348,-364.6637 428.4716,-332.1398"/>
|
||||
<polygon fill="none" stroke="#000000" points="354.1241,-453.7956 352.3659,-464.2436 360.2557,-457.1724 354.1241,-453.7956"/>
|
||||
</g>
|
||||
<!-- A10->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A10->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M231.527,-371.7451C230.1228,-369.1386 228.7284,-366.5542 227.348,-364 214.1359,-339.5527 199.2736,-312.4504 186.9099,-290.012"/>
|
||||
<polygon fill="none" stroke="#000000" points="228.5137,-373.5316 236.3333,-380.6803 234.6785,-370.2155 228.5137,-373.5316"/>
|
||||
<path fill="none" stroke="#000000" d="M231.478,-454.0506C209.0706,-411.2997 185.0929,-365.5527 167.6574,-332.2876"/>
|
||||
<polygon fill="none" stroke="#000000" points="228.4903,-455.8898 236.2327,-463.1221 234.6903,-452.6401 228.4903,-455.8898"/>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M567.1644,-160.4648C560.9703,-146.8826 554.6465,-133.016 548.7531,-120.0931"/>
|
||||
<polygon fill="none" stroke="#000000" points="564.0911,-162.1611 571.425,-169.8074 570.4601,-159.2566 564.0911,-162.1611"/>
|
||||
<path fill="none" stroke="#000000" d="M592.1173,-202.4136C582.0634,-175.28 571.0546,-145.5697 561.7056,-120.3387"/>
|
||||
<polygon fill="none" stroke="#000000" points="588.8729,-203.7311 595.6294,-211.892 595.4368,-201.2989 588.8729,-203.7311"/>
|
||||
</g>
|
||||
<!-- A11->A13 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A11->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M542.4867,-170.8745C542.1078,-170.5805 541.7282,-170.2889 541.348,-170 513.3438,-148.7162 431.3406,-111.2534 373.497,-86.0342"/>
|
||||
<polygon fill="none" stroke="#000000" points="540.4079,-173.6974 550.3407,-177.3838 544.8747,-168.3078 540.4079,-173.6974"/>
|
||||
<path fill="none" stroke="#000000" d="M586.2753,-202.8933C578.5712,-190.8884 569.6045,-179.4114 559.348,-170 530.8998,-143.8959 437.024,-105.9199 373.5518,-82.1078"/>
|
||||
<polygon fill="none" stroke="#000000" points="583.4606,-204.9994 591.6624,-211.7061 589.4331,-201.3485 583.4606,-204.9994"/>
|
||||
</g>
|
||||
<!-- A14 -->
|
||||
<g id="node15" class="node">
|
||||
<title>A14</title>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-1176 133.348,-1208 236.348,-1208 236.348,-1176 133.348,-1176"/>
|
||||
<text text-anchor="start" x="174.01" y="-1189" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-1120 133.348,-1176 236.348,-1176 236.348,-1120 133.348,-1120"/>
|
||||
<text text-anchor="start" x="176.7895" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="152.334" y="-1145" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="165.9515" y="-1133" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-980 133.348,-1120 236.348,-1120 236.348,-980 133.348,-980"/>
|
||||
<text text-anchor="start" x="160.6835" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="158.7325" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="155.6785" y="-1077" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="154.0085" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="152.058" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="167.061" y="-1041" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="161.2225" y="-1029" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="145.385" y="-1017" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="154.8335" y="-1005" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="143.1705" y="-993" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
<polygon fill="none" stroke="#000000" points="133.348,-1272 133.348,-1304 236.348,-1304 236.348,-1272 133.348,-1272"/>
|
||||
<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,-1216 133.348,-1272 236.348,-1272 236.348,-1216 133.348,-1216"/>
|
||||
<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="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</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,-1076 133.348,-1216 236.348,-1216 236.348,-1076 133.348,-1076"/>
|
||||
<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="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</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="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</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="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="161.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="145.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="154.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="143.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-814 386.348,-846 453.348,-846 453.348,-814 386.348,-814"/>
|
||||
<text text-anchor="start" x="402.341" y="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-794 386.348,-814 453.348,-814 453.348,-794 386.348,-794"/>
|
||||
<polygon fill="none" stroke="#000000" points="386.348,-750 386.348,-794 453.348,-794 453.348,-750 386.348,-750"/>
|
||||
<text text-anchor="start" x="396.232" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="404.016" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
<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="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<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,-840 386.348,-884 453.348,-884 453.348,-840 386.348,-840"/>
|
||||
<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="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A14->A15 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>A14->A15</title>
|
||||
<path fill="none" stroke="#000000" d="M242.8857,-990.9876C246.5464,-987.0913 250.3682,-983.4032 254.348,-980 298.2601,-942.4501 334.8682,-972.1855 374.348,-930 395.7725,-907.1072 407.0366,-873.5975 412.9375,-846.0704"/>
|
||||
<polygon fill="none" stroke="#000000" points="240.0515,-988.9088 236.0452,-998.717 245.2936,-993.548 240.0515,-988.9088"/>
|
||||
<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,-1084.9088 236.0452,-1094.717 245.2936,-1089.548 240.0515,-1084.9088"/>
|
||||
</g>
|
||||
<!-- A16 -->
|
||||
<g id="node17" class="node">
|
||||
<title>A16</title>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-814 142.348,-846 209.348,-846 209.348,-814 142.348,-814"/>
|
||||
<text text-anchor="start" x="155.0065" y="-827" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-794 142.348,-814 209.348,-814 209.348,-794 142.348,-794"/>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-750 142.348,-794 209.348,-794 209.348,-750 142.348,-750"/>
|
||||
<text text-anchor="start" x="152.232" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="160.016" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-904 142.348,-936 209.348,-936 209.348,-904 142.348,-904"/>
|
||||
<text text-anchor="start" x="155.0065" y="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-884 142.348,-904 209.348,-904 209.348,-884 142.348,-884"/>
|
||||
<polygon fill="none" stroke="#000000" points="142.348,-840 142.348,-884 209.348,-884 209.348,-840 142.348,-840"/>
|
||||
<text text-anchor="start" x="152.232" y="-865" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="160.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A14->A16 -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>A14->A16</title>
|
||||
<path fill="none" stroke="#000000" d="M180.5733,-969.853C179.2476,-926.2551 177.8358,-879.821 176.8143,-846.2247"/>
|
||||
<polygon fill="none" stroke="#000000" points="177.0788,-970.0931 180.8812,-979.9821 184.0756,-969.8803 177.0788,-970.0931"/>
|
||||
<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.1491,-1065.9355 180.9455,-1075.8267 184.146,-1065.7269 177.1491,-1065.9355"/>
|
||||
</g>
|
||||
<!-- A15->A6 -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>A15->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M420.598,-749.875C421.4623,-716.5989 422.6586,-670.54 423.8044,-626.4296"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="424.071,-616.1641 428.3097,-626.2776 423.9411,-621.1624 423.8113,-626.1607 423.8113,-626.1607 423.8113,-626.1607 423.9411,-621.1624 419.3128,-626.0438 424.071,-616.1641 424.071,-616.1641"/>
|
||||
<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.1376,-700.0098 428.3813,-710.1212 424.0102,-705.0082 423.8828,-710.0066 423.8828,-710.0066 423.8828,-710.0066 424.0102,-705.0082 419.3842,-709.8919 424.1376,-700.0098 424.1376,-700.0098"/>
|
||||
</g>
|
||||
<!-- A16->A7 -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>A16->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M174.8793,-749.875C174.4651,-707.3571 173.8477,-643.9701 173.3263,-590.4435"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="173.2268,-580.2253 177.8241,-590.181 173.2756,-585.2251 173.3243,-590.2249 173.3243,-590.2249 173.3243,-590.2249 173.2756,-585.2251 168.8245,-590.2687 173.2268,-580.2253 173.2268,-580.2253"/>
|
||||
<path fill="none" stroke="#000000" d="M174.8891,-839.9684C174.4696,-796.0581 173.8357,-729.7079 173.3059,-674.2644"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="173.2083,-664.0467 177.8037,-674.0032 173.2561,-669.0465 173.304,-674.0463 173.304,-674.0463 173.304,-674.0463 173.2561,-669.0465 168.8042,-674.0893 173.2083,-664.0467 173.2083,-664.0467"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
@@ -4,8 +4,8 @@
|
||||
|
||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
||||
[Modbus|err;retry_cnt|build_msg();recv_req();recv_resp()]
|
||||
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
||||
[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;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]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
|
||||
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
aiomqtt==2.0.1
|
||||
schema==0.7.5
|
||||
aiocron==1.8
|
||||
aiocron==1.8
|
||||
aiohttp==3.9.5
|
||||
@@ -1,42 +1,77 @@
|
||||
import asyncio
|
||||
import logging
|
||||
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')
|
||||
|
||||
|
||||
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__')
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.addr = addr
|
||||
self.r_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)'''
|
||||
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')
|
||||
await self.loop()
|
||||
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
|
||||
# the connection to te TSUN cloud
|
||||
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()
|
||||
try:
|
||||
await self._async_publ_mqtt_proxy_stat('proxy')
|
||||
except Exception:
|
||||
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)'''
|
||||
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
|
||||
# connection. Instead we erase the client connection stream,
|
||||
@@ -52,28 +87,45 @@ class AsyncStream():
|
||||
# than erase client connection
|
||||
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.l_addr = self.writer.get_extra_info('sockname')
|
||||
|
||||
self.proc_start = time.time()
|
||||
while True:
|
||||
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:
|
||||
await self.async_write()
|
||||
await self.__async_forward()
|
||||
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:
|
||||
logger.error(f'{error} for l{self.l_addr} | '
|
||||
logger.error(f'[{self.node_id}:{self.conn_no}] '
|
||||
f'{error} for l{self.l_addr} | '
|
||||
f'r{self.r_addr}')
|
||||
await self.disc()
|
||||
self.close()
|
||||
return self
|
||||
|
||||
except RuntimeError as error:
|
||||
logger.warning(f"{error} for {self.l_addr}")
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
f'{error} for {self.l_addr}')
|
||||
await self.disc()
|
||||
self.close()
|
||||
return self
|
||||
@@ -84,31 +136,8 @@ class AsyncStream():
|
||||
f"Exception for {self.addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def disc(self) -> None:
|
||||
if self.writer.is_closing():
|
||||
return
|
||||
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
|
||||
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:
|
||||
async def async_write(self, headline: str = 'Transmit to ') -> None:
|
||||
"""Async write handler to transmit the send_buffer"""
|
||||
if self._send_buffer:
|
||||
hex_dump_memory(logging.INFO, f'{headline}{self.addr}:',
|
||||
self._send_buffer, len(self._send_buffer))
|
||||
@@ -116,8 +145,57 @@ class AsyncStream():
|
||||
await self.writer.drain()
|
||||
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:
|
||||
if self._forward_buffer:
|
||||
"""forward handler transmits data over the remote connection"""
|
||||
if not self._forward_buffer:
|
||||
return
|
||||
try:
|
||||
if not self.remoteStream:
|
||||
await self.async_create_remote()
|
||||
if self.remoteStream:
|
||||
@@ -134,6 +212,30 @@ class AsyncStream():
|
||||
await self.remoteStream.writer.drain()
|
||||
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):
|
||||
logger.debug(
|
||||
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
||||
|
||||
@@ -84,7 +84,7 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def class_init(cls): # pragma: no cover
|
||||
def class_init(cls) -> None | str: # pragma: no cover
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
@@ -94,7 +94,9 @@ class Config():
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
cls.read()
|
||||
err_str = cls.read()
|
||||
del cls.conf_schema
|
||||
return err_str
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict: # pragma: no cover
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
# import gc
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from async_stream import AsyncStream
|
||||
from gen3.talent import Talent
|
||||
|
||||
@@ -8,12 +9,13 @@ logger = logging.getLogger('conn')
|
||||
|
||||
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:
|
||||
AsyncStream.__init__(self, reader, writer, addr)
|
||||
Talent.__init__(self, server_side, id_str)
|
||||
|
||||
self.remoteStream = remote_stream
|
||||
self.remoteStream: 'ConnectionG3' = remote_stream
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
@@ -29,6 +31,10 @@ class ConnectionG3(AsyncStream, Talent):
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
pass
|
||||
|
||||
def healthy(self) -> bool:
|
||||
logger.debug('ConnectionG3 healthy()')
|
||||
return AsyncStream.healthy(self)
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
|
||||
@@ -132,11 +132,24 @@ class InfosG3(Infos):
|
||||
errors='replace')
|
||||
ind += str_len+1
|
||||
|
||||
elif data_type == 0x00: # 'Nul' -> end
|
||||
i = elms # abort the loop
|
||||
|
||||
elif data_type == 0x41: # 'A' -> Nop ??
|
||||
# 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
|
||||
result = struct.unpack_from('!l', buf, ind)[0]
|
||||
ind += 4
|
||||
|
||||
elif data_type == 0x53: # 'S' -> short
|
||||
elif data_type == 0x53: # 'S' -> short, int16
|
||||
result = struct.unpack_from('!h', buf, ind)[0]
|
||||
ind += 2
|
||||
|
||||
@@ -144,13 +157,14 @@ class InfosG3(Infos):
|
||||
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
||||
ind += 4
|
||||
|
||||
elif data_type == 0x4c: # 'L' -> int64
|
||||
elif data_type == 0x4c: # 'L' -> long, int64
|
||||
result = struct.unpack_from('!q', buf, ind)[0]
|
||||
ind += 8
|
||||
|
||||
else:
|
||||
self.inc_counter('Invalid_Data_Type')
|
||||
logging.error(f"Infos.parse: data_type: {data_type}"
|
||||
f" @0x{addr:04x} No:{i}"
|
||||
" not supported")
|
||||
return
|
||||
|
||||
@@ -164,7 +178,7 @@ class InfosG3(Infos):
|
||||
name = str(f'info-id.0x{addr:x}')
|
||||
|
||||
if update:
|
||||
self.tracer.log(level, f'[\'{node_id}\']GEN3: {name} :'
|
||||
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
||||
f' {result}{unit}')
|
||||
|
||||
i += 1
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from config import Config
|
||||
from inverter import Inverter
|
||||
from gen3.connection_g3 import ConnectionG3
|
||||
@@ -44,7 +45,7 @@ class InverterG3(Inverter, ConnectionG3):
|
||||
destroyed
|
||||
'''
|
||||
|
||||
def __init__(self, reader, writer, addr):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, addr):
|
||||
super().__init__(reader, writer, addr, None, True)
|
||||
self.__ha_restarts = -1
|
||||
|
||||
@@ -56,11 +57,14 @@ class InverterG3(Inverter, ConnectionG3):
|
||||
addr = (host, port)
|
||||
|
||||
try:
|
||||
logging.info(f'Connected to {addr}')
|
||||
logging.info(f'[{self.node_id}] Connect to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
||||
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))
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
@@ -121,7 +125,7 @@ class InverterG3(Inverter, ConnectionG3):
|
||||
def close(self) -> None:
|
||||
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
|
||||
super().close() # call close handler in the parent class
|
||||
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
||||
# logging.info(f'Inverter refs: {gc.get_referrers(self)}')
|
||||
|
||||
def __del__(self):
|
||||
logging.debug("InverterG3.__del__")
|
||||
|
||||
@@ -4,13 +4,15 @@ import time
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == "app.src.gen3.talent":
|
||||
from app.src.messages import hex_dump_memory, Message
|
||||
from app.src.messages import hex_dump_memory, Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.infos_g3 import InfosG3
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_memory, Message
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
|
||||
@@ -35,12 +37,16 @@ class Control:
|
||||
|
||||
|
||||
class Talent(Message):
|
||||
MB_START_TIMEOUT = 40
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
|
||||
def __init__(self, server_side: bool, id_str=b''):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
|
||||
self.await_conn_resp_cnt = 0
|
||||
self.id_str = id_str
|
||||
self.contact_name = b''
|
||||
self.contact_mail = b''
|
||||
self.ts_offset = 0 # time offset between tsun cloud and local
|
||||
self.db = InfosG3()
|
||||
self.switch = {
|
||||
0x00: self.msg_contact_info,
|
||||
@@ -63,18 +69,21 @@ class Talent(Message):
|
||||
0x04: logging.INFO,
|
||||
}
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
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
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = self.STATE_CLOSED
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
super().close()
|
||||
|
||||
def __set_serial_no(self, serial_no: str):
|
||||
@@ -103,7 +112,7 @@ class Talent(Message):
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
def read(self) -> None:
|
||||
def read(self) -> float:
|
||||
self._read()
|
||||
|
||||
if not self.header_valid:
|
||||
@@ -111,6 +120,9 @@ class Talent(Message):
|
||||
|
||||
if self.header_valid and len(self._recv_buffer) >= (self.header_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)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
@@ -121,7 +133,7 @@ class Talent(Message):
|
||||
self.__set_serial_no(self.id_str.decode("utf-8"))
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
return 0.5 # wait 500ms before sending a response
|
||||
|
||||
def forward(self, buffer, buflen) -> None:
|
||||
tsun = Config.get('tsun')
|
||||
@@ -138,7 +150,9 @@ class Talent(Message):
|
||||
return
|
||||
|
||||
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
|
||||
if self.state != self.STATE_UP:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
|
||||
self.__build_header(0x70, 0x77)
|
||||
@@ -152,11 +166,25 @@ class Talent(Message):
|
||||
self.writer.write(self._send_buffer)
|
||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.MB_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:
|
||||
contact_name = self.contact_name
|
||||
contact_mail = self.contact_mail
|
||||
@@ -198,6 +226,24 @@ class Talent(Message):
|
||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||
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
|
||||
# and set
|
||||
# self.header_len
|
||||
@@ -250,7 +296,8 @@ class Talent(Message):
|
||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||
if self.unique_id:
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||
f' Ctl: {int(self.ctrl):#02x} ({self.state}) '
|
||||
f'Msg: {fnc.__name__!r}')
|
||||
fnc()
|
||||
else:
|
||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||
@@ -299,39 +346,37 @@ class Talent(Message):
|
||||
return True
|
||||
|
||||
def msg_get_time(self):
|
||||
tsun = Config.get('tsun')
|
||||
if tsun['enabled']:
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len >= 8:
|
||||
ts = self._timestamp()
|
||||
result = struct.unpack_from('!q', self._recv_buffer,
|
||||
self.header_len)
|
||||
logger.debug(f'tsun-time: {result[0]:08x}'
|
||||
f' proxy-time: {ts:08x}')
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len == 0:
|
||||
self.state = State.pend # block MODBUS cmds
|
||||
self.mb_timer.start(self.MB_START_TIMEOUT)
|
||||
ts = self._timestamp()
|
||||
logger.debug(f'time: {ts:08x}')
|
||||
self.__build_header(0x91)
|
||||
self._send_buffer += struct.pack('!q', ts)
|
||||
self.__finish_send_msg()
|
||||
|
||||
elif self.data_len >= 8:
|
||||
ts = self._timestamp()
|
||||
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:
|
||||
if self.ctrl.is_ind():
|
||||
if self.data_len == 0:
|
||||
ts = self._timestamp()
|
||||
logger.debug(f'time: {ts:08x}')
|
||||
logger.warning('Unknown Ctrl')
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
|
||||
self.__build_header(0x91)
|
||||
self._send_buffer += struct.pack('!q', ts)
|
||||
self.__finish_send_msg()
|
||||
|
||||
else:
|
||||
logger.warning('Unknown Ctrl')
|
||||
self.inc_counter('Unknown_Ctrl')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
|
||||
def parse_msg_header(self):
|
||||
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
||||
|
||||
data_id = result[0] # len of complete message
|
||||
id_len = result[1] # len of variable id string
|
||||
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
|
||||
logger.debug(f'Data_ID: 0x{data_id:08x} id_len: {id_len}')
|
||||
|
||||
msg_hdr_len = 5+id_len+9
|
||||
|
||||
@@ -350,7 +395,6 @@ class Talent(Message):
|
||||
self._send_buffer += b'\x01'
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.state = self.STATE_UP
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
@@ -366,7 +410,7 @@ class Talent(Message):
|
||||
self._send_buffer += b'\x01'
|
||||
self.__finish_send_msg()
|
||||
self.__process_data()
|
||||
self.state = self.STATE_UP
|
||||
self.state = State.up # allow MODBUS cmds
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
@@ -420,13 +464,19 @@ class Talent(Message):
|
||||
|
||||
if self.ctrl.is_req():
|
||||
if self.remoteStream.mb.recv_req(data[hdr_len:],
|
||||
self.msg_forward):
|
||||
self.remoteStream.
|
||||
msg_forward):
|
||||
self.inc_counter('Modbus_Command')
|
||||
else:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
elif self.ctrl.is_ind():
|
||||
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
|
||||
self.modbus_elms = 0
|
||||
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
|
||||
if not self.server_side:
|
||||
logger.warning('Unknown Message')
|
||||
self.inc_counter('Unknown_Msg')
|
||||
return
|
||||
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||
hdr_len:],
|
||||
self.node_id):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
# import gc
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from async_stream import AsyncStream
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
|
||||
@@ -8,12 +9,13 @@ logger = logging.getLogger('conn')
|
||||
|
||||
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:
|
||||
AsyncStream.__init__(self, reader, writer, addr)
|
||||
SolarmanV5.__init__(self, server_side)
|
||||
|
||||
self.remoteStream = remote_stream
|
||||
self.remoteStream: 'ConnectionG3P' = remote_stream
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
@@ -29,6 +31,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
pass
|
||||
|
||||
def healthy(self) -> bool:
|
||||
logger.debug('ConnectionG3P healthy()')
|
||||
return AsyncStream.healthy(self)
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
|
||||
@@ -123,5 +123,5 @@ class InfosG3P(Infos):
|
||||
update = False
|
||||
|
||||
if update:
|
||||
self.tracer.log(level, f'[\'{node_id}\']GEN3PLUS: {name}'
|
||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from config import Config
|
||||
from inverter import Inverter
|
||||
from gen3plus.connection_g3p import ConnectionG3P
|
||||
@@ -44,7 +45,7 @@ class InverterG3P(Inverter, ConnectionG3P):
|
||||
destroyed
|
||||
'''
|
||||
|
||||
def __init__(self, reader, writer, addr):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, addr):
|
||||
super().__init__(reader, writer, addr, None, True)
|
||||
self.__ha_restarts = -1
|
||||
|
||||
@@ -56,11 +57,14 @@ class InverterG3P(Inverter, ConnectionG3P):
|
||||
addr = (host, port)
|
||||
|
||||
try:
|
||||
logging.info(f'Connected to {addr}')
|
||||
logging.info(f'[{self.node_id}] Connect to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
||||
False)
|
||||
logging.info(f'[{self.remoteStream.node_id}:'
|
||||
f'{self.remoteStream.conn_no}] '
|
||||
f'Connected to {addr}')
|
||||
asyncio.create_task(self.client_loop(addr))
|
||||
|
||||
except (ConnectionRefusedError, TimeoutError) as error:
|
||||
|
||||
@@ -6,15 +6,17 @@ import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||
from app.src.messages import hex_dump_memory, Message
|
||||
from app.src.messages import hex_dump_memory, Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_memory, Message
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from config import Config
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register
|
||||
# import traceback
|
||||
@@ -51,6 +53,8 @@ class Sequence():
|
||||
class SolarmanV5(Message):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
MB_START_TIMEOUT = 40
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
|
||||
def __init__(self, server_side: bool):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
|
||||
@@ -122,17 +126,21 @@ class SolarmanV5(Message):
|
||||
if 'at_acl' in g3p_cnf: # pragma: no cover
|
||||
self.at_acl = g3p_cnf['at_acl']
|
||||
|
||||
self.node_id = 'G3P' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
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
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = self.STATE_CLOSED
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
super().close()
|
||||
|
||||
def __set_serial_no(self, snr: int):
|
||||
@@ -166,7 +174,7 @@ class SolarmanV5(Message):
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
def read(self) -> None:
|
||||
def read(self) -> float:
|
||||
self._read()
|
||||
|
||||
if not self.header_valid:
|
||||
@@ -181,10 +189,13 @@ class SolarmanV5(Message):
|
||||
self._recv_buffer, self.header_len+self.data_len+2)
|
||||
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||
+ self.data_len + 2):
|
||||
if self.state == State.init:
|
||||
self.state = State.received
|
||||
|
||||
self.__set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
return 0 # wait 0s before sending a response
|
||||
|
||||
def forward(self, buffer, buflen) -> None:
|
||||
tsun = Config.get('solarman')
|
||||
@@ -250,6 +261,10 @@ class SolarmanV5(Message):
|
||||
self.snr = result[4]
|
||||
|
||||
if start != 0xA5:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid start byte from'
|
||||
f' {self.addr}:', buf, buf_len)
|
||||
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
# erase broken recv buffer
|
||||
self._recv_buffer = bytearray()
|
||||
@@ -261,6 +276,9 @@ class SolarmanV5(Message):
|
||||
crc = buf[self.data_len+11]
|
||||
stop = buf[self.data_len+12]
|
||||
if stop != 0x15:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid stop byte from '
|
||||
f'{self.addr}:', buf, buf_len)
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
if len(self._recv_buffer) > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
@@ -335,7 +353,9 @@ class SolarmanV5(Message):
|
||||
self.__finish_send_msg()
|
||||
|
||||
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||
if self.state != self.STATE_UP:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
self.__build_header(0x4510)
|
||||
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||
@@ -347,17 +367,33 @@ class SolarmanV5(Message):
|
||||
self.writer.write(self._send_buffer)
|
||||
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
|
||||
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
|
||||
|
||||
if 0 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
||||
|
||||
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||
cmd.startswith(tuple(self.at_acl[connection]['block']))
|
||||
|
||||
async def send_at_cmd(self, AT_cmd: str) -> None:
|
||||
if self.state != self.STATE_UP:
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
AT_cmd = AT_cmd.strip()
|
||||
|
||||
@@ -366,8 +402,7 @@ class SolarmanV5(Message):
|
||||
node_id = self.node_id
|
||||
key = 'at_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
asyncio.ensure_future(
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
|
||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
self.forward_at_cmd_resp = False
|
||||
@@ -456,7 +491,9 @@ class SolarmanV5(Message):
|
||||
self.__process_data(ftype)
|
||||
self.__forward_msg()
|
||||
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):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
@@ -486,16 +523,19 @@ class SolarmanV5(Message):
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
if self.remoteStream.mb.recv_req(data[15:],
|
||||
self.__forward_msg()):
|
||||
self.remoteStream.
|
||||
__forward_msg):
|
||||
self.inc_counter('Modbus_Command')
|
||||
else:
|
||||
logger.error('Invalid Modbus Msg')
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
return
|
||||
|
||||
self.__forward_msg()
|
||||
|
||||
async def publish_mqtt(self, key, data):
|
||||
await self.mqtt.publish(key, data) # pragma: no cover
|
||||
def publish_mqtt(self, key, data):
|
||||
asyncio.ensure_future(
|
||||
self.mqtt.publish(key, data))
|
||||
|
||||
def get_cmd_rsp_log_lvl(self) -> int:
|
||||
ftype = self._recv_buffer[self.header_len]
|
||||
@@ -519,8 +559,7 @@ class SolarmanV5(Message):
|
||||
node_id = self.node_id
|
||||
key = 'at_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
asyncio.ensure_future(
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
|
||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
valid = data[1]
|
||||
@@ -551,7 +590,9 @@ class SolarmanV5(Message):
|
||||
|
||||
self.__forward_msg()
|
||||
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):
|
||||
data = self._recv_buffer[self.header_len:]
|
||||
|
||||
@@ -343,7 +343,7 @@ class Infos:
|
||||
dict[counter] -= 1
|
||||
|
||||
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
||||
-> Generator[tuple[dict, str], None, None]:
|
||||
-> Generator[tuple[str, str, str, str], None, None]:
|
||||
'''Generator function yields json register struct for home-assistant
|
||||
auto configuration and the unique entity string, for all proxy
|
||||
registers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[loggers]
|
||||
keys=root,tracer,mesg,conn,data,mqtt
|
||||
keys=root,tracer,mesg,conn,data,mqtt,asyncio
|
||||
|
||||
[handlers]
|
||||
keys=console_handler,file_handler_name1,file_handler_name2
|
||||
@@ -24,6 +24,12 @@ handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=mqtt
|
||||
|
||||
[logger_asyncio]
|
||||
level=INFO
|
||||
handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=asyncio
|
||||
|
||||
[logger_data]
|
||||
level=DEBUG
|
||||
handlers=file_handler_name1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import weakref
|
||||
from typing import Callable
|
||||
from typing import Callable, Generator
|
||||
from enum import Enum
|
||||
|
||||
|
||||
if __name__ == "app.src.messages":
|
||||
@@ -45,21 +46,32 @@ def hex_dump_memory(level, info, data, num):
|
||||
|
||||
|
||||
class IterRegistry(type):
|
||||
def __iter__(cls):
|
||||
def __iter__(cls) -> Generator['Message', None, None]:
|
||||
for ref in cls._registry:
|
||||
obj = ref()
|
||||
if obj is not None:
|
||||
yield obj
|
||||
|
||||
|
||||
class State(Enum):
|
||||
'''state of the logical connection'''
|
||||
init = 0
|
||||
'''just created'''
|
||||
received = 1
|
||||
'''at least one packet received'''
|
||||
up = 2
|
||||
'''at least one cmd-rsp transaction'''
|
||||
pend = 3
|
||||
'''inverter transaction pending, don't send MODBUS cmds'''
|
||||
closed = 4
|
||||
'''connection closed'''
|
||||
|
||||
|
||||
class Message(metaclass=IterRegistry):
|
||||
_registry = []
|
||||
STATE_INIT = 0
|
||||
STATE_UP = 2
|
||||
STATE_CLOSED = 3
|
||||
|
||||
def __init__(self, server_side: bool, send_modbus_cb:
|
||||
Callable[[bytes, int, str], None], mb_timeout):
|
||||
Callable[[bytes, int, str], None], mb_timeout: int):
|
||||
self._registry.append(weakref.ref(self))
|
||||
|
||||
self.server_side = server_side
|
||||
@@ -72,13 +84,13 @@ class Message(metaclass=IterRegistry):
|
||||
self.header_len = 0
|
||||
self.data_len = 0
|
||||
self.unique_id = 0
|
||||
self.node_id = ''
|
||||
self.node_id = '' # will be overwritten in the child class's __init__
|
||||
self.sug_area = ''
|
||||
self._recv_buffer = bytearray(0)
|
||||
self._send_buffer = bytearray(0)
|
||||
self._forward_buffer = bytearray(0)
|
||||
self.new_data = {}
|
||||
self.state = self.STATE_INIT
|
||||
self.state = State.init
|
||||
|
||||
'''
|
||||
Empty methods, that have to be implemented in any child class which
|
||||
@@ -97,7 +109,7 @@ class Message(metaclass=IterRegistry):
|
||||
'''
|
||||
def close(self) -> None:
|
||||
if self.mb:
|
||||
del self.mb
|
||||
self.mb.close()
|
||||
self.mb = None
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
@@ -105,7 +105,17 @@ class Modbus():
|
||||
self.req_pend = False
|
||||
self.tim = None
|
||||
|
||||
def close(self):
|
||||
"""free the queue and erase the callback handlers"""
|
||||
logging.debug('Modbus close:')
|
||||
self.__stop_timer()
|
||||
self.rsp_handler = None
|
||||
self.snd_handler = None
|
||||
while not self.que.empty:
|
||||
self.que.get_nowait()
|
||||
|
||||
def __del__(self):
|
||||
"""log statistics on the deleting of a MODBUS instance"""
|
||||
logging.debug(f'Modbus __del__:\n {self.counter}')
|
||||
|
||||
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||
@@ -172,23 +182,25 @@ class Modbus():
|
||||
self.err = 5
|
||||
return
|
||||
if not self.__check_crc(buf):
|
||||
logger.error('Modbus resp: CRC error')
|
||||
logger.error(f'[{node_id}] Modbus resp: CRC error')
|
||||
self.err = 1
|
||||
return
|
||||
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
|
||||
return
|
||||
fcode = buf[1]
|
||||
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
|
||||
return
|
||||
if self.last_addr == self.INV_ADDR and \
|
||||
(fcode == 3 or fcode == 4):
|
||||
elmlen = buf[2] >> 1
|
||||
if elmlen != self.last_len:
|
||||
logger.info(f'Modbus: len error {elmlen} != {self.last_len}')
|
||||
logger.info(f'[{node_id}] Modbus: len error {elmlen}'
|
||||
f' != {self.last_len}')
|
||||
self.err = 4
|
||||
return
|
||||
first_reg = self.last_reg # save last_reg before sending next pdu
|
||||
@@ -216,7 +228,7 @@ class Modbus():
|
||||
yield keys[0], update, result
|
||||
if update:
|
||||
info_db.tracer.log(level,
|
||||
f'[\'{node_id}\']MODBUS: {name}'
|
||||
f'[{node_id}] MODBUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
else:
|
||||
self.__stop_timer()
|
||||
@@ -241,6 +253,7 @@ class Modbus():
|
||||
# logging.debug(f'Modbus stop timer {self}')
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = None
|
||||
|
||||
def __timeout_cb(self) -> None:
|
||||
'''Rsponse timeout handler retransmit pdu or send next pdu'''
|
||||
|
||||
@@ -148,10 +148,10 @@ class Mqtt(metaclass=Singleton):
|
||||
node_id = topic.split('/')[1] + '/'
|
||||
# refactor into a loop over a table
|
||||
payload = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info(f'InvCnf: {node_id}:{payload}')
|
||||
logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}')
|
||||
for m in Message:
|
||||
if m.server_side and (m.node_id == node_id):
|
||||
logger_mqtt.info(f'Found: {node_id}')
|
||||
logger_mqtt.debug(f'Found: {node_id}')
|
||||
fnc = getattr(m, "send_modbus_cmd", None)
|
||||
res = payload.split(',')
|
||||
if params != len(res):
|
||||
|
||||
35
app/src/my_timer.py
Normal file
35
app/src/my_timer.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from itertools import count
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, cb, id_str: str = ''):
|
||||
self.__timeout_cb = cb
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.tim = None
|
||||
self.id_str = id_str
|
||||
self.exp_count = count(0)
|
||||
|
||||
def start(self, timeout: float) -> None:
|
||||
'''Start timer with timeout seconds'''
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = self.loop.call_later(timeout, self.__timeout)
|
||||
logging.debug(f'[{self.id_str}]Start timer')
|
||||
|
||||
def stop(self) -> None:
|
||||
'''Stop timer'''
|
||||
logging.debug(f'[{self.id_str}]Stop timer')
|
||||
if self.tim:
|
||||
self.tim.cancel()
|
||||
self.tim = None
|
||||
|
||||
def __timeout(self) -> None:
|
||||
'''timer expired handler'''
|
||||
logging.debug(f'[{self.id_str}]Timer expired')
|
||||
self.__timeout_cb(next(self.exp_count))
|
||||
|
||||
def close(self) -> None:
|
||||
self.stop()
|
||||
self.__timeout_cb = None
|
||||
@@ -3,8 +3,6 @@ import json
|
||||
from mqtt import Mqtt
|
||||
from aiocron import crontab
|
||||
from infos import ClrAtMidnight
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
@@ -21,9 +19,6 @@ class Schedule:
|
||||
|
||||
crontab('0 0 * * *', func=cls.atmidnight, start=True)
|
||||
|
||||
# every minute
|
||||
crontab('* * * * *', func=cls.regular_modbus_cmds, start=True)
|
||||
|
||||
@classmethod
|
||||
async def atmidnight(cls) -> None:
|
||||
'''Clear daily counters at midnight'''
|
||||
@@ -33,15 +28,3 @@ class Schedule:
|
||||
logger_mqtt.debug(f'{key}: {data}')
|
||||
data_json = json.dumps(data)
|
||||
await cls.mqtt.publish(f"{key}", data_json)
|
||||
|
||||
@classmethod
|
||||
async def regular_modbus_cmds(cls):
|
||||
for m in Message:
|
||||
if m.server_side:
|
||||
fnc = getattr(m, "send_modbus_cmd", None)
|
||||
if callable(fnc):
|
||||
await fnc(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
|
||||
if 0 == (cls.count % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
await fnc(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
||||
cls.count += 1
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import ssl
|
||||
import signal
|
||||
import os
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
from messages import Message
|
||||
from inverter import Inverter
|
||||
@@ -10,22 +13,85 @@ from gen3plus.inverter_g3p import InverterG3P
|
||||
from scheduler import Schedule
|
||||
from config import Config
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
proxy_is_up = False
|
||||
|
||||
async def handle_client(reader, writer):
|
||||
|
||||
@routes.get('/')
|
||||
async def hello(request):
|
||||
return web.Response(text="Hello, world")
|
||||
|
||||
|
||||
@routes.get('/-/ready')
|
||||
async def ready(request):
|
||||
if proxy_is_up:
|
||||
status = 200
|
||||
text = 'Is ready'
|
||||
else:
|
||||
status = 503
|
||||
text = 'Not ready'
|
||||
return web.Response(status=status, text=text)
|
||||
|
||||
|
||||
@routes.get('/-/healthy')
|
||||
async def healthy(request):
|
||||
|
||||
if proxy_is_up:
|
||||
# logging.info('web reqeust healthy()')
|
||||
for stream in Message:
|
||||
try:
|
||||
res = stream.healthy()
|
||||
if not res:
|
||||
return web.Response(status=503, text="I have a problem")
|
||||
except Exception as err:
|
||||
logging.info(f'Exception:{err}')
|
||||
|
||||
return web.Response(status=200, text="I'm fine")
|
||||
|
||||
|
||||
async def webserver(addr, port):
|
||||
'''coro running our webserver'''
|
||||
app = web.Application()
|
||||
app.add_routes(routes)
|
||||
runner = web.AppRunner(app)
|
||||
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, addr, port)
|
||||
await site.start()
|
||||
logging.info(f'HTTP server listen on port: {port}')
|
||||
|
||||
try:
|
||||
# Normal interaction with aiohttp
|
||||
while True:
|
||||
await asyncio.sleep(3600) # sleep forever
|
||||
except asyncio.CancelledError:
|
||||
logging.info('HTTP server cancelled')
|
||||
await runner.cleanup()
|
||||
logging.debug('HTTP cleanup done')
|
||||
|
||||
|
||||
async def handle_client(reader: StreamReader, writer: StreamWriter):
|
||||
'''Handles a new incoming connection and starts an async loop'''
|
||||
|
||||
addr = writer.get_extra_info('peername')
|
||||
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'''
|
||||
|
||||
addr = writer.get_extra_info('peername')
|
||||
await InverterG3P(reader, writer, addr).server_loop(addr)
|
||||
|
||||
|
||||
async def handle_shutdown(loop):
|
||||
async def handle_client_v3(reader: StreamReader, writer: StreamWriter):
|
||||
'''Handles a new incoming connection and starts an async loop'''
|
||||
logging.info('Accept on port 10443')
|
||||
addr = writer.get_extra_info('peername')
|
||||
await InverterG3P(reader, writer, addr).server_loop(addr)
|
||||
|
||||
|
||||
async def handle_shutdown(loop, runner):
|
||||
'''Close all TCP connections and stop the event loop'''
|
||||
|
||||
logging.info('Shutdown due to SIGTERM')
|
||||
@@ -38,7 +104,7 @@ async def handle_shutdown(loop):
|
||||
await asyncio.wait_for(stream.disc(), 2)
|
||||
except Exception:
|
||||
pass
|
||||
logging.info('Disconnecting done')
|
||||
logging.info('Proxy disconnecting done')
|
||||
|
||||
#
|
||||
# second, close all open TCP connections
|
||||
@@ -46,13 +112,21 @@ async def handle_shutdown(loop):
|
||||
for stream in Message:
|
||||
stream.close()
|
||||
|
||||
await asyncio.sleep(0.1) # give time for closing
|
||||
logging.info('Proxy closing done')
|
||||
|
||||
#
|
||||
# third, cancel the web server
|
||||
#
|
||||
web_task.cancel()
|
||||
await web_task
|
||||
|
||||
#
|
||||
# at last, we stop the loop
|
||||
#
|
||||
logging.debug("Stop event loop")
|
||||
loop.stop()
|
||||
|
||||
logging.info('Shutdown complete')
|
||||
|
||||
|
||||
def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
@@ -84,17 +158,62 @@ if __name__ == "__main__":
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
logging.getLogger('data').setLevel(log_level)
|
||||
logging.getLogger('tracer').setLevel(log_level)
|
||||
logging.getLogger('asyncio').setLevel(log_level)
|
||||
# logging.getLogger('mqtt').setLevel(log_level)
|
||||
|
||||
# read config file
|
||||
Config.class_init()
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# read config file
|
||||
ConfigErr = Config.class_init()
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
Inverter.class_init()
|
||||
Schedule.start()
|
||||
|
||||
#
|
||||
# Create tasks for our listening servers. These must be tasks! If we call
|
||||
# start_server directly out of our main task, the eventloop will be blocked
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
||||
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
||||
|
||||
# https://crypto.stackexchange.com/questions/26591/tls-encryption-with-a-self-signed-pki-and-python-s-asyncio-module
|
||||
'''
|
||||
openssl genrsa -out -des3 ca.key.pem 2048
|
||||
openssl genrsa -out server.key.pem 2048
|
||||
openssl genrsa -out client.key.pem 2048
|
||||
|
||||
openssl req -x509 -new -nodes -key ca.key.pem -sha256 -days 365
|
||||
-out ca.cert.pem -subj /C=US/ST=CA/L=Somewhere/O=Someone/CN=FoobarCA
|
||||
|
||||
openssl req -new -sha256 -key server.key.pem
|
||||
-subj /C=US/ST=CA/L=Somewhere/O=Someone/CN=Foobar -out server.csr
|
||||
openssl x509 -req -in server.csr -CA ca.cert.pem -CAkey ca.key.pem
|
||||
-CAcreateserial -out server.cert.pem -days 365 -sha256
|
||||
|
||||
openssl req -new -sha256 -key client.key.pem
|
||||
-subj /C=US/ST=CA/L=Somewhere/O=Someone/CN=Foobar -out client.csr
|
||||
openssl x509 -req -in client.csr -CA ca.cert.pem -CAkey ca.key.pem
|
||||
-CAcreateserial -out client.cert.pem -days 365 -sha256
|
||||
'''
|
||||
|
||||
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
server_ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
server_ctx.maximum_version = ssl.TLSVersion.TLSv1_3
|
||||
server_ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
server_ctx.options |= ssl.OP_SINGLE_ECDH_USE
|
||||
server_ctx.options |= ssl.OP_NO_COMPRESSION
|
||||
server_ctx.load_cert_chain(certfile='cert/server.pem',
|
||||
keyfile='cert/server.key')
|
||||
server_ctx.load_verify_locations(cafile='cert/ca.pem')
|
||||
server_ctx.set_ciphers('ECDH+AESGCM')
|
||||
|
||||
loop.create_task(asyncio.start_server(handle_client_v3, '0.0.0.0', 10443,
|
||||
ssl=server_ctx))
|
||||
web_task = loop.create_task(webserver('0.0.0.0', 8127))
|
||||
|
||||
#
|
||||
# Register some UNIX Signal handler for a gracefully server shutdown
|
||||
# on Docker restart and stop
|
||||
@@ -102,22 +221,18 @@ if __name__ == "__main__":
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
lambda loop=loop: asyncio.create_task(
|
||||
handle_shutdown(loop)))
|
||||
|
||||
#
|
||||
# Create taska for our listening servera. These must be tasks! If we call
|
||||
# start_server directly out of our main task, the eventloop will be blocked
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
||||
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
||||
handle_shutdown(web_task)))
|
||||
|
||||
loop.set_debug(True)
|
||||
try:
|
||||
if ConfigErr is None:
|
||||
proxy_is_up = True
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
logging.info("Event loop is stopped")
|
||||
Inverter.class_close(loop)
|
||||
logging.info('Close event loop')
|
||||
logging.debug('Close event loop')
|
||||
loop.close()
|
||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||
|
||||
@@ -140,6 +140,82 @@ def InvDataSeq2(): # Data indication from the controller
|
||||
msg += b'\x53\x00\x00'
|
||||
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
|
||||
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'
|
||||
@@ -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['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):
|
||||
i = InfosG3()
|
||||
|
||||
@@ -5,9 +5,9 @@ from app.src.modbus import Modbus
|
||||
from app.src.infos import Infos, Register
|
||||
|
||||
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):
|
||||
super().__init__(self.send_cb)
|
||||
self.db = Infos()
|
||||
@@ -35,7 +35,7 @@ def test_modbus_crc():
|
||||
|
||||
def test_build_modbus_pdu():
|
||||
'''Check building and sending a MODBUS RTU'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,6,0x2000,0x12)
|
||||
assert mb.pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07'
|
||||
assert mb._Modbus__check_crc(mb.pdu)
|
||||
@@ -47,7 +47,7 @@ def test_build_modbus_pdu():
|
||||
|
||||
def test_recv_req():
|
||||
'''Receive a valid request, which must transmitted'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x07')
|
||||
assert mb.last_fcode == 6
|
||||
assert mb.last_reg == 0x2000
|
||||
@@ -56,7 +56,7 @@ def test_recv_req():
|
||||
|
||||
def test_recv_req_crc_err():
|
||||
'''Receive a request with invalid CRC, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert not mb.recv_req(b'\x01\x06\x20\x00\x00\x12\x02\x08')
|
||||
assert mb.pdu == None
|
||||
assert mb.last_fcode == 0
|
||||
@@ -66,7 +66,7 @@ def test_recv_req_crc_err():
|
||||
|
||||
def test_recv_resp_crc_err():
|
||||
'''Receive a response with invalid CRC, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
# simulate a transmitted request
|
||||
mb.req_pend = True
|
||||
mb.last_addr = 1
|
||||
@@ -86,7 +86,7 @@ def test_recv_resp_crc_err():
|
||||
|
||||
def test_recv_resp_invalid_addr():
|
||||
'''Receive a response with wrong server addr, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.req_pend = True
|
||||
# simulate a transmitted request
|
||||
mb.last_addr = 1
|
||||
@@ -109,7 +109,7 @@ def test_recv_resp_invalid_addr():
|
||||
|
||||
def test_recv_recv_fcode():
|
||||
'''Receive a response with wrong function code, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,4,0x300e,2)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -130,7 +130,7 @@ def test_recv_recv_fcode():
|
||||
|
||||
def test_recv_resp_len():
|
||||
'''Receive a response with wrong data length, which must be dropped'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x300e,3)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -152,7 +152,7 @@ def test_recv_resp_len():
|
||||
|
||||
def test_recv_unexpect_resp():
|
||||
'''Receive a response when we havb't sent a request'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert not mb.req_pend
|
||||
|
||||
# check unexpected response, which must be dropped
|
||||
@@ -167,7 +167,7 @@ def test_recv_unexpect_resp():
|
||||
|
||||
def test_parse_resp():
|
||||
'''Receive matching response and parse the values'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -191,7 +191,7 @@ def test_parse_resp():
|
||||
assert not mb.req_pend
|
||||
|
||||
def test_queue():
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3022,4)
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.req_pend
|
||||
@@ -210,7 +210,7 @@ def test_queue():
|
||||
|
||||
def test_queue2():
|
||||
'''Check queue handling for build_msg() calls'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.build_msg(1,3,0x3007,6)
|
||||
mb.build_msg(1,6,0x2008,4)
|
||||
assert mb.que.qsize() == 1
|
||||
@@ -258,7 +258,7 @@ def test_queue2():
|
||||
|
||||
def test_queue3():
|
||||
'''Check queue handling for recv_req() calls'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert mb.recv_req(b'\x01\x03\x30\x07\x00\x06{\t', mb.resp_handler)
|
||||
assert mb.recv_req(b'\x01\x06\x20\x08\x00\x04\x02\x0b', mb.resp_handler)
|
||||
assert mb.que.qsize() == 1
|
||||
@@ -315,7 +315,7 @@ def test_queue3():
|
||||
async def test_timeout():
|
||||
'''Test MODBUS response timeout and RTU retransmitting'''
|
||||
assert asyncio.get_running_loop()
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
mb.max_retries = 2
|
||||
mb.timeout = 0.1 # 100ms timeout for fast testing, expect a time resolution of at least 10ms
|
||||
assert asyncio.get_running_loop() == mb.loop
|
||||
@@ -363,7 +363,7 @@ async def test_timeout():
|
||||
|
||||
def test_recv_unknown_data():
|
||||
'''Receive a response with an unknwon register'''
|
||||
mb = TestHelper()
|
||||
mb = ModbusTestHelper()
|
||||
assert 0x9000 not in mb.map
|
||||
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -24,12 +25,24 @@ class Writer():
|
||||
def write(self, pdu: bytearray):
|
||||
self.sent_pdu = pdu
|
||||
|
||||
|
||||
class Mqtt():
|
||||
def __init__(self):
|
||||
self.key = ''
|
||||
self.data = ''
|
||||
|
||||
async def publish(self, key, data):
|
||||
self.key = key
|
||||
self.data = data
|
||||
|
||||
|
||||
class MemoryStream(SolarmanV5):
|
||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||
super().__init__(server_side)
|
||||
if server_side:
|
||||
self.mb.timeout = 1 # overwrite for faster testing
|
||||
self.writer = Writer()
|
||||
self.mqtt = Mqtt()
|
||||
self.__msg = msg
|
||||
self.__msg_len = len(msg)
|
||||
self.__chunks = chunks
|
||||
@@ -43,6 +56,8 @@ class MemoryStream(SolarmanV5):
|
||||
self.test_exception_async_write = False
|
||||
self.entity_prfx = ''
|
||||
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
|
||||
self.key = ''
|
||||
self.data = ''
|
||||
|
||||
def _timestamp(self):
|
||||
return timestamp
|
||||
@@ -54,6 +69,10 @@ class MemoryStream(SolarmanV5):
|
||||
self.__msg += msg
|
||||
self.__msg_len += len(msg)
|
||||
|
||||
def publish_mqtt(self, key, data):
|
||||
self.key = key
|
||||
self.data = data
|
||||
|
||||
def _read(self) -> int:
|
||||
copied_bytes = 0
|
||||
try:
|
||||
@@ -91,6 +110,9 @@ class MemoryStream(SolarmanV5):
|
||||
def get_sn() -> bytes:
|
||||
return b'\x21\x43\x65\x7b'
|
||||
|
||||
def get_sn_int() -> int:
|
||||
return 2070233889
|
||||
|
||||
def get_inv_no() -> bytes:
|
||||
return b'T170000000000001'
|
||||
|
||||
@@ -478,9 +500,10 @@ def AtCommandIndMsgBlock(): # 0x4510
|
||||
|
||||
@pytest.fixture
|
||||
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 += hb()
|
||||
msg += b'\x00\x00\x00\x00+ok'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
@@ -531,6 +554,15 @@ def MsgModbusCmd():
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgModbusCmdFwd():
|
||||
msg = b'\xa5\x17\x00\x10\x45\x01\x00' +get_sn() +b'\x02\xb0\x02'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
|
||||
msg += b'\x00\x00\x03\xc8'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgModbusCmdCrcErr():
|
||||
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
|
||||
@@ -820,8 +852,7 @@ def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
|
||||
assert m.data_len == 0xd4
|
||||
assert m.msg_count == 1
|
||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||
while m.read(): # read rest of message
|
||||
pass
|
||||
m.read() # read rest of message
|
||||
assert m.msg_count == 1
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
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()
|
||||
|
||||
@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
|
||||
m = MemoryStream(DeviceIndMsg, (0,), True)
|
||||
m.append_msg(InverterIndMsg)
|
||||
m.read()
|
||||
m.append_msg(AtCommandRspMsg)
|
||||
m.read() # read device ind
|
||||
assert m.control == 0x4110
|
||||
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._forward_buffer==DeviceIndMsg
|
||||
|
||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||
await m.send_at_cmd('AT+TIME=214028,1,60,120')
|
||||
assert m._recv_buffer==InverterIndMsg # unhandled next message
|
||||
assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
|
||||
assert m._send_buffer==b''
|
||||
assert m._forward_buffer==b''
|
||||
assert str(m.seq) == '01:01'
|
||||
assert m.mqtt.key == ''
|
||||
assert m.mqtt.data == ""
|
||||
|
||||
m.read()
|
||||
m.read() # read inverter ind
|
||||
assert m.control == 0x4210
|
||||
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._forward_buffer==InverterIndMsg
|
||||
|
||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||
m._forward_buffer = bytearray(0) # clear send buffer for next test
|
||||
await m.send_at_cmd('AT+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._forward_buffer==b''
|
||||
assert str(m.seq) == '02:03'
|
||||
assert m.mqtt.key == ''
|
||||
assert m.mqtt.data == ""
|
||||
|
||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||
m.read() # read at resp
|
||||
assert m.control == 0x1510
|
||||
assert str(m.seq) == '03:03'
|
||||
assert m._recv_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m._forward_buffer==b''
|
||||
assert m.key == 'at_resp'
|
||||
assert m.data == "+ok"
|
||||
|
||||
m.test_exception_async_write = True
|
||||
await m.send_at_cmd('AT+TIME=214028,1,60,120')
|
||||
assert m._recv_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m._forward_buffer==b''
|
||||
assert str(m.seq) == '02:04'
|
||||
assert str(m.seq) == '03:04'
|
||||
assert m.forward_at_cmd_resp == False
|
||||
assert m.mqtt.key == ''
|
||||
assert m.mqtt.data == ""
|
||||
m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1306,6 +1353,8 @@ async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, In
|
||||
assert m._send_buffer==b''
|
||||
assert m._forward_buffer==b''
|
||||
assert str(m.seq) == '01:01'
|
||||
assert m.mqtt.key == ''
|
||||
assert m.mqtt.data == ""
|
||||
|
||||
m.read()
|
||||
assert m.control == 0x4210
|
||||
@@ -1322,6 +1371,8 @@ async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, In
|
||||
assert m._forward_buffer==b''
|
||||
assert str(m.seq) == '02:02'
|
||||
assert m.forward_at_cmd_resp == False
|
||||
assert m.mqtt.key == 'at_resp'
|
||||
assert m.mqtt.data == "'AT+WEBU' is forbidden"
|
||||
m.close()
|
||||
|
||||
def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg):
|
||||
@@ -1386,7 +1437,7 @@ def test_msg_at_command_rsp1(ConfigTsunInv1, AtCommandRspMsg):
|
||||
assert m.control == 0x1510
|
||||
assert str(m.seq) == '03:03'
|
||||
assert m.header_len==11
|
||||
assert m.data_len==10
|
||||
assert m.data_len==17
|
||||
assert m._forward_buffer==AtCommandRspMsg
|
||||
assert m._send_buffer==b''
|
||||
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 str(m.seq) == '03:03'
|
||||
assert m.header_len==11
|
||||
assert m.data_len==10
|
||||
assert m.data_len==17
|
||||
assert m._forward_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||
m.close()
|
||||
|
||||
def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||
def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd, MsgModbusCmdFwd):
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(b'')
|
||||
m.snr = get_sn_int()
|
||||
m.state = State.up
|
||||
c = m.createClientStream(MsgModbusCmd)
|
||||
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
@@ -1428,8 +1481,9 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||
assert str(c.seq) == '03:02'
|
||||
assert c.header_len==11
|
||||
assert c.data_len==23
|
||||
assert c._forward_buffer==MsgModbusCmd
|
||||
assert c._forward_buffer==b''
|
||||
assert c._send_buffer==b''
|
||||
assert m.writer.sent_pdu == MsgModbusCmdFwd
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['AT_Command'] == 0
|
||||
assert m.db.stat['proxy']['Modbus_Command'] == 1
|
||||
@@ -1439,6 +1493,8 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||
def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(b'')
|
||||
m.snr = get_sn_int()
|
||||
m.state = State.up
|
||||
c = m.createClientStream(MsgModbusCmdCrcErr)
|
||||
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
@@ -1452,8 +1508,9 @@ def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
|
||||
assert str(c.seq) == '03:02'
|
||||
assert c.header_len==11
|
||||
assert c.data_len==23
|
||||
assert c._forward_buffer==MsgModbusCmdCrcErr
|
||||
assert c._forward_buffer==b''
|
||||
assert c._send_buffer==b''
|
||||
assert m.writer.sent_pdu==b''
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['AT_Command'] == 0
|
||||
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||
@@ -1643,21 +1700,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
|
||||
m1 = MemoryStream(MsgInverterInd, (0,))
|
||||
m2 = MemoryStream(MsgInverterInd, (0,))
|
||||
m3 = MemoryStream(MsgInverterInd, (0,))
|
||||
assert m1.state == m1.STATE_INIT
|
||||
assert m2.state == m2.STATE_INIT
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.init
|
||||
assert m2.state == m2.State.init
|
||||
assert m3.state == m3.State.init
|
||||
m1.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_INIT
|
||||
assert m2.state == m2.STATE_INIT
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.init
|
||||
assert m2.state == m2.State.init
|
||||
assert m3.state == m3.State.init
|
||||
m2.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_CLOSED
|
||||
assert m2.state == m2.STATE_INIT
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.closed
|
||||
assert m2.state == m2.State.init
|
||||
assert m3.state == m3.State.init
|
||||
m3.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_CLOSED
|
||||
assert m2.state == m2.STATE_CLOSED
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.closed
|
||||
assert m2.state == m2.State.closed
|
||||
assert m3.state == m3.State.init
|
||||
m1.close()
|
||||
m2.close()
|
||||
m3.close()
|
||||
|
||||
@@ -4,6 +4,7 @@ from app.src.gen3.talent import Talent, Control
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -60,7 +61,8 @@ class MemoryStream(Talent):
|
||||
return copied_bytes
|
||||
|
||||
def _timestamp(self):
|
||||
return 1700260990000
|
||||
# return 1700260990000
|
||||
return 1691246944000
|
||||
|
||||
def createClientStream(self, msg, chunks = (0,)):
|
||||
c = MemoryStream(msg, chunks, False)
|
||||
@@ -113,6 +115,10 @@ def MsgGetTime(): # Get Time Request message
|
||||
def MsgTimeResp(): # Get Time Resonse message
|
||||
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
|
||||
def MsgTimeInvalid(): # Get Time Request message
|
||||
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'
|
||||
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
|
||||
def MsgControllerAck(): # Get Time Request message
|
||||
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'
|
||||
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
|
||||
def MsgInverterAck(): # Get Time Request message
|
||||
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 m.msg_id==0
|
||||
assert m.msg_count == 1
|
||||
while m.read(): # read rest of message
|
||||
pass
|
||||
m.read() # read rest of message
|
||||
assert m.msg_count == 1
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
m.close()
|
||||
@@ -471,9 +574,10 @@ def test_msg_get_time(ConfigTsunInv1, MsgGetTime):
|
||||
assert int(m.ctrl)==145
|
||||
assert m.msg_id==34
|
||||
assert m.header_len==23
|
||||
assert m.ts_offset==0
|
||||
assert m.data_len==0
|
||||
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
|
||||
m.close()
|
||||
|
||||
@@ -489,9 +593,10 @@ def test_msg_get_time_autark(ConfigNoTsunInv1, MsgGetTime):
|
||||
assert int(m.ctrl)==145
|
||||
assert m.msg_id==34
|
||||
assert m.header_len==23
|
||||
assert m.ts_offset==0
|
||||
assert m.data_len==0
|
||||
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
|
||||
m.close()
|
||||
|
||||
@@ -507,8 +612,9 @@ def test_msg_time_resp(ConfigTsunInv1, MsgTimeResp):
|
||||
assert int(m.ctrl)==145
|
||||
assert m.msg_id==34
|
||||
assert m.header_len==23
|
||||
assert m.ts_offset==3600000
|
||||
assert m.data_len==8
|
||||
assert m._forward_buffer==MsgTimeResp
|
||||
assert m._forward_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
m.close()
|
||||
@@ -525,12 +631,32 @@ def test_msg_time_resp_autark(ConfigNoTsunInv1, MsgTimeResp):
|
||||
assert int(m.ctrl)==145
|
||||
assert m.msg_id==34
|
||||
assert m.header_len==23
|
||||
assert m.ts_offset==3600000
|
||||
assert m.data_len==8
|
||||
assert m._forward_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
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):
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(MsgTimeInvalid, (0,), False)
|
||||
@@ -543,6 +669,7 @@ def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
|
||||
assert int(m.ctrl)==148
|
||||
assert m.msg_id==34
|
||||
assert m.header_len==23
|
||||
assert m.ts_offset==0
|
||||
assert m.data_len==0
|
||||
assert m._forward_buffer==MsgTimeInvalid
|
||||
assert m._send_buffer==b''
|
||||
@@ -560,6 +687,7 @@ def test_msg_time_invalid_autark(ConfigNoTsunInv1, MsgTimeInvalid):
|
||||
assert m.unique_id == 'R170000000000001'
|
||||
assert int(m.ctrl)==148
|
||||
assert m.msg_id==34
|
||||
assert m.ts_offset==0
|
||||
assert m.header_len==23
|
||||
assert m.data_len==0
|
||||
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
|
||||
m.close()
|
||||
|
||||
def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerAck):
|
||||
def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerIndTsOffs, MsgControllerAck):
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(MsgControllerInd, (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.header_len==23
|
||||
assert m.data_len==284
|
||||
m.ts_offset = 0
|
||||
m._update_header(m._forward_buffer)
|
||||
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.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
m.close()
|
||||
@@ -616,12 +749,17 @@ def test_msg_cntrl_invalid(ConfigTsunInv1, MsgControllerInvalid):
|
||||
assert m.msg_id==113
|
||||
assert m.header_len==23
|
||||
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._send_buffer==b''
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||
m.close()
|
||||
|
||||
def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterAck):
|
||||
def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterIndTsOffs, MsgInverterAck):
|
||||
ConfigTsunInv1
|
||||
tracer.setLevel(logging.DEBUG)
|
||||
m = MemoryStream(MsgInverterInd, (0,))
|
||||
@@ -635,11 +773,62 @@ def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterAck):
|
||||
assert m.msg_id==4
|
||||
assert m.header_len==23
|
||||
assert m.data_len==120
|
||||
m.ts_offset = 0
|
||||
m._update_header(m._forward_buffer)
|
||||
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.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
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):
|
||||
ConfigTsunInv1
|
||||
tracer.setLevel(logging.ERROR)
|
||||
@@ -673,6 +862,11 @@ def test_msg_inv_invalid(ConfigTsunInv1, MsgInverterInvalid):
|
||||
assert m.msg_id==4
|
||||
assert m.header_len==23
|
||||
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._send_buffer==b''
|
||||
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.header_len==23
|
||||
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._send_buffer==b''
|
||||
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.header_len==23
|
||||
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._send_buffer==b''
|
||||
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.header_len==23
|
||||
assert m.data_len==1
|
||||
m.ts_offset = 0
|
||||
m._update_header(m._forward_buffer)
|
||||
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.db.stat['proxy']['Unknown_Ctrl'] == 1
|
||||
assert m.db.stat['proxy']['OTA_Start_Msg'] == 0
|
||||
@@ -847,7 +1056,7 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(b'')
|
||||
m.id_str = b"R170000000000001"
|
||||
m.state = m.STATE_UP
|
||||
m.state = State.up
|
||||
|
||||
c = m.createClientStream(MsgModbusCmd)
|
||||
|
||||
@@ -953,6 +1162,29 @@ def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
|
||||
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||
m.close()
|
||||
|
||||
def test_msg_modbus_cloud_rsp(ConfigTsunInv1, MsgModbusRsp):
|
||||
'''Modbus response from TSUN without a valid Modbus request must be dropped'''
|
||||
ConfigTsunInv1
|
||||
m = MemoryStream(MsgModbusRsp, (0,), False)
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||
m.db.stat['proxy']['Modbus_Command'] = 0
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert m.msg_count == 1
|
||||
assert m.id_str == b"R170000000000001"
|
||||
assert m.unique_id == 'R170000000000001'
|
||||
assert int(m.ctrl)==145
|
||||
assert m.msg_id==119
|
||||
assert m.header_len==23
|
||||
assert m.data_len==13
|
||||
assert m._forward_buffer==b''
|
||||
assert m._send_buffer==b''
|
||||
assert m.db.stat['proxy']['Unknown_Msg'] == 1
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['Modbus_Command'] == 0
|
||||
m.close()
|
||||
|
||||
def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20):
|
||||
'''Modbus response with a valid Modbus request must be forwarded'''
|
||||
ConfigTsunInv1
|
||||
@@ -1097,7 +1329,7 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
|
||||
assert m._send_buffer == 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)
|
||||
assert 0 == m.send_msg_ofs
|
||||
assert m._forward_buffer == b''
|
||||
@@ -1127,21 +1359,21 @@ def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
|
||||
m3 = MemoryStream(MsgInverterInd, (0,))
|
||||
assert MemoryStream._RefNo == 3 + start_val
|
||||
assert m3.RefNo == 3 + start_val
|
||||
assert m1.state == m1.STATE_INIT
|
||||
assert m2.state == m2.STATE_INIT
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.init
|
||||
assert m2.state == m2.State.init
|
||||
assert m3.state == m3.State.init
|
||||
m1.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_UP
|
||||
assert m2.state == m2.STATE_INIT
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.up
|
||||
assert m2.state == m2.State.init
|
||||
assert m3.state == m3.State.init
|
||||
m2.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_CLOSED
|
||||
assert m2.state == m2.STATE_UP
|
||||
assert m3.state == m3.STATE_INIT
|
||||
assert m1.state == m1.State.closed
|
||||
assert m2.state == m2.State.up
|
||||
assert m3.state == m3.State.init
|
||||
m3.read() # read complete msg, and set unique_id
|
||||
assert m1.state == m1.STATE_CLOSED
|
||||
assert m2.state == m2.STATE_CLOSED
|
||||
assert m3.state == m3.STATE_UP
|
||||
assert m1.state == m1.State.closed
|
||||
assert m2.state == m2.State.closed
|
||||
assert m3.state == m3.State.up
|
||||
m1.close()
|
||||
m2.close()
|
||||
m3.close()
|
||||
|
||||
2
app/tests/timestamp_old.svg
Normal file
2
app/tests/timestamp_old.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
26
app/tests/timestamp_old.yuml
Normal file
26
app/tests/timestamp_old.yuml
Normal file
@@ -0,0 +1,26 @@
|
||||
// {type:sequence}
|
||||
// {generate:true}
|
||||
|
||||
[Inverter]ContactInd>[Proxy]
|
||||
[Proxy]-[note: store Contact Info in proxy{bg:cornsilk}]
|
||||
[Proxy]ContactRsp (Ok).>[Inverter]
|
||||
|
||||
[Inverter]getTimeReq>[Proxy]
|
||||
[Proxy]ContactInd>[Cloud]
|
||||
[Cloud]ContactRsp (Ok).>[Proxy]
|
||||
[Proxy]getTimeReq>[Cloud]
|
||||
[Cloud]TimeRsp (time).>[Proxy]
|
||||
[Proxy]TimeRsp (time).>[Inverter]
|
||||
[Inverter]-[note: set clock in inverter{bg:cornsilk}]
|
||||
|
||||
[Inverter]DataInd (ts:=time)>[Proxy]
|
||||
[Proxy]DataRsp>[Inverter]
|
||||
[Proxy]DataInd (ts)>>[Cloud]
|
||||
[Proxy]DataInd>>[MQTT-Broker]
|
||||
[Cloud]DataRsp>>[Proxy]
|
||||
|
||||
[Inverter]DataInd (ts:=time)>[Proxy]
|
||||
[Proxy]DataRsp>[Inverter]
|
||||
[Proxy]DataInd (ts)>>[Cloud]
|
||||
[Proxy]DataInd>>[MQTT-Broker]
|
||||
[Cloud]DataRsp>>[Proxy]
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
####### H O M E - A S S I S T A N T #####
|
||||
home-assistant:
|
||||
@@ -34,7 +31,7 @@ services:
|
||||
ports:
|
||||
- 8123:8123
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./homeassistant/config:/config
|
||||
- ${PROJECT_DIR:-./}homeassistant/config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
healthcheck:
|
||||
test: curl --fail http://0.0.0.0:8123/auth/providers || exit 1
|
||||
@@ -56,13 +53,12 @@ services:
|
||||
expose:
|
||||
- 1883
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./mosquitto/config:/mosquitto/config
|
||||
- ${PROJECT_DIR}./mosquitto/data:/mosquitto/data
|
||||
- ${PROJECT_DIR:-./}mosquitto/config:/mosquitto/config
|
||||
- ${PROJECT_DIR:-./}mosquitto/data:/mosquitto/data
|
||||
networks:
|
||||
outside:
|
||||
ipv4_address: 172.28.1.5 # static IP required to receive mDNS traffic
|
||||
|
||||
- outside
|
||||
|
||||
|
||||
|
||||
####### T S U N - P R O X Y ######
|
||||
tsun-proxy:
|
||||
@@ -78,13 +74,18 @@ services:
|
||||
- GID=${GID:-1000}
|
||||
dns:
|
||||
- ${DNS1:-8.8.8.8}
|
||||
- $(DNS2:-4.4.4.4}
|
||||
- ${DNS2:-4.4.4.4}
|
||||
ports:
|
||||
- 5005:5005
|
||||
- 8127:8127
|
||||
- 10000:10000
|
||||
volumes:
|
||||
- ${PROJECT_DIR}./tsun-proxy/log:/home/tsun-proxy/log
|
||||
- ${PROJECT_DIR}./tsun-proxy/config:/home/tsun-proxy/config
|
||||
- ${PROJECT_DIR:-./}tsun-proxy/log:/home/tsun-proxy/log
|
||||
- ${PROJECT_DIR:-./}tsun-proxy/config:/home/tsun-proxy/config
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8127/-/healthy || exit 1
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
networks:
|
||||
- outside
|
||||
|
||||
@@ -94,11 +95,4 @@ services:
|
||||
networks:
|
||||
outside:
|
||||
name: home-assistant
|
||||
external: true
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.28.1.0/26
|
||||
ip_range: 172.28.1.32/27
|
||||
gateway: 172.28.1.62
|
||||
|
||||
@@ -89,6 +89,24 @@ def MsgDataResp(): # Contact Response message
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInvalidInfo(): # Contact Info message wrong start byte
|
||||
msg = b'\x47\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c'
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -155,4 +173,22 @@ def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
|
||||
# time.sleep(2.5)
|
||||
checkResponse(data, MsgDataResp)
|
||||
|
||||
def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgInvalidInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
checkResponse(data, MsgContactResp)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user