3
.cover_ghaction_rc
Normal file
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
@@ -1,3 +1,2 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
9
.env_example
Normal file
@@ -0,0 +1,9 @@
|
||||
# example file for the .env file. The .env set private values
|
||||
# which are needed for builing containers
|
||||
|
||||
# registry for debug an dev container
|
||||
PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
|
||||
|
||||
# registry for official container (preview, rc, rel)
|
||||
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
|
||||
PUBLIC_CR_KEY=
|
||||
6
.github/workflows/python-app.yml
vendored
@@ -31,7 +31,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
python -m pytest app ha_addon --cov=app/src --cov=ha_addon/rootfs/home --cov-report=xml
|
||||
python -m pytest app --cov=app/src --cov-config=.cover_ghaction_rc --cov-report=xml
|
||||
coverage report
|
||||
- name: Analyze with SonarCloud
|
||||
if: ${{ env.SONAR_TOKEN != 0 }}
|
||||
uses: SonarSource/sonarcloud-github-action@v3.1.0
|
||||
uses: SonarSource/sonarqube-scan-action@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
5
.gitignore
vendored
@@ -1,10 +1,11 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
.venv/**
|
||||
bin/**
|
||||
mosquitto/**
|
||||
homeassistant/**
|
||||
ha_addon/rootfs/home/proxy/*
|
||||
ha_addon/rootfs/requirements.txt
|
||||
ha_addons/ha_addon/rootfs/home/proxy/*
|
||||
ha_addons/ha_addon/rootfs/requirements.txt
|
||||
tsun_proxy/**
|
||||
Doku/**
|
||||
.DS_Store
|
||||
|
||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12.7
|
||||
24
.vscode/settings.json
vendored
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
"app/src",
|
||||
"app/tests",
|
||||
".venv/lib",
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"-vv",
|
||||
"app",
|
||||
"-vvv",
|
||||
"--cov=app/src",
|
||||
"--cov=ha_addon/rootfs/home",
|
||||
"--cov-report=xml",
|
||||
"--cov-report=html",
|
||||
"system_tests",
|
||||
"ha_addon"
|
||||
"app",
|
||||
"system_tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"flake8.args": [
|
||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
||||
"--extend-exclude=app/tests/*.py,system_tests/*.py"
|
||||
],
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "s-allius",
|
||||
@@ -20,5 +22,11 @@
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/*.pyi": true
|
||||
}
|
||||
},
|
||||
"python.analysis.typeEvaluation.deprecateTypingAliases": true,
|
||||
"python.autoComplete.extraPaths": [
|
||||
".venv/lib"
|
||||
],
|
||||
"coverage-gutters.coverageBaseDir": "tsun",
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
18
CHANGELOG.md
@@ -7,8 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
- make the configuration more flexible, add command line args to control this
|
||||
- fix the python path so we don't need special import paths for unit tests anymore
|
||||
- support test coverager in vscode
|
||||
- upgrade SonarQube action to version 4
|
||||
- update github action to Ubuntu 24-04
|
||||
- add initial support for home assistant add-ons from @mime24
|
||||
- github action: use ubuntu 24.04 and sonar-scanner-action 4 [#222](https://github.com/s-allius/tsun-gen3-proxy/issues/222)
|
||||
- migrate paho.mqtt CallbackAPIVersion to VERSION2 [#224](https://github.com/s-allius/tsun-gen3-proxy/issues/224)
|
||||
- add PROD_COMPL_TYPE to trace
|
||||
- add SolarmanV5 messages builder
|
||||
- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7)
|
||||
|
||||
## [0.11.1] - 2024-11-20
|
||||
|
||||
- fix pytest setup that can be startet from the rootdir
|
||||
- support python venv environment
|
||||
- add pytest.ini
|
||||
- move common settings from .vscode/settings.json into pytest.ini
|
||||
- add missing requirements
|
||||
- fix import paths for pytests
|
||||
- Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
|
||||
|
||||
## [0.11.0] - 2024-10-13
|
||||
|
||||
7
Makefile
@@ -1,7 +1,10 @@
|
||||
.PHONY: build clean
|
||||
.PHONY: build clean addon-dev addon-debug sddon-rc
|
||||
|
||||
# debug dev:
|
||||
# $(MAKE) -C app $@
|
||||
|
||||
clean build:
|
||||
$(MAKE) -C ha_addon $@
|
||||
$(MAKE) -C ha_addons/ha_addon $@
|
||||
|
||||
addon-dev addon-debug addon-rc:
|
||||
$(MAKE) -C ha_addons/ha_addon $(patsubst addon-%,%,$@)
|
||||
1
app/.version
Normal file
@@ -0,0 +1 @@
|
||||
0.12.0
|
||||
657
app/proxy_2.svg
@@ -4,429 +4,368 @@
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="468pt" height="1942pt"
|
||||
viewBox="0.00 0.00 468.35 1942.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1938)">
|
||||
<svg width="539pt" height="2000pt"
|
||||
viewBox="0.00 0.00 538.57 2000.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1996)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1938 464.348,-1938 464.348,4 -4,4"/>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1996 534.566,-1996 534.566,4 -4,4"/>
|
||||
<!-- A0 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1910 .1516,-1910 .1516,-1874 114.5444,-1874 114.5444,-1904 108.5444,-1910"/>
|
||||
<polyline fill="none" stroke="#000000" points="108.5444,-1910 108.5444,-1904 "/>
|
||||
<polyline fill="none" stroke="#000000" points="114.5444,-1904 108.5444,-1904 "/>
|
||||
<text text-anchor="middle" x="57.348" y="-1895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||
<text text-anchor="middle" x="57.348" y="-1883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="98.1981,-1972 -.0661,-1972 -.0661,-1928 104.1981,-1928 104.1981,-1966 98.1981,-1972"/>
|
||||
<polyline fill="none" stroke="#000000" points="98.1981,-1972 98.1981,-1966 "/>
|
||||
<polyline fill="none" stroke="#000000" points="104.1981,-1966 98.1981,-1966 "/>
|
||||
<text text-anchor="middle" x="52.066" y="-1959" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
|
||||
<text text-anchor="middle" x="52.066" y="-1947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
|
||||
<text text-anchor="middle" x="52.066" y="-1935" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3 inverter!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1902 132.348,-1934 248.348,-1934 248.348,-1902 132.348,-1902"/>
|
||||
<text text-anchor="start" x="141.997" y="-1915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1882 132.348,-1902 248.348,-1902 248.348,-1882 132.348,-1882"/>
|
||||
<polygon fill="none" stroke="#000000" points="132.348,-1850 132.348,-1882 248.348,-1882 248.348,-1850 132.348,-1850"/>
|
||||
<text text-anchor="start" x="168.958" y="-1863" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1960 122.066,-1992 238.066,-1992 238.066,-1960 122.066,-1960"/>
|
||||
<text text-anchor="start" x="131.715" y="-1973" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1940 122.066,-1960 238.066,-1960 238.066,-1940 122.066,-1940"/>
|
||||
<polygon fill="none" stroke="#000000" points="122.066,-1908 122.066,-1940 238.066,-1940 238.066,-1908 122.066,-1908"/>
|
||||
<text text-anchor="start" x="158.676" y="-1921" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
||||
</g>
|
||||
<!-- A14 -->
|
||||
<g id="node15" class="node">
|
||||
<title>A14</title>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1768 145.348,-1800 235.348,-1800 235.348,-1768 145.348,-1768"/>
|
||||
<text text-anchor="start" x="155.0545" y="-1781" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1736 145.348,-1768 235.348,-1768 235.348,-1736 145.348,-1736"/>
|
||||
<text text-anchor="start" x="171.1815" y="-1749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="145.348,-1704 145.348,-1736 235.348,-1736 235.348,-1704 145.348,-1704"/>
|
||||
<text text-anchor="start" x="175.3505" y="-1717" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1748 135.066,-1780 225.066,-1780 225.066,-1748 135.066,-1748"/>
|
||||
<text text-anchor="start" x="144.7725" y="-1761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1716 135.066,-1748 225.066,-1748 225.066,-1716 135.066,-1716"/>
|
||||
<text text-anchor="start" x="160.8995" y="-1729" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="135.066,-1684 135.066,-1716 225.066,-1716 225.066,-1684 135.066,-1684"/>
|
||||
<text text-anchor="start" x="165.0685" y="-1697" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A1->A14 -->
|
||||
<g id="edge19" class="edge">
|
||||
<g id="edge14" class="edge">
|
||||
<title>A1->A14</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1839.847C190.348,-1826.914 190.348,-1813.1101 190.348,-1800.3669"/>
|
||||
<polygon fill="none" stroke="#000000" points="186.8481,-1839.9953 190.348,-1849.9954 193.8481,-1839.9954 186.8481,-1839.9953"/>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M180.066,-1897.756C180.066,-1862.0883 180.066,-1815.1755 180.066,-1780.3644"/>
|
||||
<polygon fill="none" stroke="#000000" points="176.5661,-1897.9674 180.066,-1907.9674 183.5661,-1897.9674 176.5661,-1897.9674"/>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-584 362.348,-616 460.348,-616 460.348,-584 362.348,-584"/>
|
||||
<text text-anchor="start" x="387.7325" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-528 362.348,-584 460.348,-584 460.348,-528 362.348,-528"/>
|
||||
<text text-anchor="start" x="401.345" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="371.901" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="377.18" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="362.348,-472 362.348,-528 460.348,-528 460.348,-472 362.348,-472"/>
|
||||
<text text-anchor="start" x="375.79" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="396.3505" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,-100 29.348,-132 207.348,-132 207.348,-100 29.348,-100"/>
|
||||
<text text-anchor="start" x="73.8995" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,-68 29.348,-100 207.348,-100 207.348,-68 29.348,-68"/>
|
||||
<text text-anchor="start" x="86.119" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="29.348,0 29.348,-68 207.348,-68 207.348,0 29.348,0"/>
|
||||
<text text-anchor="start" x="70.0055" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="60.8365" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="39.157" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="103.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A2->A7 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A2->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M377.1669,-460.7015C372.2935,-447.8271 367.5247,-434.6178 363.348,-422 328.6206,-317.0888 364.6855,-270.3837 298.348,-182 272.7246,-147.8611 252.2817,-155.039 216.348,-132 216.2563,-131.9412 216.1645,-131.8823 216.0727,-131.8234"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="377.1669,-460.7018 383.0503,-464.8714 381.4643,-471.9059 375.5809,-467.7363 377.1669,-460.7018"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="207.433,-126.2441 218.2748,-127.8888 211.6333,-128.9566 215.8336,-131.6691 215.8336,-131.6691 215.8336,-131.6691 211.6333,-128.9566 213.3923,-135.4493 207.433,-126.2441 207.433,-126.2441"/>
|
||||
<text text-anchor="middle" x="367.0813" y="-455.0087" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-82 225.348,-114 363.348,-114 363.348,-82 225.348,-82"/>
|
||||
<text text-anchor="start" x="251.845" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-62 225.348,-82 363.348,-82 363.348,-62 225.348,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="225.348,-18 225.348,-62 363.348,-62 363.348,-18 225.348,-18"/>
|
||||
<text text-anchor="start" x="248.226" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="235.172" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A2->A8 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A2->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M410.2845,-471.9734C407.2471,-397.4776 396.9339,-278.7213 363.348,-182 356.28,-161.6455 345.3872,-140.9471 334.3293,-122.7781"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="328.9036,-114.0745 338.0125,-120.18 331.5487,-118.3175 334.1938,-122.5606 334.1938,-122.5606 334.1938,-122.5606 331.5487,-118.3175 330.375,-124.9412 328.9036,-114.0745 328.9036,-114.0745"/>
|
||||
<text text-anchor="middle" x="345.6654" y="-121.9851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-662 179.066,-694 277.066,-694 277.066,-662 179.066,-662"/>
|
||||
<text text-anchor="start" x="204.4505" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-606 179.066,-662 277.066,-662 277.066,-606 179.066,-606"/>
|
||||
<text text-anchor="start" x="218.063" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="188.619" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="193.898" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="179.066,-550 179.066,-606 277.066,-606 277.066,-550 179.066,-550"/>
|
||||
<text text-anchor="start" x="192.508" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="213.0685" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-342 40.348,-374 138.348,-374 138.348,-342 40.348,-342"/>
|
||||
<text text-anchor="start" x="62.398" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-286 40.348,-342 138.348,-342 138.348,-286 40.348,-286"/>
|
||||
<text text-anchor="start" x="79.345" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="49.901" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="55.18" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="40.348,-230 40.348,-286 138.348,-286 138.348,-230 40.348,-230"/>
|
||||
<text text-anchor="start" x="53.79" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="74.3505" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="400.4026,-320 303.7294,-320 303.7294,-284 400.4026,-284 400.4026,-320"/>
|
||||
<text text-anchor="middle" x="352.066" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A3->A7 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A3->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M99.7163,-217.6237C102.7418,-193.0021 106.0292,-166.2494 108.9885,-142.1671"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="99.6702,-217.9995 102.9085,-224.4425 98.2065,-229.9099 94.9682,-223.4668 99.6702,-217.9995"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="110.2349,-132.0235 113.4816,-142.4978 109.6251,-136.9862 109.0152,-141.9489 109.0152,-141.9489 109.0152,-141.9489 109.6251,-136.9862 104.5488,-141.4 110.2349,-132.0235 110.2349,-132.0235"/>
|
||||
<text text-anchor="middle" x="92.028" y="-207.8882" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M138.4873,-232.1115C151.0204,-215.3231 164.7944,-197.7122 178.348,-182 196.1632,-161.3474 216.8826,-139.9135 235.8403,-121.173"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="243.01,-114.1296 239.0299,-124.3477 239.4432,-117.6336 235.8763,-121.1375 235.8763,-121.1375 235.8763,-121.1375 239.4432,-117.6336 232.7227,-117.9274 243.01,-114.1296 243.01,-114.1296"/>
|
||||
<text text-anchor="middle" x="236.0028" y="-129.8619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
<!-- A2->A3 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A2->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M260.3657,-538.4062C268.7304,-516.7744 277.7293,-493.5168 286.066,-472 305.502,-421.8362 328.2143,-363.368 341.2906,-329.7205"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="260.2523,-538.6998 261.8194,-545.7386 255.9247,-549.8923 254.3577,-542.8536 260.2523,-538.6998"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="345.0251,-320.1117 345.5968,-331.0627 343.2138,-324.7721 341.4024,-329.4325 341.4024,-329.4325 341.4024,-329.4325 343.2138,-324.7721 337.2081,-327.8023 345.0251,-320.1117 345.0251,-320.1117"/>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-958 180.348,-990 297.348,-990 297.348,-958 180.348,-958"/>
|
||||
<text text-anchor="start" x="208.277" y="-971" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-938 180.348,-958 297.348,-958 297.348,-938 180.348,-938"/>
|
||||
<polygon fill="none" stroke="#000000" points="180.348,-666 180.348,-938 297.348,-938 297.348,-666 180.348,-666"/>
|
||||
<text text-anchor="start" x="208.284" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="206.614" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="220.5115" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="218.292" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="221.9015" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="218.0115" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="222.1815" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="218.017" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="222.1815" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="216.6225" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="218.2925" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="221.6265" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="217.7365" y="-751" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="221.9065" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="217.742" y="-727" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="221.9065" y="-715" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="213.847" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="190.2275" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
<polygon fill="none" stroke="#000000" points="285.4601,-320 178.6719,-320 178.6719,-284 285.4601,-284 285.4601,-320"/>
|
||||
<text text-anchor="middle" x="232.066" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="192.348,-574 192.348,-606 285.348,-606 285.348,-574 192.348,-574"/>
|
||||
<text text-anchor="start" x="210.512" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="192.348,-482 192.348,-574 285.348,-574 285.348,-482 192.348,-482"/>
|
||||
<text text-anchor="start" x="201.896" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="205.785" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="205.51" y="-531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="204.944" y="-519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="221.0615" y="-507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="214.3975" y="-495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A4->A5 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A4->A5</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M238.348,-655.339C238.348,-637.8929 238.348,-621.0952 238.348,-606.0686"/>
|
||||
<polygon fill="none" stroke="#000000" points="234.8481,-655.6774 238.348,-665.6775 241.8481,-655.6775 234.8481,-655.6774"/>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-390 187.348,-422 289.348,-422 289.348,-390 187.348,-390"/>
|
||||
<text text-anchor="start" x="208.622" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-310 187.348,-390 289.348,-390 289.348,-310 187.348,-310"/>
|
||||
<text text-anchor="start" x="223.901" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="226.131" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="228.345" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="223.901" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="224.456" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="187.348,-182 187.348,-310 289.348,-310 289.348,-182 187.348,-182"/>
|
||||
<text text-anchor="start" x="210.002" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="226.13" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="223.3505" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="218.902" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="203.6185" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="203.069" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="196.955" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<!-- A2->A4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" d="M238.348,-471.886C238.348,-456.1951 238.348,-439.1858 238.348,-422.1976"/>
|
||||
<polygon fill="none" stroke="#000000" points="234.8481,-471.9932 238.348,-481.9932 241.8481,-471.9933 234.8481,-471.9932"/>
|
||||
<title>A2->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M229.12,-537.6831C229.9778,-469.0527 231.1375,-376.283 231.7124,-330.2853"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="229.1188,-537.7877 233.0434,-543.8372 228.9687,-549.7868 225.044,-543.7372 229.1188,-537.7877"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="231.839,-320.1609 236.2135,-330.2164 231.7764,-325.1605 231.7139,-330.1601 231.7139,-330.1601 231.7139,-330.1601 231.7764,-325.1605 227.2143,-330.1038 231.839,-320.1609 231.839,-320.1609"/>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M182.6502,-192.461C172.1789,-171.8674 161.5319,-150.9284 151.9938,-132.1701"/>
|
||||
<polygon fill="none" stroke="#000000" points="179.6308,-194.2451 187.2832,-201.5725 185.8705,-191.0723 179.6308,-194.2451"/>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,-100 246.066,-132 424.066,-132 424.066,-100 246.066,-100"/>
|
||||
<text text-anchor="start" x="290.6175" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,-68 246.066,-100 424.066,-100 424.066,-68 246.066,-68"/>
|
||||
<text text-anchor="start" x="302.837" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="246.066,0 246.066,-68 424.066,-68 424.066,0 246.066,0"/>
|
||||
<text text-anchor="start" x="286.7235" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="277.5545" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="255.875" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="320.0685" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A6->A8 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M269.1746,-172.088C274.0746,-151.438 278.8579,-131.2796 282.9161,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="265.7436,-171.3879 266.8402,-181.9259 272.5545,-173.0041 265.7436,-171.3879"/>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M349.8809,-271.6651C347.5364,-239.1181 343.722,-186.1658 340.5509,-142.1431"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="349.898,-271.9044 354.3188,-277.6014 350.7603,-283.8733 346.3395,-278.1763 349.898,-271.9044"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="339.8226,-132.0321 345.0295,-141.6829 340.1818,-137.0192 340.5411,-142.0063 340.5411,-142.0063 340.5411,-142.0063 340.1818,-137.0192 336.0527,-142.3296 339.8226,-132.0321 339.8226,-132.0321"/>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1320 302.348,-1352 416.348,-1352 416.348,-1320 302.348,-1320"/>
|
||||
<text text-anchor="start" x="345.456" y="-1333" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1168 302.348,-1320 416.348,-1320 416.348,-1168 302.348,-1168"/>
|
||||
<text text-anchor="start" x="334.0665" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="340.171" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="349.345" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="312.111" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="347.1255" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="327.948" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="331.288" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="334.8925" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="333.232" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="345.46" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="302.348,-1040 302.348,-1168 416.348,-1168 416.348,-1040 302.348,-1040"/>
|
||||
<text text-anchor="start" x="316.8405" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="318.7805" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="324.6245" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="312.6765" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="314.6215" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="323.7885" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="339.902" y="-1065" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="344.3505" y="-1053" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-82 74.066,-114 212.066,-114 212.066,-82 74.066,-82"/>
|
||||
<text text-anchor="start" x="100.563" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-62 74.066,-82 212.066,-82 212.066,-62 74.066,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="74.066,-18 74.066,-62 212.066,-62 212.066,-18 74.066,-18"/>
|
||||
<text text-anchor="start" x="96.944" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="83.89" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A9->A2 -->
|
||||
<!-- A4->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A4->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M225.2301,-283.8733C212.4699,-250.0372 184.5329,-175.9573 164.7878,-123.5994"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="161.2018,-114.0904 168.941,-121.8593 162.9661,-118.7688 164.7305,-123.4472 164.7305,-123.4472 164.7305,-123.4472 162.9661,-118.7688 160.5199,-125.0351 161.2018,-114.0904 161.2018,-114.0904"/>
|
||||
<text text-anchor="middle" x="210.9254" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-1114 129.066,-1146 246.066,-1146 246.066,-1114 129.066,-1114"/>
|
||||
<text text-anchor="start" x="156.995" y="-1127" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-1094 129.066,-1114 246.066,-1114 246.066,-1094 129.066,-1094"/>
|
||||
<polygon fill="none" stroke="#000000" points="129.066,-822 129.066,-1094 246.066,-1094 246.066,-822 129.066,-822"/>
|
||||
<text text-anchor="start" x="157.002" y="-1075" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="155.332" y="-1063" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="169.2295" y="-1039" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="167.01" y="-1027" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="170.6195" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="166.7295" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="170.8995" y="-991" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="166.735" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="170.8995" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="165.3405" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="167.0105" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="170.3445" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="166.4545" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="170.6245" y="-895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="166.46" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="170.6245" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="162.565" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="138.9455" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="66.066,-652 66.066,-684 159.066,-684 159.066,-652 66.066,-652"/>
|
||||
<text text-anchor="start" x="84.23" y="-665" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="66.066,-560 66.066,-652 159.066,-652 159.066,-560 66.066,-560"/>
|
||||
<text text-anchor="start" x="75.614" y="-633" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="79.503" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="79.228" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="78.662" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="94.7795" y="-585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="88.1155" y="-573" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M151.3775,-811.7434C141.9017,-766.0069 132.2713,-719.5241 124.914,-684.013"/>
|
||||
<polygon fill="none" stroke="#000000" points="148.0039,-812.7126 153.4599,-821.7945 154.8583,-811.2924 148.0039,-812.7126"/>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-390 59.066,-422 161.066,-422 161.066,-390 59.066,-390"/>
|
||||
<text text-anchor="start" x="80.34" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-310 59.066,-390 161.066,-390 161.066,-310 59.066,-310"/>
|
||||
<text text-anchor="start" x="95.619" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="97.849" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="100.063" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="95.619" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="96.174" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="59.066,-182 59.066,-310 161.066,-310 161.066,-182 59.066,-182"/>
|
||||
<text text-anchor="start" x="81.72" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="97.848" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="95.0685" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="90.62" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="75.3365" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="74.787" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="68.673" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M111.6134,-549.5774C111.3784,-511.9877 111.0852,-465.0771 110.8174,-422.2295"/>
|
||||
<polygon fill="none" stroke="#000000" points="108.1155,-549.9435 111.678,-559.9214 115.1153,-549.8996 108.1155,-549.9435"/>
|
||||
</g>
|
||||
<!-- A7->A8 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A9->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M377.8061,-1029.6429C379.5037,-1016.2527 381.0593,-1002.9144 382.348,-990 395.4702,-858.5013 399.9848,-704.3277 404.1955,-616.0127"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="376.4823,-1039.8864 373.3012,-1029.3921 377.1232,-1034.9276 377.764,-1029.9689 377.764,-1029.9689 377.764,-1029.9689 377.1232,-1034.9276 382.2269,-1030.5457 376.4823,-1039.8864 376.4823,-1039.8864"/>
|
||||
<text text-anchor="middle" x="370.4228" y="-1017.8264" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
<title>A7->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M167.5272,-185.0204C168.3649,-184.0001 169.2111,-182.9929 170.066,-182 191.4283,-157.1889 219.1964,-135.0276 245.8416,-116.8901"/>
|
||||
<polygon fill="none" stroke="#000000" points="164.637,-183.0361 161.2751,-193.0834 170.1688,-187.3255 164.637,-183.0361"/>
|
||||
</g>
|
||||
<!-- A9->A2 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A9->A2</title>
|
||||
<path fill="none" stroke="#000000" d="M395.7566,-1029.6429C397.5037,-1016.2527 399.0593,-1002.9144 400.348,-990 412.8936,-864.2801 417.5714,-717.8344 416.9909,-628.0298"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="394.3845,-1039.8864 391.2521,-1029.3774 395.0484,-1034.9307 395.7122,-1029.9749 395.7122,-1029.9749 395.7122,-1029.9749 395.0484,-1034.9307 400.1724,-1030.5724 394.3845,-1039.8864 394.3845,-1039.8864"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="416.9908,-628.0122 412.9345,-622.0501 416.8778,-616.0127 420.9341,-621.9747 416.9908,-628.0122"/>
|
||||
<text text-anchor="middle" x="425.5004" y="-631.0585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
</g>
|
||||
<!-- A9->A4 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A9->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M308.0331,-1039.9349C303.6793,-1026.6936 299.2605,-1013.2547 294.8698,-999.9011"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="291.679,-990.1968 299.0774,-998.2908 293.2408,-994.9466 294.8026,-999.6964 294.8026,-999.6964 294.8026,-999.6964 293.2408,-994.9466 290.5277,-1001.102 291.679,-990.1968 291.679,-990.1968"/>
|
||||
<text text-anchor="middle" x="294.3419" y="-1022.3558" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-844 315.348,-876 382.348,-876 382.348,-844 315.348,-844"/>
|
||||
<text text-anchor="start" x="331.341" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-824 315.348,-844 382.348,-844 382.348,-824 315.348,-824"/>
|
||||
<polygon fill="none" stroke="#000000" points="315.348,-780 315.348,-824 382.348,-824 382.348,-780 315.348,-780"/>
|
||||
<text text-anchor="start" x="325.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="333.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A9->A12 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A9->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M354.683,-1039.9349C353.0582,-985.577 351.3338,-927.8888 350.095,-886.4463"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="349.7909,-876.272 354.5878,-886.1331 349.9404,-881.2698 350.0898,-886.2676 350.0898,-886.2676 350.0898,-886.2676 349.9404,-881.2698 345.5918,-886.4021 349.7909,-876.272 349.7909,-876.272"/>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M128.2709,-171.8077C131.1447,-151.2556 133.9487,-131.2022 136.3294,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="124.7747,-171.5375 126.856,-181.9259 131.7072,-172.5069 124.7747,-171.5375"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1284 72.348,-1316 163.348,-1316 163.348,-1284 72.348,-1284"/>
|
||||
<text text-anchor="start" x="90.343" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1144 72.348,-1284 163.348,-1284 163.348,-1144 72.348,-1144"/>
|
||||
<text text-anchor="start" x="92.5665" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="98.671" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="107.845" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="102.846" y="-1217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="105.9055" y="-1205" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="110.904" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="90.058" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="91.732" y="-1169" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="103.96" y="-1157" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="72.348,-1076 72.348,-1144 163.348,-1144 163.348,-1076 72.348,-1076"/>
|
||||
<text text-anchor="start" x="82.2885" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="98.402" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="102.8505" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-740 295.066,-772 409.066,-772 409.066,-740 295.066,-740"/>
|
||||
<text text-anchor="start" x="338.174" y="-753" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-600 295.066,-740 409.066,-740 409.066,-600 295.066,-600"/>
|
||||
<text text-anchor="start" x="332.889" y="-721" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="342.063" y="-709" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="304.829" y="-685" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||
<text text-anchor="start" x="339.8435" y="-673" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||
<text text-anchor="start" x="320.666" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||
<text text-anchor="start" x="324.006" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||
<text text-anchor="start" x="327.6105" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
|
||||
<text text-anchor="start" x="325.95" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="338.178" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="295.066,-472 295.066,-600 409.066,-600 409.066,-472 295.066,-472"/>
|
||||
<text text-anchor="start" x="309.5585" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||
<text text-anchor="start" x="311.4985" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||
<text text-anchor="start" x="317.3425" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||
<text text-anchor="start" x="305.3945" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||
<text text-anchor="start" x="307.3395" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||
<text text-anchor="start" x="316.5065" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="332.62" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="337.0685" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge9" class="edge">
|
||||
<g id="edge7" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M85.2254,-1066.0442C81.4271,-1040.8951 78.2542,-1014.6889 76.348,-990 59.0619,-766.107 69.322,-500.0139 79.5568,-374.449"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="86.7695,-1075.9973 80.7895,-1066.8055 86.0029,-1071.0565 85.2364,-1066.1156 85.2364,-1066.1156 85.2364,-1066.1156 86.0029,-1071.0565 89.6832,-1065.4256 86.7695,-1075.9973 86.7695,-1075.9973"/>
|
||||
<text text-anchor="middle" x="75.6382" y="-1056.3813" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M102.8817,-1066.0442C99.4271,-1040.8951 96.2542,-1014.6889 94.348,-990 77.6021,-773.1036 86.7076,-516.6035 90.1168,-386.6269"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="104.2701,-1075.9973 98.4317,-1066.7149 103.5793,-1071.0453 102.8885,-1066.0932 102.8885,-1066.0932 102.8885,-1066.0932 103.5793,-1071.0453 107.3454,-1065.4715 104.2701,-1075.9973 104.2701,-1075.9973"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="90.1213,-386.4451 86.2759,-380.3449 90.4278,-374.449 94.2733,-380.5493 90.1213,-386.4451"/>
|
||||
<text text-anchor="middle" x="98.4146" y="-389.7851" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local</text>
|
||||
<path fill="none" stroke="#000000" d="M352.066,-461.6172C352.066,-412.1611 352.066,-362.7538 352.066,-332.2961"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="352.066,-471.8382 347.5661,-461.8382 352.066,-466.8382 352.0661,-461.8382 352.0661,-461.8382 352.0661,-461.8382 352.066,-466.8382 356.5661,-461.8383 352.066,-471.8382 352.066,-471.8382"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="352.0661,-332.0807 348.066,-326.0808 352.066,-320.0807 356.066,-326.0807 352.0661,-332.0807"/>
|
||||
</g>
|
||||
<!-- A10->A4 -->
|
||||
<g id="edge17" class="edge">
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M156.8842,-1075.7577C164.8622,-1051.494 173.3952,-1025.5424 181.8248,-999.9053"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="185.0491,-990.0992 186.2003,-1001.0045 183.4873,-994.849 181.9255,-999.5989 181.9255,-999.5989 181.9255,-999.5989 183.4873,-994.849 177.6506,-998.1932 185.0491,-990.0992 185.0491,-990.0992"/>
|
||||
<text text-anchor="middle" x="154.5165" y="-1052.8983" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
<path fill="none" stroke="#000000" d="M292.1869,-462.3225C270.8082,-405.3126 249.4091,-348.2482 238.8463,-320.0807"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="295.7553,-471.8382 288.0306,-464.055 293.9997,-467.1566 292.244,-462.4749 292.244,-462.4749 292.244,-462.4749 293.9997,-467.1566 296.4575,-460.8948 295.7553,-471.8382 295.7553,-471.8382"/>
|
||||
<text text-anchor="middle" x="253.125" y="-331.0849" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-844 95.348,-876 162.348,-876 162.348,-844 95.348,-844"/>
|
||||
<text text-anchor="start" x="108.0065" y="-857" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-824 95.348,-844 162.348,-844 162.348,-824 95.348,-824"/>
|
||||
<polygon fill="none" stroke="#000000" points="95.348,-780 95.348,-824 162.348,-824 162.348,-780 95.348,-780"/>
|
||||
<text text-anchor="start" x="105.232" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="113.016" y="-793" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-318 432.066,-350 499.066,-350 499.066,-318 432.066,-318"/>
|
||||
<text text-anchor="start" x="448.059" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-298 432.066,-318 499.066,-318 499.066,-298 432.066,-298"/>
|
||||
<polygon fill="none" stroke="#000000" points="432.066,-254 432.066,-298 499.066,-298 499.066,-254 432.066,-254"/>
|
||||
<text text-anchor="start" x="441.95" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="449.734" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A10->A13 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>A10->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M120.9422,-1075.7577C122.842,-1012.1995 125.0881,-937.0598 126.6045,-886.3285"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="126.9072,-876.2026 131.1063,-886.3326 126.7577,-881.2004 126.6083,-886.1981 126.6083,-886.1981 126.6083,-886.1981 126.7577,-881.2004 122.1103,-886.0636 126.9072,-876.2026 126.9072,-876.2026"/>
|
||||
<!-- A10->A12 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A10->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M405.0919,-471.8382C419.1748,-431.9575 433.5466,-391.2585 444.6898,-359.7024"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="448.0405,-350.2137 448.9539,-361.1415 446.3756,-354.9284 444.7107,-359.6431 444.7107,-359.6431 444.7107,-359.6431 446.3756,-354.9284 440.4675,-358.1447 448.0405,-350.2137 448.0405,-350.2137"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1284 181.348,-1316 284.348,-1316 284.348,-1284 181.348,-1284"/>
|
||||
<text text-anchor="start" x="222.01" y="-1297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1228 181.348,-1284 284.348,-1284 284.348,-1228 181.348,-1228"/>
|
||||
<text text-anchor="start" x="224.7895" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="200.334" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="213.9515" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="181.348,-1076 181.348,-1228 284.348,-1228 284.348,-1076 181.348,-1076"/>
|
||||
<text text-anchor="start" x="208.6835" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="206.7325" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="203.6785" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="202.0085" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="200.058" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="215.061" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="207.842" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="209.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="193.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="202.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="191.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-710 428.066,-742 531.066,-742 531.066,-710 428.066,-710"/>
|
||||
<text text-anchor="start" x="468.728" y="-723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-654 428.066,-710 531.066,-710 531.066,-654 428.066,-654"/>
|
||||
<text text-anchor="start" x="471.5075" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="447.052" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="460.6695" y="-667" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.066,-502 428.066,-654 531.066,-654 531.066,-502 428.066,-502"/>
|
||||
<text text-anchor="start" x="455.4015" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="453.4505" y="-623" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="450.3965" y="-611" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="448.7265" y="-599" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="446.776" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="461.779" y="-575" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="454.56" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="455.9405" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="440.103" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="449.5515" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="437.8885" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge13" class="edge">
|
||||
<g id="edge11" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M280.9382,-1066.3836C289.808,-1041.1849 298.6736,-1014.8736 306.348,-990 318.0407,-952.103 329.205,-908.562 337.0802,-876.1703"/>
|
||||
<polygon fill="none" stroke="#000000" points="277.5783,-1065.3868 277.541,-1075.9815 284.1771,-1067.7225 277.5783,-1065.3868"/>
|
||||
<path fill="none" stroke="#000000" d="M473.3644,-491.6786C471.1803,-441.7544 468.8213,-387.8351 467.1788,-350.293"/>
|
||||
<polygon fill="none" stroke="#000000" points="469.8793,-492.0959 473.8131,-501.9334 476.8726,-491.7899 469.8793,-492.0959"/>
|
||||
</g>
|
||||
<!-- A11->A13 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A11->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M192.4636,-1066.2454C184.9411,-1041.0063 177.2684,-1014.7156 170.348,-990 159.6936,-951.9489 148.506,-908.6064 140.3587,-876.3336"/>
|
||||
<polygon fill="none" stroke="#000000" points="189.1205,-1067.2825 195.3374,-1075.8616 195.8274,-1065.2781 189.1205,-1067.2825"/>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1524 156.066,-1556 305.066,-1556 305.066,-1524 156.066,-1524"/>
|
||||
<text text-anchor="start" x="210.2835" y="-1537" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1300 156.066,-1524 305.066,-1524 305.066,-1300 156.066,-1300"/>
|
||||
<text text-anchor="start" x="193.8925" y="-1505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="204.45" y="-1493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="205.2845" y="-1481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="212.7795" y="-1469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="191.109" y="-1457" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="205.556" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
|
||||
<text text-anchor="start" x="211.39" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
|
||||
<text text-anchor="start" x="208.8905" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="202.781" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
|
||||
<text text-anchor="start" x="199.722" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
|
||||
<text text-anchor="start" x="206.666" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
|
||||
<text text-anchor="start" x="180.2705" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
|
||||
<text text-anchor="start" x="199.4505" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
|
||||
<text text-anchor="start" x="195.573" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
|
||||
<text text-anchor="start" x="204.451" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
|
||||
<text text-anchor="start" x="193.6185" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
|
||||
<text text-anchor="start" x="184.72" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
|
||||
<polygon fill="none" stroke="#000000" points="156.066,-1196 156.066,-1300 305.066,-1300 305.066,-1196 156.066,-1196"/>
|
||||
<text text-anchor="start" x="179.4505" y="-1281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
|
||||
<text text-anchor="start" x="208.066" y="-1269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
|
||||
<text text-anchor="start" x="180.8335" y="-1257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="165.8255" y="-1245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async> end_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="215.5685" y="-1233" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="201.3965" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="199.7265" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
</g>
|
||||
<!-- A13->A5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A13->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M210.2965,-1195.7758C208.8462,-1182.5547 207.3854,-1169.2373 205.9406,-1156.0662"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="204.8393,-1146.0268 210.403,-1155.4764 205.3846,-1150.997 205.9298,-1155.9672 205.9298,-1155.9672 205.9298,-1155.9672 205.3846,-1150.997 201.4567,-1156.4579 204.8393,-1146.0268 204.8393,-1146.0268"/>
|
||||
<text text-anchor="middle" x="199.9181" y="-1175.6794" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13->A10 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A13->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M260.8183,-1185.9405C281.556,-1057.7747 308.5382,-891.0162 327.7708,-772.1524"/>
|
||||
<polygon fill="none" stroke="#000000" points="257.3528,-1185.4467 259.2105,-1195.8774 264.2629,-1186.5648 257.3528,-1185.4467"/>
|
||||
</g>
|
||||
<!-- A14->A13 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A14->A13</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M188.2401,-1673.8004C192.8037,-1641.3079 198.7631,-1598.8764 204.747,-1556.2713"/>
|
||||
<polygon fill="none" stroke="#000000" points="184.7342,-1673.5986 186.8092,-1683.9883 191.6661,-1674.5723 184.7342,-1673.5986"/>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1550 150.348,-1582 231.348,-1582 231.348,-1550 150.348,-1550"/>
|
||||
<text text-anchor="start" x="170.5655" y="-1563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1518 150.348,-1550 231.348,-1550 231.348,-1518 150.348,-1518"/>
|
||||
<text text-anchor="start" x="173.0615" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<polygon fill="none" stroke="#000000" points="150.348,-1474 150.348,-1518 231.348,-1518 231.348,-1474 150.348,-1474"/>
|
||||
<text text-anchor="start" x="161.6785" y="-1499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="160.0085" y="-1487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1826 244.066,-1858 319.066,-1858 319.066,-1826 244.066,-1826"/>
|
||||
<text text-anchor="start" x="263.7835" y="-1839" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1674 244.066,-1826 319.066,-1826 319.066,-1674 244.066,-1674"/>
|
||||
<text text-anchor="start" x="273.2275" y="-1807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="254.056" y="-1783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="255.171" y="-1771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="265.1745" y="-1759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="255.4555" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="263.508" y="-1735" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="275.4575" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="262.1195" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="260.445" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="274.9025" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="244.066,-1606 244.066,-1674 319.066,-1674 319.066,-1606 244.066,-1606"/>
|
||||
<text text-anchor="start" x="255.456" y="-1655" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="258.79" y="-1643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="256.29" y="-1631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="266.5685" y="-1619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A14->A15 -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>A14->A15</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M190.348,-1693.5712C190.348,-1659.1613 190.348,-1615.9264 190.348,-1582.2666"/>
|
||||
<polygon fill="none" stroke="#000000" points="186.8481,-1693.9463 190.348,-1703.9464 193.8481,-1693.9464 186.8481,-1693.9463"/>
|
||||
</g>
|
||||
<!-- A15->A9 -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>A15->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M229.7336,-1465.2507C249.8317,-1432.1375 274.0435,-1390.492 293.348,-1352 296.3143,-1346.0855 299.269,-1340.0055 302.1912,-1333.8349"/>
|
||||
<polygon fill="none" stroke="#000000" points="226.6683,-1463.555 224.4506,-1473.9151 232.6449,-1467.1992 226.6683,-1463.555"/>
|
||||
</g>
|
||||
<!-- A15->A10 -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>A15->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M176.3052,-1464.1339C167.0994,-1422.2665 154.7762,-1366.2214 143.7769,-1316.197"/>
|
||||
<polygon fill="none" stroke="#000000" points="172.8909,-1464.9045 178.4568,-1473.9196 179.7276,-1463.4012 172.8909,-1464.9045"/>
|
||||
</g>
|
||||
<!-- A16 -->
|
||||
<g id="node17" class="node">
|
||||
<title>A16</title>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1622 285.348,-1654 360.348,-1654 360.348,-1622 285.348,-1622"/>
|
||||
<text text-anchor="start" x="305.0655" y="-1635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1470 285.348,-1622 360.348,-1622 360.348,-1470 285.348,-1470"/>
|
||||
<text text-anchor="start" x="314.5095" y="-1603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="295.338" y="-1579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="296.453" y="-1567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="306.4565" y="-1555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="296.7375" y="-1543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="304.79" y="-1531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="316.7395" y="-1519" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="303.4015" y="-1507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="301.727" y="-1495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="316.1845" y="-1483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="285.348,-1402 285.348,-1470 360.348,-1470 360.348,-1402 285.348,-1402"/>
|
||||
<text text-anchor="start" x="296.738" y="-1451" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="300.072" y="-1439" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="297.572" y="-1427" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="307.8505" y="-1415" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A16->A9 -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>A16->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M337.5861,-1391.2689C339.0212,-1378.3918 340.4833,-1365.272 341.9365,-1352.2329"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="336.4417,-1401.5379 333.077,-1391.101 336.9955,-1396.5687 337.5493,-1391.5995 337.5493,-1391.5995 337.5493,-1391.5995 336.9955,-1396.5687 342.0217,-1392.0979 336.4417,-1401.5379 336.4417,-1401.5379"/>
|
||||
<text text-anchor="middle" x="348.3292" y="-1368.1837" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="330.049" y="-1379.5871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
</g>
|
||||
<!-- A16->A10 -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>A16->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M279.1725,-1451.7757C267.6828,-1434.4941 254.4915,-1416.871 240.348,-1402 214.2483,-1374.5578 193.9188,-1382.411 171.348,-1352 163.2346,-1341.0684 156.273,-1328.8227 150.315,-1316.1269"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="284.6729,-1460.2184 275.4437,-1454.2962 281.9435,-1456.0291 279.2141,-1451.8397 279.2141,-1451.8397 279.2141,-1451.8397 281.9435,-1456.0291 282.9845,-1449.3833 284.6729,-1460.2184 284.6729,-1460.2184"/>
|
||||
<text text-anchor="middle" x="165.7688" y="-1325.8225" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="267.6964" y="-1446.645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
|
||||
<!-- A15->A13 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A15->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M261.5887,-1596.041C259.7128,-1582.9463 257.7908,-1569.5297 255.8664,-1556.0971"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="263.0135,-1605.9867 257.1408,-1596.726 262.3044,-1601.0373 261.5953,-1596.0878 261.5953,-1596.0878 261.5953,-1596.0878 262.3044,-1601.0373 266.0499,-1595.4496 263.0135,-1605.9867 263.0135,-1605.9867"/>
|
||||
<text text-anchor="middle" x="266.8039" y="-1569.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="252.0761" y="-1586.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 33 KiB |
@@ -2,11 +2,12 @@
|
||||
// {direction:topDown}
|
||||
// {generate:true}
|
||||
|
||||
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||
[note: Example of instantiation for a GEN3 inverter!{bg:cornsilk}]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[InverterG3|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3]++->[local:StreamPtr]
|
||||
[InverterG3]++->[remote:StreamPtr]
|
||||
|
||||
[<<AsyncIfc>>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()]
|
||||
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
|
||||
@@ -19,33 +20,24 @@
|
||||
[AsyncStream]^[AsyncStreamClient]
|
||||
|
||||
|
||||
[Talent|ifc:AsyncIfc;conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()]
|
||||
[Talent]<remote-[InverterG3]
|
||||
[InverterG3]-remote>[AsyncStreamClient]
|
||||
[Talent]<-local++[InverterG3]
|
||||
[InverterG3]++local->[AsyncStreamServer]
|
||||
|
||||
[SolarmanV5|ifc:AsyncIfc;conn_no;addr;;control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;healthy();close()]
|
||||
[SolarmanV5]<remote-[InverterG3P]
|
||||
[InverterG3P]-remote>[AsyncStreamClient]
|
||||
[SolarmanV5]<-local++[InverterG3P]
|
||||
[InverterG3P]++local->[AsyncStreamServer]
|
||||
[Talent|conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()]
|
||||
[Talent]<-++[local:StreamPtr]
|
||||
[local:StreamPtr]++->[AsyncStreamServer]
|
||||
[Talent]<-0..1[remote:StreamPtr]
|
||||
[remote:StreamPtr]0..1->[AsyncStreamClient]
|
||||
|
||||
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
|
||||
[Infos]^[InfosG3||ha_confs();parse()]
|
||||
[Infos]^[InfosG3P||ha_confs();parse()]
|
||||
|
||||
[Talent]use->[<<AsyncIfc>>]
|
||||
[Talent]->[InfosG3]
|
||||
[SolarmanV5]use->[<<AsyncIfc>>]
|
||||
[SolarmanV5]->[InfosG3P]
|
||||
|
||||
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
|
||||
[Message]use->[<<AsyncIfc>>]
|
||||
|
||||
[<<ProtocolIfc>>|_registry|close()]
|
||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||
[<<ProtocolIfc>>]^-.-[Message|node_id|inc_counter();dec_counter()]
|
||||
[<<ProtocolIfc>>]^-.-[Message]
|
||||
[Message]^[Talent]
|
||||
[Message]^[SolarmanV5]
|
||||
|
||||
[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
|
||||
[Modbus]<1-has[SolarmanV5]
|
||||
[Modbus]<1-has[Talent]
|
||||
[Modbus]<0..1-has[Message]
|
||||
|
||||
364
app/proxy_3.svg
Normal file
@@ -0,0 +1,364 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="539pt" height="1940pt"
|
||||
viewBox="0.00 0.00 538.62 1940.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1936)">
|
||||
<title>G</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1936 534.6165,-1936 534.6165,4 -4,4"/>
|
||||
<!-- A0 -->
|
||||
<g id="node1" class="node">
|
||||
<title>A0</title>
|
||||
<polygon fill="#fff8dc" stroke="#000000" points="114.3497,-1912 -.1167,-1912 -.1167,-1868 120.3497,-1868 120.3497,-1906 114.3497,-1912"/>
|
||||
<polyline fill="none" stroke="#000000" points="114.3497,-1912 114.3497,-1906 "/>
|
||||
<polyline fill="none" stroke="#000000" points="120.3497,-1906 114.3497,-1906 "/>
|
||||
<text text-anchor="middle" x="60.1165" y="-1899" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
|
||||
<text text-anchor="middle" x="60.1165" y="-1887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
|
||||
<text text-anchor="middle" x="60.1165" y="-1875" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3PLUS inverter!</text>
|
||||
</g>
|
||||
<!-- A1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>A1</title>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1900 138.1165,-1932 254.1165,-1932 254.1165,-1900 138.1165,-1900"/>
|
||||
<text text-anchor="start" x="147.7655" y="-1913" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AbstractIterMeta>></text>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1880 138.1165,-1900 254.1165,-1900 254.1165,-1880 138.1165,-1880"/>
|
||||
<polygon fill="none" stroke="#000000" points="138.1165,-1848 138.1165,-1880 254.1165,-1880 254.1165,-1848 138.1165,-1848"/>
|
||||
<text text-anchor="start" x="174.7265" y="-1861" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
|
||||
</g>
|
||||
<!-- A14 -->
|
||||
<g id="node15" class="node">
|
||||
<title>A14</title>
|
||||
<polygon fill="none" stroke="#000000" points="151.1165,-1688 151.1165,-1720 241.1165,-1720 241.1165,-1688 151.1165,-1688"/>
|
||||
<text text-anchor="start" x="160.823" y="-1701" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<ProtocolIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="151.1165,-1656 151.1165,-1688 241.1165,-1688 241.1165,-1656 151.1165,-1656"/>
|
||||
<text text-anchor="start" x="176.95" y="-1669" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
|
||||
<polygon fill="none" stroke="#000000" points="151.1165,-1624 151.1165,-1656 241.1165,-1656 241.1165,-1624 151.1165,-1624"/>
|
||||
<text text-anchor="start" x="181.119" y="-1637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A1->A14 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>A1->A14</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M196.1165,-1837.756C196.1165,-1802.0883 196.1165,-1755.1755 196.1165,-1720.3644"/>
|
||||
<polygon fill="none" stroke="#000000" points="192.6166,-1837.9674 196.1165,-1847.9674 199.6166,-1837.9674 192.6166,-1837.9674"/>
|
||||
</g>
|
||||
<!-- A2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>A2</title>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-632 202.1165,-664 300.1165,-664 300.1165,-632 202.1165,-632"/>
|
||||
<text text-anchor="start" x="224.1665" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-576 202.1165,-632 300.1165,-632 300.1165,-576 202.1165,-576"/>
|
||||
<text text-anchor="start" x="241.1135" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="211.6695" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
<text text-anchor="start" x="216.9485" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
<polygon fill="none" stroke="#000000" points="202.1165,-520 202.1165,-576 300.1165,-576 300.1165,-520 202.1165,-520"/>
|
||||
<text text-anchor="start" x="215.5585" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
|
||||
<text text-anchor="start" x="236.119" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>A3</title>
|
||||
<polygon fill="none" stroke="#000000" points="419.4531,-320 322.7799,-320 322.7799,-284 419.4531,-284 419.4531,-320"/>
|
||||
<text text-anchor="middle" x="371.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A2->A3 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A2->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M285.5402,-508.8093C310.5478,-448.3743 342.848,-370.3156 359.7149,-329.5539"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="285.5219,-508.8538 286.9238,-515.9273 280.9336,-519.942 279.5317,-512.8685 285.5219,-508.8538"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="363.5595,-320.2627 363.894,-331.2235 361.6477,-324.8828 359.7359,-329.5029 359.7359,-329.5029 359.7359,-329.5029 361.6477,-324.8828 355.5779,-327.7823 363.5595,-320.2627 363.5595,-320.2627"/>
|
||||
</g>
|
||||
<!-- A4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>A4</title>
|
||||
<polygon fill="none" stroke="#000000" points="304.5106,-320 197.7224,-320 197.7224,-284 304.5106,-284 304.5106,-320"/>
|
||||
<text text-anchor="middle" x="251.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
|
||||
</g>
|
||||
<!-- A2->A4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A2->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M251.1165,-507.5905C251.1165,-447.68 251.1165,-370.9429 251.1165,-330.266"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="251.1166,-507.942 255.1165,-513.942 251.1165,-519.942 247.1165,-513.942 251.1166,-507.942"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="251.1165,-320.2627 255.6166,-330.2626 251.1165,-325.2627 251.1166,-330.2627 251.1166,-330.2627 251.1166,-330.2627 251.1165,-325.2627 246.6166,-330.2627 251.1165,-320.2627 251.1165,-320.2627"/>
|
||||
</g>
|
||||
<!-- A8 -->
|
||||
<g id="node9" class="node">
|
||||
<title>A8</title>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,-100 265.1165,-132 443.1165,-132 443.1165,-100 265.1165,-100"/>
|
||||
<text text-anchor="start" x="309.668" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,-68 265.1165,-100 443.1165,-100 443.1165,-68 265.1165,-68"/>
|
||||
<text text-anchor="start" x="321.8875" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
|
||||
<polygon fill="none" stroke="#000000" points="265.1165,0 265.1165,-68 443.1165,-68 443.1165,0 265.1165,0"/>
|
||||
<text text-anchor="start" x="305.774" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||
<text text-anchor="start" x="296.605" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward()</text>
|
||||
<text text-anchor="start" x="274.9255" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish_outstanding_mqtt()</text>
|
||||
<text text-anchor="start" x="339.119" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A3->A8 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A3->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M368.9314,-271.6651C366.5869,-239.1181 362.7725,-186.1658 359.6014,-142.1431"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="368.9485,-271.9044 373.3693,-277.6014 369.8108,-283.8733 365.39,-278.1763 368.9485,-271.9044"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="358.8731,-132.0321 364.08,-141.6829 359.2323,-137.0192 359.5916,-142.0063 359.5916,-142.0063 359.5916,-142.0063 359.2323,-137.0192 355.1032,-142.3296 358.8731,-132.0321 358.8731,-132.0321"/>
|
||||
</g>
|
||||
<!-- A9 -->
|
||||
<g id="node10" class="node">
|
||||
<title>A9</title>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-82 93.1165,-114 231.1165,-114 231.1165,-82 93.1165,-82"/>
|
||||
<text text-anchor="start" x="119.6135" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-62 93.1165,-82 231.1165,-82 231.1165,-62 93.1165,-62"/>
|
||||
<polygon fill="none" stroke="#000000" points="93.1165,-18 93.1165,-62 231.1165,-62 231.1165,-18 93.1165,-18"/>
|
||||
<text text-anchor="start" x="115.9945" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||
<text text-anchor="start" x="102.9405" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>_async_forward())</text>
|
||||
</g>
|
||||
<!-- A4->A9 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A4->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M244.2806,-283.8733C231.5204,-250.0372 203.5834,-175.9573 183.8383,-123.5994"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="180.2523,-114.0904 187.9915,-121.8593 182.0166,-118.7688 183.781,-123.4472 183.781,-123.4472 183.781,-123.4472 182.0166,-118.7688 179.5704,-125.0351 180.2523,-114.0904 180.2523,-114.0904"/>
|
||||
<text text-anchor="middle" x="229.9759" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A5 -->
|
||||
<g id="node6" class="node">
|
||||
<title>A5</title>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-1054 145.1165,-1086 262.1165,-1086 262.1165,-1054 145.1165,-1054"/>
|
||||
<text text-anchor="start" x="173.0455" y="-1067" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><<AsyncIfc>></text>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-1034 145.1165,-1054 262.1165,-1054 262.1165,-1034 145.1165,-1034"/>
|
||||
<polygon fill="none" stroke="#000000" points="145.1165,-762 145.1165,-1034 262.1165,-1034 262.1165,-762 145.1165,-762"/>
|
||||
<text text-anchor="start" x="173.0525" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
|
||||
<text text-anchor="start" x="171.3825" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
|
||||
<text text-anchor="start" x="185.28" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
|
||||
<text text-anchor="start" x="183.0605" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
|
||||
<text text-anchor="start" x="186.67" y="-955" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
|
||||
<text text-anchor="start" x="182.78" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
|
||||
<text text-anchor="start" x="186.95" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
|
||||
<text text-anchor="start" x="182.7855" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
|
||||
<text text-anchor="start" x="186.95" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
|
||||
<text text-anchor="start" x="181.391" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
|
||||
<text text-anchor="start" x="183.061" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
|
||||
<text text-anchor="start" x="186.395" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
|
||||
<text text-anchor="start" x="182.505" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
|
||||
<text text-anchor="start" x="186.675" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
|
||||
<text text-anchor="start" x="182.5105" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
|
||||
<text text-anchor="start" x="186.675" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
|
||||
<text text-anchor="start" x="178.6155" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
|
||||
<text text-anchor="start" x="154.996" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
|
||||
</g>
|
||||
<!-- A6 -->
|
||||
<g id="node7" class="node">
|
||||
<title>A6</title>
|
||||
<polygon fill="none" stroke="#000000" points="87.1165,-622 87.1165,-654 180.1165,-654 180.1165,-622 87.1165,-622"/>
|
||||
<text text-anchor="start" x="105.2805" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
|
||||
<polygon fill="none" stroke="#000000" points="87.1165,-530 87.1165,-622 180.1165,-622 180.1165,-530 87.1165,-530"/>
|
||||
<text text-anchor="start" x="96.6645" y="-603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="100.5535" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="100.2785" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
|
||||
<text text-anchor="start" x="99.7125" y="-567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
|
||||
<text text-anchor="start" x="115.83" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="109.166" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
|
||||
</g>
|
||||
<!-- A5->A6 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A5->A6</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M166.8518,-752.0017C159.4629,-716.9571 152.1492,-682.2694 146.2303,-654.1971"/>
|
||||
<polygon fill="none" stroke="#000000" points="163.4489,-752.8275 168.9367,-761.8903 170.2983,-751.3833 163.4489,-752.8275"/>
|
||||
</g>
|
||||
<!-- A7 -->
|
||||
<g id="node8" class="node">
|
||||
<title>A7</title>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-390 78.1165,-422 180.1165,-422 180.1165,-390 78.1165,-390"/>
|
||||
<text text-anchor="start" x="99.3905" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-310 78.1165,-390 180.1165,-390 180.1165,-310 78.1165,-310"/>
|
||||
<text text-anchor="start" x="114.6695" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||
<text text-anchor="start" x="116.8995" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||
<text text-anchor="start" x="119.1135" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="114.6695" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||
<text text-anchor="start" x="115.2245" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||
<polygon fill="none" stroke="#000000" points="78.1165,-182 78.1165,-310 180.1165,-310 180.1165,-182 78.1165,-182"/>
|
||||
<text text-anchor="start" x="100.7705" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||
<text text-anchor="start" x="116.8985" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||
<text text-anchor="start" x="114.119" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="109.6705" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="94.387" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||
<text text-anchor="start" x="93.8375" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||
<text text-anchor="start" x="87.7235" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||
</g>
|
||||
<!-- A6->A7 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A6->A7</title>
|
||||
<path fill="none" stroke="#000000" d="M132.1177,-519.5861C131.7106,-490.0737 131.229,-455.1552 130.7721,-422.0295"/>
|
||||
<polygon fill="none" stroke="#000000" points="128.6207,-519.837 132.2584,-529.7877 135.6201,-519.7404 128.6207,-519.837"/>
|
||||
</g>
|
||||
<!-- A7->A8 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A7->A8</title>
|
||||
<path fill="none" stroke="#000000" d="M186.5777,-185.0204C187.4154,-184.0001 188.2616,-182.9929 189.1165,-182 210.4788,-157.1889 238.2469,-135.0276 264.8921,-116.8901"/>
|
||||
<polygon fill="none" stroke="#000000" points="183.6875,-183.0361 180.3256,-193.0834 189.2193,-187.3255 183.6875,-183.0361"/>
|
||||
</g>
|
||||
<!-- A7->A9 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A7->A9</title>
|
||||
<path fill="none" stroke="#000000" d="M147.3214,-171.8077C150.1952,-151.2556 152.9992,-131.2022 155.3799,-114.1772"/>
|
||||
<polygon fill="none" stroke="#000000" points="143.8252,-171.5375 145.9065,-181.9259 150.7577,-172.5069 143.8252,-171.5375"/>
|
||||
</g>
|
||||
<!-- A10 -->
|
||||
<g id="node11" class="node">
|
||||
<title>A10</title>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-668 319.1165,-700 410.1165,-700 410.1165,-668 319.1165,-668"/>
|
||||
<text text-anchor="start" x="337.1115" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-552 319.1165,-668 410.1165,-668 410.1165,-552 319.1165,-552"/>
|
||||
<text text-anchor="start" x="345.4395" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
|
||||
<text text-anchor="start" x="354.6135" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||
<text text-anchor="start" x="349.6145" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||
<text text-anchor="start" x="352.674" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||
<text text-anchor="start" x="357.6725" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||
<text text-anchor="start" x="336.8265" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
|
||||
<text text-anchor="start" x="350.7285" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||
<polygon fill="none" stroke="#000000" points="319.1165,-484 319.1165,-552 410.1165,-552 410.1165,-484 319.1165,-484"/>
|
||||
<text text-anchor="start" x="329.057" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||
<text text-anchor="start" x="345.1705" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||
<text text-anchor="start" x="349.619" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A10->A3 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A10->A3</title>
|
||||
<path fill="none" stroke="#000000" d="M366.9763,-473.5237C368.222,-421.9136 369.5798,-365.6622 370.389,-332.138"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="366.733,-483.6023 362.4757,-473.4966 366.8537,-478.6038 366.9744,-473.6052 366.9744,-473.6052 366.9744,-473.6052 366.8537,-478.6038 371.4731,-473.7139 366.733,-483.6023 366.733,-483.6023"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="370.3911,-332.0495 366.5371,-325.9547 370.6807,-320.053 374.5347,-326.1478 370.3911,-332.0495"/>
|
||||
</g>
|
||||
<!-- A10->A4 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A10->A4</title>
|
||||
<path fill="none" stroke="#000000" d="M318.2339,-474.2481C295.3796,-415.5956 270.1211,-350.7729 258.151,-320.053"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="321.8788,-483.6023 314.0551,-475.9185 320.0634,-478.9435 318.2481,-474.2847 318.2481,-474.2847 318.2481,-474.2847 320.0634,-478.9435 322.441,-472.6508 321.8788,-483.6023 321.8788,-483.6023"/>
|
||||
<text text-anchor="middle" x="272.6076" y="-330.8736" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
<!-- A12 -->
|
||||
<g id="node13" class="node">
|
||||
<title>A12</title>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-318 442.1165,-350 509.1165,-350 509.1165,-318 442.1165,-318"/>
|
||||
<text text-anchor="start" x="454.775" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-298 442.1165,-318 509.1165,-318 509.1165,-298 442.1165,-298"/>
|
||||
<polygon fill="none" stroke="#000000" points="442.1165,-254 442.1165,-298 509.1165,-298 509.1165,-254 442.1165,-254"/>
|
||||
<text text-anchor="start" x="452.0005" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
|
||||
<text text-anchor="start" x="459.7845" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
|
||||
</g>
|
||||
<!-- A10->A12 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A10->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M405.6067,-483.6023C421.7045,-441.5449 439.4849,-395.0916 453.0329,-359.6958"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="456.737,-350.0185 457.3649,-360.9664 454.9496,-354.6881 453.1623,-359.3577 453.1623,-359.3577 453.1623,-359.3577 454.9496,-354.6881 448.9596,-357.7491 456.737,-350.0185 456.737,-350.0185"/>
|
||||
</g>
|
||||
<!-- A11 -->
|
||||
<g id="node12" class="node">
|
||||
<title>A11</title>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-680 428.1165,-712 531.1165,-712 531.1165,-680 428.1165,-680"/>
|
||||
<text text-anchor="start" x="468.7785" y="-693" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-624 428.1165,-680 531.1165,-680 531.1165,-624 428.1165,-624"/>
|
||||
<text text-anchor="start" x="471.558" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
|
||||
<text text-anchor="start" x="447.1025" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
|
||||
<text text-anchor="start" x="460.72" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
|
||||
<polygon fill="none" stroke="#000000" points="428.1165,-472 428.1165,-624 531.1165,-624 531.1165,-472 428.1165,-472"/>
|
||||
<text text-anchor="start" x="455.452" y="-605" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
|
||||
<text text-anchor="start" x="453.501" y="-593" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
|
||||
<text text-anchor="start" x="450.447" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="448.777" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
<text text-anchor="start" x="446.8265" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
|
||||
<text text-anchor="start" x="461.8295" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
|
||||
<text text-anchor="start" x="454.6105" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
|
||||
<text text-anchor="start" x="455.991" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
|
||||
<text text-anchor="start" x="440.1535" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
|
||||
<text text-anchor="start" x="449.602" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
|
||||
<text text-anchor="start" x="437.939" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
|
||||
</g>
|
||||
<!-- A11->A12 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A11->A12</title>
|
||||
<path fill="none" stroke="#000000" d="M477.322,-461.8987C476.7744,-422.1971 476.206,-380.9898 475.7834,-350.352"/>
|
||||
<polygon fill="none" stroke="#000000" points="473.823,-462.0018 477.4607,-471.9525 480.8223,-461.9052 473.823,-462.0018"/>
|
||||
</g>
|
||||
<!-- A13 -->
|
||||
<g id="node14" class="node">
|
||||
<title>A13</title>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1464 172.1165,-1496 321.1165,-1496 321.1165,-1464 172.1165,-1464"/>
|
||||
<text text-anchor="start" x="226.334" y="-1477" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1240 172.1165,-1464 321.1165,-1464 321.1165,-1240 172.1165,-1240"/>
|
||||
<text text-anchor="start" x="209.943" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||
<text text-anchor="start" x="220.5005" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
|
||||
<text text-anchor="start" x="221.335" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
|
||||
<text text-anchor="start" x="228.83" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||
<text text-anchor="start" x="207.1595" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||
<text text-anchor="start" x="221.6065" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
|
||||
<text text-anchor="start" x="227.4405" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
|
||||
<text text-anchor="start" x="224.941" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||
<text text-anchor="start" x="218.8315" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
|
||||
<text text-anchor="start" x="215.7725" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
|
||||
<text text-anchor="start" x="222.7165" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
|
||||
<text text-anchor="start" x="196.321" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
|
||||
<text text-anchor="start" x="215.501" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
|
||||
<text text-anchor="start" x="211.6235" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
|
||||
<text text-anchor="start" x="220.5015" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
|
||||
<text text-anchor="start" x="209.669" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
|
||||
<text text-anchor="start" x="200.7705" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
|
||||
<polygon fill="none" stroke="#000000" points="172.1165,-1136 172.1165,-1240 321.1165,-1240 321.1165,-1136 172.1165,-1136"/>
|
||||
<text text-anchor="start" x="195.501" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
|
||||
<text text-anchor="start" x="224.1165" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
|
||||
<text text-anchor="start" x="196.884" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="181.876" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async> end_modbus_cmd()</text>
|
||||
<text text-anchor="start" x="231.619" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
<text text-anchor="start" x="217.447" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
|
||||
<text text-anchor="start" x="215.777" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
|
||||
</g>
|
||||
<!-- A13->A5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A13->A5</title>
|
||||
<path fill="none" stroke="#000000" d="M226.347,-1135.7758C224.8967,-1122.5547 223.4359,-1109.2373 221.9911,-1096.0662"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="220.8898,-1086.0268 226.4535,-1095.4764 221.4351,-1090.997 221.9803,-1095.9672 221.9803,-1095.9672 221.9803,-1095.9672 221.4351,-1090.997 217.5072,-1096.4579 220.8898,-1086.0268 220.8898,-1086.0268"/>
|
||||
<text text-anchor="middle" x="215.9686" y="-1115.6794" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
|
||||
</g>
|
||||
<!-- A13->A10 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>A13->A10</title>
|
||||
<path fill="none" stroke="#000000" d="M277.1595,-1125.5329C299.2708,-989.8666 328.1962,-812.3923 346.4719,-700.2604"/>
|
||||
<polygon fill="none" stroke="#000000" points="273.6668,-1125.205 275.5125,-1135.6378 280.5757,-1126.3311 273.6668,-1125.205"/>
|
||||
</g>
|
||||
<!-- A14->A13 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>A14->A13</title>
|
||||
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M204.2906,-1613.8004C208.8542,-1581.3079 214.8136,-1538.8764 220.7975,-1496.2713"/>
|
||||
<polygon fill="none" stroke="#000000" points="200.7847,-1613.5986 202.8597,-1623.9883 207.7166,-1614.5723 200.7847,-1613.5986"/>
|
||||
</g>
|
||||
<!-- A15 -->
|
||||
<g id="node16" class="node">
|
||||
<title>A15</title>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1766 260.1165,-1798 335.1165,-1798 335.1165,-1766 260.1165,-1766"/>
|
||||
<text text-anchor="start" x="279.834" y="-1779" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1614 260.1165,-1766 335.1165,-1766 335.1165,-1614 260.1165,-1614"/>
|
||||
<text text-anchor="start" x="289.278" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
|
||||
<text text-anchor="start" x="270.1065" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
|
||||
<text text-anchor="start" x="271.2215" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
|
||||
<text text-anchor="start" x="281.225" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
|
||||
<text text-anchor="start" x="271.506" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
|
||||
<text text-anchor="start" x="279.5585" y="-1675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
|
||||
<text text-anchor="start" x="291.508" y="-1663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
|
||||
<text text-anchor="start" x="278.17" y="-1651" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
|
||||
<text text-anchor="start" x="276.4955" y="-1639" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
|
||||
<text text-anchor="start" x="290.953" y="-1627" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
|
||||
<polygon fill="none" stroke="#000000" points="260.1165,-1546 260.1165,-1614 335.1165,-1614 335.1165,-1546 260.1165,-1546"/>
|
||||
<text text-anchor="start" x="271.5065" y="-1595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
|
||||
<text text-anchor="start" x="274.8405" y="-1583" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
|
||||
<text text-anchor="start" x="272.3405" y="-1571" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
|
||||
<text text-anchor="start" x="282.619" y="-1559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||
</g>
|
||||
<!-- A15->A13 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>A15->A13</title>
|
||||
<path fill="none" stroke="#000000" d="M277.6392,-1536.041C275.7633,-1522.9463 273.8413,-1509.5297 271.9169,-1496.0971"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="279.064,-1545.9867 273.1913,-1536.726 278.3549,-1541.0373 277.6458,-1536.0878 277.6458,-1536.0878 277.6458,-1536.0878 278.3549,-1541.0373 282.1004,-1535.4496 279.064,-1545.9867 279.064,-1545.9867"/>
|
||||
<text text-anchor="middle" x="282.8544" y="-1509.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||
<text text-anchor="middle" x="268.1266" y="-1526.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
42
app/proxy_3.yuml
Normal file
@@ -0,0 +1,42 @@
|
||||
// {type:class}
|
||||
// {direction:topDown}
|
||||
// {generate:true}
|
||||
|
||||
[note: Example of instantiation for a GEN3PLUS inverter!{bg:cornsilk}]
|
||||
[<<AbstractIterMeta>>||__iter__()]
|
||||
|
||||
[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
|
||||
[InverterG3P]++->[local:StreamPtr]
|
||||
[InverterG3P]++->[remote:StreamPtr]
|
||||
|
||||
[<<AsyncIfc>>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()]
|
||||
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
|
||||
[AsyncStream|reader;writer;addr;r_addr;l_addr|;<async>loop;disc();close();healthy();;__async_read();__async_write();__async_forward()]
|
||||
[AsyncStreamServer|create_remote|<async>server_loop();<async>_async_forward();<async>publish_outstanding_mqtt();close()]
|
||||
[AsyncStreamClient||<async>client_loop();<async>_async_forward())]
|
||||
[<<AsyncIfc>>]^-.-[AsyncIfcImpl]
|
||||
[AsyncIfcImpl]^[AsyncStream]
|
||||
[AsyncStream]^[AsyncStreamServer]
|
||||
[AsyncStream]^[AsyncStreamClient]
|
||||
|
||||
[SolarmanV5|conn_no;addr;;control;serial;snr;db:InfosG3P;switch|msg_unknown();;healthy();close()]
|
||||
[SolarmanV5]<-++[local:StreamPtr]
|
||||
[local:StreamPtr]++->[AsyncStreamServer]
|
||||
[SolarmanV5]<-0..1[remote:StreamPtr]
|
||||
[remote:StreamPtr]0..1->[AsyncStreamClient]
|
||||
|
||||
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
|
||||
[Infos]^[InfosG3P||ha_confs();parse()]
|
||||
|
||||
[SolarmanV5]->[InfosG3P]
|
||||
|
||||
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
|
||||
[Message]use->[<<AsyncIfc>>]
|
||||
|
||||
[<<ProtocolIfc>>|_registry|close()]
|
||||
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
|
||||
[<<ProtocolIfc>>]^-.-[Message]
|
||||
[Message]^[SolarmanV5]
|
||||
|
||||
[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
|
||||
[Modbus]<0..1-has[Message]
|
||||
@@ -2,5 +2,6 @@
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
python-dotenv
|
||||
mock
|
||||
coverage
|
||||
@@ -6,16 +6,10 @@ from asyncio import StreamReader, StreamWriter
|
||||
from typing import Self
|
||||
from itertools import count
|
||||
|
||||
if __name__ == "app.src.async_stream":
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
|
||||
|
||||
import gc
|
||||
@@ -221,7 +215,6 @@ class AsyncStream(AsyncIfcImpl):
|
||||
|
||||
async def disc(self) -> None:
|
||||
"""Async disc handler for graceful disconnect"""
|
||||
self.remote = None
|
||||
if self._writer.is_closing():
|
||||
return
|
||||
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
||||
@@ -306,6 +299,14 @@ class AsyncStream(AsyncIfcImpl):
|
||||
f"Fwd Exception for {self.r_addr}:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def publish_outstanding_mqtt(self):
|
||||
'''Publish all outstanding MQTT topics'''
|
||||
try:
|
||||
await self.async_publ_mqtt()
|
||||
await Proxy._async_publ_mqtt_proxy_stat('proxy')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class AsyncStreamServer(AsyncStream):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
@@ -355,14 +356,6 @@ class AsyncStreamServer(AsyncStream):
|
||||
self.remote.ifc._writer.write(self.fwd_fifo.get())
|
||||
await self.remote.ifc._writer.drain()
|
||||
|
||||
async def publish_outstanding_mqtt(self):
|
||||
'''Publish all outstanding MQTT topics'''
|
||||
try:
|
||||
await self.async_publ_mqtt()
|
||||
await Proxy._async_publ_mqtt_proxy_stat('proxy')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class AsyncStreamClient(AsyncStream):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
@@ -370,6 +363,11 @@ class AsyncStreamClient(AsyncStream):
|
||||
AsyncStream.__init__(self, reader, writer, rstream)
|
||||
self.close_cb = close_cb
|
||||
|
||||
async def disc(self) -> None:
|
||||
logging.debug('AsyncStreamClient.disc()')
|
||||
self.remote = None
|
||||
await super().disc()
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug('AsyncStreamClient.close()')
|
||||
self.close_cb = None
|
||||
@@ -377,7 +375,11 @@ class AsyncStreamClient(AsyncStream):
|
||||
|
||||
async def client_loop(self, _: str) -> None:
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
Infos.inc_counter('Cloud_Conn_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
await self.loop()
|
||||
Infos.dec_counter('Cloud_Conn_Cnt')
|
||||
await self.publish_outstanding_mqtt()
|
||||
logger.info(f'[{self.node_id}:{self.conn_no}] '
|
||||
'Client loop stopped for'
|
||||
f' l{self.l_addr}')
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
|
||||
if __name__ == "app.src.byte_fifo":
|
||||
from app.src.messages import hex_dump_str, hex_dump_memory
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
|
||||
|
||||
class ByteFifo:
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
'''Config module handles the proxy configuration in the config.toml file'''
|
||||
'''Config module handles the proxy configuration'''
|
||||
|
||||
import shutil
|
||||
import tomllib
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from schema import Schema, And, Or, Use, Optional
|
||||
|
||||
|
||||
class ConfigIfc(ABC):
|
||||
'''Abstract basis class for config readers'''
|
||||
def __init__(self):
|
||||
Config.add(self)
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self) -> dict: # pragma: no cover
|
||||
'''get the unverified config from the reader'''
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def descr(self) -> str: # pragma: no cover
|
||||
'''return a descriction of the source, e.g. the file name'''
|
||||
pass
|
||||
|
||||
def _extend_key(self, conf, key, val):
|
||||
'''split a dotted dict key into a hierarchical dict tree '''
|
||||
lst = key.split('.')
|
||||
d = conf
|
||||
for i, idx in enumerate(lst, 1): # pragma: no branch
|
||||
if i == len(lst):
|
||||
d[idx] = val
|
||||
break
|
||||
if idx not in d:
|
||||
d[idx] = {}
|
||||
d = d[idx]
|
||||
|
||||
|
||||
class Config():
|
||||
'''Static class Config is reads and sanitize the config.
|
||||
'''Static class Config build and sanitize the internal config dictenary.
|
||||
|
||||
Read config.toml file and sanitize it with read().
|
||||
Get named parts of the config with get()'''
|
||||
Using config readers, a partial configuration is added to config.
|
||||
Config readers are a derivation of the abstract ConfigIfc reader.
|
||||
When a config reader is instantiated, theits `get_config` method is
|
||||
called automatically and afterwards the config will be merged.
|
||||
'''
|
||||
|
||||
act_config = {}
|
||||
def_config = {}
|
||||
conf_schema = Schema({
|
||||
'tsun': {
|
||||
'enabled': Use(bool),
|
||||
@@ -28,8 +57,10 @@ class Config():
|
||||
'mqtt': {
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
|
||||
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||
'user': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None))),
|
||||
'passwd': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None)))
|
||||
},
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
@@ -57,7 +88,8 @@ class Config():
|
||||
Optional('client_mode'): {
|
||||
'host': Use(str),
|
||||
Optional('port', default=8899):
|
||||
And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||
And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
Optional('forward', default=False): Use(bool),
|
||||
},
|
||||
Optional('modbus_polling', default=True): Use(bool),
|
||||
Optional('suggested_area', default=""): Use(str),
|
||||
@@ -92,7 +124,13 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def class_init(cls) -> None | str: # pragma: no cover
|
||||
def init(cls, def_reader: ConfigIfc) -> None | str:
|
||||
'''Initialise the Proxy-Config
|
||||
|
||||
Copy the internal default config file into the config directory
|
||||
and initialise the Config with the default configuration '''
|
||||
cls.err = None
|
||||
cls.def_config = {}
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
@@ -102,66 +140,58 @@ class Config():
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
err_str = cls.read()
|
||||
del cls.conf_schema
|
||||
return err_str
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict: # pragma: no cover
|
||||
usr_config = {}
|
||||
|
||||
# read example config file as default configuration
|
||||
try:
|
||||
with open("config/config.toml", "rb") as f:
|
||||
usr_config = tomllib.load(f)
|
||||
def_config = def_reader.get_config()
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
logging.info(f'Read from {def_reader.descr()} => ok')
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
logging.info(
|
||||
'\n To create the missing config.toml file, '
|
||||
'you can rename the template config.example.toml\n'
|
||||
' and customize it for your scenario.\n')
|
||||
return usr_config
|
||||
cls.err = f'Config.read: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {def_reader.descr()} => error\n {error}")
|
||||
|
||||
cls.act_config = cls.def_config.copy()
|
||||
|
||||
@classmethod
|
||||
def read(cls, path='') -> None | str:
|
||||
'''Read config file, merge it with the default config
|
||||
def add(cls, reader: ConfigIfc):
|
||||
'''Merge the config from the Config Reader into the config
|
||||
|
||||
Checks if a default config exists. If no default configuration exists,
|
||||
the Config.init method has not yet been called.This is normal for the very
|
||||
first Config Reader which creates the default config and must be ignored
|
||||
here. The default config reader is handled in the Config.init method'''
|
||||
if hasattr(cls, 'def_config'):
|
||||
cls.__parse(reader)
|
||||
|
||||
@classmethod
|
||||
def get_error(cls) -> None | str:
|
||||
'''return the last error as a string or None if there is no error'''
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def __parse(cls, reader) -> None | str:
|
||||
'''Read config from the reader, merge it with the default config
|
||||
and sanitize the result'''
|
||||
err = None
|
||||
config = {}
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
res = 'ok'
|
||||
try:
|
||||
# read example config file as default configuration
|
||||
cls.def_config = {}
|
||||
with open(f"{path}default_config.toml", "rb") as f:
|
||||
def_config = tomllib.load(f)
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
|
||||
# overwrite the default values, with values from
|
||||
# the config.toml file
|
||||
usr_config = cls._read_config_file()
|
||||
|
||||
# merge the default and the user config
|
||||
config = def_config.copy()
|
||||
rd_config = reader.get_config()
|
||||
config = cls.act_config.copy()
|
||||
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
|
||||
'gen3plus']:
|
||||
if key in usr_config:
|
||||
config[key] |= usr_config[key]
|
||||
|
||||
try:
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
|
||||
# logging.debug(f'Readed config: "{cls.act_config}" ')
|
||||
if key in rd_config:
|
||||
config[key] = config[key] | rd_config[key]
|
||||
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except FileNotFoundError:
|
||||
res = 'n/a'
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logger.error(err)
|
||||
cls.act_config = {}
|
||||
cls.err = f'error: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {reader.descr()} => error\n {error}")
|
||||
|
||||
return err
|
||||
logging.info(f'Read from {reader.descr()} => {res}')
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def get(cls, member: str = None):
|
||||
25
app/src/cnf/config_read_env.py
Normal file
@@ -0,0 +1,25 @@
|
||||
'''Config Reader module which handles config values from the environment'''
|
||||
|
||||
import os
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadEnv(ConfigIfc):
|
||||
'''Reader for environment values of the configuration'''
|
||||
|
||||
def get_config(self) -> dict:
|
||||
conf = {}
|
||||
data = [
|
||||
('mqtt.host', 'MQTT_HOST'),
|
||||
('mqtt.port', 'MQTT_PORT'),
|
||||
('mqtt.user', 'MQTT_USER'),
|
||||
('mqtt.passwd', 'MQTT_PASSWORD'),
|
||||
]
|
||||
for key, env_var in data:
|
||||
val = os.getenv(env_var)
|
||||
if val:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def descr(self):
|
||||
return "Read environment"
|
||||
46
app/src/cnf/config_read_json.py
Normal file
@@ -0,0 +1,46 @@
|
||||
'''Config Reader module which handles *.json config files'''
|
||||
|
||||
import json
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadJson(ConfigIfc):
|
||||
'''Reader for json config files'''
|
||||
def __init__(self, cnf_file='/data/options.json'):
|
||||
'''Read a json file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def convert_inv(self, conf, inv):
|
||||
if 'serial' in inv:
|
||||
snr = inv['serial']
|
||||
del inv['serial']
|
||||
conf[snr] = {}
|
||||
|
||||
for key, val in inv.items():
|
||||
self._extend_key(conf[snr], key, val)
|
||||
|
||||
def convert_inv_arr(self, conf, key, val: list):
|
||||
if key not in conf:
|
||||
conf[key] = {}
|
||||
for elm in val:
|
||||
self.convert_inv(conf[key], elm)
|
||||
|
||||
def convert_to_obj(self, data):
|
||||
conf = {}
|
||||
for key, val in data.items():
|
||||
if key == 'inverters' and isinstance(val, list):
|
||||
self.convert_inv_arr(conf, key, val)
|
||||
else:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file) as f:
|
||||
data = json.load(f)
|
||||
return self.convert_to_obj(data)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
21
app/src/cnf/config_read_toml.py
Normal file
@@ -0,0 +1,21 @@
|
||||
'''Config Reader module which handles *.toml config files'''
|
||||
|
||||
import tomllib
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadToml(ConfigIfc):
|
||||
'''Reader for toml config files'''
|
||||
def __init__(self, cnf_file):
|
||||
'''Read a toml file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
@@ -3,10 +3,7 @@ import struct
|
||||
import logging
|
||||
from typing import Generator
|
||||
|
||||
if __name__ == "app.src.gen3.infos_g3":
|
||||
from app.src.infos import Infos, Register
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register
|
||||
from infos import Infos, Register
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
@@ -70,24 +67,21 @@ class RegisterMap:
|
||||
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
|
||||
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
|
||||
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
|
||||
0x00000191: {'reg': Register.EVENT_401},
|
||||
0x00000192: {'reg': Register.EVENT_402},
|
||||
0x00000193: {'reg': Register.EVENT_403},
|
||||
0x00000194: {'reg': Register.EVENT_404},
|
||||
0x00000195: {'reg': Register.EVENT_405},
|
||||
0x00000196: {'reg': Register.EVENT_406},
|
||||
0x00000197: {'reg': Register.EVENT_407},
|
||||
0x00000198: {'reg': Register.EVENT_408},
|
||||
0x00000199: {'reg': Register.EVENT_409},
|
||||
0x0000019a: {'reg': Register.EVENT_410},
|
||||
0x0000019b: {'reg': Register.EVENT_411},
|
||||
0x0000019c: {'reg': Register.EVENT_412},
|
||||
0x0000019d: {'reg': Register.EVENT_413},
|
||||
0x0000019e: {'reg': Register.EVENT_414},
|
||||
0x0000019f: {'reg': Register.EVENT_415},
|
||||
0x000001a0: {'reg': Register.EVENT_416},
|
||||
0x00000190: {'reg': Register.EVENT_ALARM},
|
||||
0x000001f4: {'reg': Register.EVENT_FAULT},
|
||||
0x00000258: {'reg': Register.EVENT_BF1},
|
||||
0x000002bc: {'reg': Register.EVENT_BF2},
|
||||
0x00000064: {'reg': Register.INVERTER_STATUS},
|
||||
|
||||
0x00000fa0: {'reg': Register.BOOT_STATUS},
|
||||
0x00001004: {'reg': Register.DSP_STATUS},
|
||||
0x000010cc: {'reg': Register.WORK_MODE},
|
||||
0x000011f8: {'reg': Register.OUTPUT_SHUTDOWN},
|
||||
0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
|
||||
0x000012c0: {'reg': Register.RATED_LEVEL},
|
||||
0x00001324: {'reg': Register.INPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
0x00001388: {'reg': Register.GRID_VOLT_CAL_COEF},
|
||||
0x00002710: {'reg': Register.PROD_COMPL_TYPE},
|
||||
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
}
|
||||
|
||||
@@ -183,11 +177,8 @@ class InfosG3(Infos):
|
||||
i += 1
|
||||
|
||||
def __modify_val(self, row, result):
|
||||
if row:
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
if row and 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
def __store_result(self, addr, result, info_id, node_id):
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
if __name__ == "app.src.gen3.inverter_g3":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3.talent import Talent
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
|
||||
class InverterG3(InverterBase):
|
||||
|
||||
@@ -4,22 +4,12 @@ from zoneinfo import ZoneInfo
|
||||
from datetime import datetime
|
||||
from tzlocal import get_localzone
|
||||
|
||||
if __name__ == "app.src.gen3.talent":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.infos_g3 import InfosG3
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from cnf.config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -42,19 +32,18 @@ class Control:
|
||||
|
||||
|
||||
class Talent(Message):
|
||||
MB_START_TIMEOUT = 40
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
|
||||
|
||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||
client_mode: bool = False, id_str=b''):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=15)
|
||||
super().__init__('G3', ifc, server_side, self.send_modbus_cb,
|
||||
mb_timeout=15)
|
||||
ifc.rx_set_cb(self.read)
|
||||
ifc.prot_set_timeout_cb(self._timeout)
|
||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||
ifc.prot_set_update_header_cb(self._update_header)
|
||||
|
||||
self.addr = addr
|
||||
self.ifc = ifc
|
||||
self.conn_no = ifc.get_conn_no()
|
||||
self.await_conn_resp_cnt = 0
|
||||
self.id_str = id_str
|
||||
@@ -86,38 +75,17 @@ class Talent(Message):
|
||||
0x87: self.get_modbus_log_lvl,
|
||||
0x04: logging.INFO,
|
||||
}
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
||||
self.modbus_polling = False
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.debug('Talent.close()')
|
||||
if self.server_side:
|
||||
# set inverter state to offline, if output power is very low
|
||||
logging.debug('close power: '
|
||||
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
|
||||
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
|
||||
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
|
||||
self.new_data['env'] = True
|
||||
|
||||
# we have references to methods of this class in self.switch
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
self.ifc.rx_set_cb(None)
|
||||
self.ifc.prot_set_timeout_cb(None)
|
||||
self.ifc.prot_set_init_new_client_conn_cb(None)
|
||||
self.ifc.prot_set_update_header_cb(None)
|
||||
self.ifc = None
|
||||
super().close()
|
||||
|
||||
def __set_serial_no(self, serial_no: str):
|
||||
@@ -135,6 +103,8 @@ class Talent(Message):
|
||||
self.modbus_polling = inv['modbus_polling']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
self.db.set_pv_module_details(inv)
|
||||
if self.mb:
|
||||
self.mb.set_node_id(self.node_id)
|
||||
else:
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
@@ -203,16 +173,6 @@ class Talent(Message):
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
@@ -590,8 +550,7 @@ class Talent(Message):
|
||||
return
|
||||
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[
|
||||
hdr_len:],
|
||||
self.node_id):
|
||||
hdr_len:]):
|
||||
if update:
|
||||
self._set_mqtt_timestamp(key, self._utc())
|
||||
self.new_data[key] = True
|
||||
|
||||
@@ -1,39 +1,57 @@
|
||||
|
||||
import struct
|
||||
from typing import Generator
|
||||
|
||||
if __name__ == "app.src.gen3plus.infos_g3p":
|
||||
from app.src.infos import Infos, Register, ProxyMode
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register, ProxyMode
|
||||
from infos import Infos, Register, ProxyMode, Fmt
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
# make the class read/only by using __slots__
|
||||
__slots__ = ()
|
||||
|
||||
FMT_2_16BIT_VAL = '!HH'
|
||||
FMT_3_16BIT_VAL = '!HHH'
|
||||
FMT_4_16BIT_VAL = '!HHHH'
|
||||
|
||||
map = {
|
||||
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'eval': 'round(result/60)', 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'quotient': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
|
||||
0x4102001b: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 Max No Of Connected Devices
|
||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||
0x4102001d: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501
|
||||
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
||||
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!BBBBBB', 'eval': '"%02x:%02x:%02x:%02x:%02x:%02x" % res'}, # noqa: E501
|
||||
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!6B', 'func': Fmt.mac}, # noqa: E501
|
||||
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x4102005c: {'reg': None, 'fmt': '<B', 'const': 15}, # noqa: E501
|
||||
0x4102005e: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 No Of Sensors (ListLen)
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x41020061: {'reg': None, 'fmt': '<HB', 'const': (15, 255)}, # noqa: E501
|
||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
|
||||
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
|
||||
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
|
||||
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
|
||||
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||
|
||||
# Start MODBUS Block: 0x3000 (R/O Measurements)
|
||||
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||
0x420100c2: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c4: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c6: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100c8: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100ca: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100cc: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
# 0x420100ce
|
||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
|
||||
0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
|
||||
# 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
|
||||
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
|
||||
# 0x420100da
|
||||
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
@@ -58,12 +76,39 @@ class RegisterMap:
|
||||
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x42010116: {'reg': Register.INV_UNKNOWN_1, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
# Start MODBUS Block: 0x2000 (R/W Config Paramaneters)
|
||||
0x42010118: {'reg': Register.BOOT_STATUS, 'fmt': '!H'},
|
||||
0x4201011a: {'reg': Register.DSP_STATUS, 'fmt': '!H'},
|
||||
0x4201011c: {'reg': None, 'fmt': '!H', 'const': 1}, # noqa: E501
|
||||
0x4201011e: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||
0x42010124: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
|
||||
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H'},
|
||||
0x42010128: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
|
||||
0x4201012a: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x4201012c: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x4201012e: {'reg': None, 'fmt': '!H', 'const': 1024}, # noqa: E501
|
||||
0x42010130: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1024, 1, 0xffff, 1)}, # noqa: E501
|
||||
0x42010138: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
0x4201013a: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x68, 0x68, 0x500)}, # noqa: E501
|
||||
0x42010140: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9cd, 0x7b6, 0x139c, 0x1324)}, # noqa: E501
|
||||
0x42010148: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1, 0x7ae, 0x40f, 0x41)}, # noqa: E501
|
||||
0x42010150: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0xf, 0xa64, 0xa64, 0x6)}, # noqa: E501
|
||||
0x42010158: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x6, 0x9f6, 0x128c, 0x128c)}, # noqa: E501
|
||||
0x42010160: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x1452, 0x1452)}, # noqa: E501
|
||||
0x42010168: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x151, 0x5)}, # noqa: E501
|
||||
0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x42010172: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x1, 0x139c, 0xfa0)}, # noqa: E501
|
||||
0x42010178: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x4e, 0x66, 0x3e8, 0x400)}, # noqa: E501
|
||||
0x42010180: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9ce, 0x7a8, 0x139c, 0x1326)}, # noqa: E501
|
||||
0x42010188: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 0x0, 0)}, # noqa: E501
|
||||
0x42010190: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 1024, 1024)}, # noqa: E501
|
||||
0x42010198: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0, 0, 0xffff, 0)}, # noqa: E501
|
||||
0x420101a0: {'reg': None, 'fmt': FMT_2_16BIT_VAL, 'const': (0x0, 0x0)}, # noqa: E501
|
||||
|
||||
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +168,7 @@ class InfosG3P(Infos):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
info_id = row['reg']
|
||||
result = self.__get_value(buf, addr, row)
|
||||
result = Fmt.get_value(buf, addr, row)
|
||||
|
||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||
|
||||
@@ -138,15 +183,22 @@ class InfosG3P(Infos):
|
||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||
f' : {result}{unit}')
|
||||
|
||||
def __get_value(self, buf, idx, row):
|
||||
'''Get a value from buf and interpret as in row'''
|
||||
fmt = row['fmt']
|
||||
res = struct.unpack_from(fmt, buf, idx)
|
||||
result = res[0]
|
||||
if isinstance(result, (bytearray, bytes)):
|
||||
result = result.decode().split('\x00')[0]
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
def build(self, len, msg_type: int, rcv_ftype: int):
|
||||
buf = bytearray(len)
|
||||
for idx, row in RegisterMap.map.items():
|
||||
addr = idx & 0xffff
|
||||
ftype = (idx >> 16) & 0xff
|
||||
mtype = (idx >> 24) & 0xff
|
||||
if ftype != rcv_ftype or mtype != msg_type:
|
||||
continue
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if 'const' in row:
|
||||
val = row['const']
|
||||
else:
|
||||
info_id = row['reg']
|
||||
val = self.get_db_value(info_id)
|
||||
if not val:
|
||||
continue
|
||||
Fmt.set_value(buf, addr, row, val)
|
||||
return buf
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
if __name__ == "app.src.gen3plus.inverter_g3p":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
|
||||
|
||||
class InverterG3P(InverterBase):
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
client_mode: bool = False):
|
||||
remote_prot = None
|
||||
if client_mode:
|
||||
remote_prot = SolarmanEmu
|
||||
super().__init__(reader, writer, 'solarman',
|
||||
SolarmanV5, client_mode)
|
||||
SolarmanV5, client_mode, remote_prot)
|
||||
|
||||
138
app/src/gen3plus/solarman_emu.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from gen3plus.solarman_v5 import SolarmanBase
|
||||
from my_timer import Timer
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
|
||||
class SolarmanEmu(SolarmanBase):
|
||||
def __init__(self, addr, ifc: "AsyncIfc",
|
||||
server_side: bool, client_mode: bool):
|
||||
super().__init__(addr, ifc, server_side=False,
|
||||
_send_modbus_cb=None,
|
||||
mb_timeout=8)
|
||||
logging.debug('SolarmanEmu.init()')
|
||||
self.db = ifc.remote.stream.db
|
||||
self.snr = ifc.remote.stream.snr
|
||||
self.hb_timeout = 60
|
||||
'''actual heatbeat timeout from the last response message'''
|
||||
self.data_up_inv = self.db.get_db_value(Register.DATA_UP_INTERVAL)
|
||||
'''time interval for getting new MQTT data messages'''
|
||||
self.hb_timer = Timer(self.send_heartbeat_cb, self.node_id)
|
||||
self.data_timer = Timer(self.send_data_cb, self.node_id)
|
||||
self.last_sync = self._emu_timestamp()
|
||||
'''timestamp when we send the last sync message (4110)'''
|
||||
self.pkt_cnt = 0
|
||||
'''last sent packet number'''
|
||||
|
||||
self.switch = {
|
||||
|
||||
0x4210: 'msg_data_ind', # real time data
|
||||
0x1210: self.msg_response, # at least every 5 minutes
|
||||
|
||||
0x4710: 'msg_hbeat_ind', # heatbeat
|
||||
0x1710: self.msg_response, # every 2 minutes
|
||||
|
||||
0x4110: 'msg_dev_ind', # device data, sync start
|
||||
0x1110: self.msg_response, # every 3 hours
|
||||
|
||||
}
|
||||
|
||||
self.log_lvl = {
|
||||
|
||||
0x4110: logging.INFO, # device data, sync start
|
||||
0x1110: logging.INFO, # every 3 hours
|
||||
|
||||
0x4210: logging.INFO, # real time data
|
||||
0x1210: logging.INFO, # at least every 5 minutes
|
||||
|
||||
0x4710: logging.DEBUG, # heatbeat
|
||||
0x1710: logging.DEBUG, # every 2 minutes
|
||||
|
||||
}
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.info('SolarmanEmu.close()')
|
||||
# we have references to methods of this class in self.switch
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.hb_timer.close()
|
||||
self.data_timer.close()
|
||||
self.db = None
|
||||
super().close()
|
||||
|
||||
def _set_serial_no(self, snr: int):
|
||||
logging.debug(f'SolarmanEmu._set_serial_no, snr: {snr}')
|
||||
self.unique_id = str(snr)
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
logging.debug('SolarmanEmu.init_new()')
|
||||
self.data_timer.start(self.data_up_inv)
|
||||
return False
|
||||
|
||||
def next_pkt_cnt(self):
|
||||
'''get the next packet number'''
|
||||
self.pkt_cnt = (self.pkt_cnt + 1) & 0xffffffff
|
||||
return self.pkt_cnt
|
||||
|
||||
def seconds_since_last_sync(self):
|
||||
'''get seconds since last 0x4110 message was sent'''
|
||||
return self._emu_timestamp() - self.last_sync
|
||||
|
||||
def send_heartbeat_cb(self, exp_cnt):
|
||||
'''send a heartbeat to the TSUN cloud'''
|
||||
self._build_header(0x4710)
|
||||
self.ifc.tx_add(struct.pack('<B', 0))
|
||||
self._finish_send_msg()
|
||||
log_lvl = self.log_lvl.get(0x4710, logging.WARNING)
|
||||
self.ifc.tx_log(log_lvl, 'Send heartbeat:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def send_data_cb(self, exp_cnt):
|
||||
'''send a inverter data message to the TSUN cloud'''
|
||||
self.hb_timer.start(self.hb_timeout)
|
||||
self.data_timer.start(self.data_up_inv)
|
||||
_len = 420
|
||||
ftype = 1
|
||||
build_msg = self.db.build(_len, 0x42, ftype)
|
||||
|
||||
self._build_header(0x4210)
|
||||
self.ifc.tx_add(
|
||||
struct.pack(
|
||||
'<BHLLLHL', ftype, 0x02b0,
|
||||
self._emu_timestamp(),
|
||||
self.seconds_since_last_sync(),
|
||||
self.time_ofs,
|
||||
1, # offset 0x1a
|
||||
self.next_pkt_cnt()))
|
||||
self.ifc.tx_add(build_msg[0x20:])
|
||||
self._finish_send_msg()
|
||||
log_lvl = self.log_lvl.get(0x4210, logging.WARNING)
|
||||
self.ifc.tx_log(log_lvl, 'Send inv-data:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
'''
|
||||
Message handler methods
|
||||
'''
|
||||
def msg_response(self):
|
||||
'''handle a received response from the TSUN cloud'''
|
||||
logger.debug("EMU received rsp:")
|
||||
_, _, ts, hb = super().msg_response()
|
||||
logger.debug(f"EMU ts:{ts} hb:{hb}")
|
||||
self.hb_timeout = hb
|
||||
self.time_ofs = ts - self._emu_timestamp()
|
||||
self.hb_timer.start(self.hb_timeout)
|
||||
|
||||
def msg_unknown(self):
|
||||
'''counts a unknown or unexpected message from the TSUN cloud'''
|
||||
logger.warning(f"EMU Unknow Msg: ID:{int(self.control):#04x}")
|
||||
self.inc_counter('Unknown_Msg')
|
||||
@@ -4,22 +4,12 @@ import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import hex_dump_memory, Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from config import Config
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from cnf.config import Config
|
||||
from modbus import Modbus
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -50,13 +40,211 @@ class Sequence():
|
||||
return f'{self.rcv_idx:02x}:{self.snd_idx:02x}'
|
||||
|
||||
|
||||
class SolarmanV5(Message):
|
||||
class SolarmanBase(Message):
|
||||
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
|
||||
_send_modbus_cb, mb_timeout: int):
|
||||
super().__init__('G3P', ifc, server_side, _send_modbus_cb,
|
||||
mb_timeout)
|
||||
ifc.rx_set_cb(self.read)
|
||||
ifc.prot_set_timeout_cb(self._timeout)
|
||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||
ifc.prot_set_update_header_cb(self.__update_header)
|
||||
self.addr = addr
|
||||
self.conn_no = ifc.get_conn_no()
|
||||
self.header_len = 11 # overwrite construcor in class Message
|
||||
self.control = 0
|
||||
self.seq = Sequence(server_side)
|
||||
self.snr = 0
|
||||
self.time_ofs = 0
|
||||
|
||||
def read(self) -> float:
|
||||
'''process all received messages in the _recv_buffer'''
|
||||
self._read()
|
||||
while True:
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self.ifc.rx_peek(),
|
||||
self.ifc.rx_len())
|
||||
|
||||
if self.header_valid and self.ifc.rx_len() >= \
|
||||
(self.header_len + self.data_len+2):
|
||||
self.__process_complete_received_msg()
|
||||
self.__flush_recv_msg()
|
||||
else:
|
||||
return 0 # wait 0s before sending a response
|
||||
'''
|
||||
Our public methods
|
||||
'''
|
||||
def _flow_str(self, server_side: bool, type: str): # noqa: F821
|
||||
switch = {
|
||||
'rx': ' <',
|
||||
'tx': ' >',
|
||||
'forwrd': '<< ',
|
||||
'drop': ' xx',
|
||||
'rxS': '> ',
|
||||
'txS': '< ',
|
||||
'forwrdS': ' >>',
|
||||
'dropS': 'xx ',
|
||||
}
|
||||
if server_side:
|
||||
type += 'S'
|
||||
return switch.get(type, '???')
|
||||
|
||||
def get_fnc_handler(self, ctrl):
|
||||
fnc = self.switch.get(ctrl, self.msg_unknown)
|
||||
if callable(fnc):
|
||||
return fnc, repr(fnc.__name__)
|
||||
else:
|
||||
return self.msg_unknown, repr(fnc)
|
||||
|
||||
def _build_header(self, ctrl) -> None:
|
||||
'''build header for new transmit message'''
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
|
||||
self.ifc.tx_add(struct.pack(
|
||||
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr))
|
||||
_fnc, _str = self.get_fnc_handler(ctrl)
|
||||
logger.info(self._flow_str(self.server_side, 'tx') +
|
||||
f' Ctl: {int(ctrl):#04x} Msg: {_str}')
|
||||
|
||||
def _finish_send_msg(self) -> None:
|
||||
'''finish the transmit message, set lenght and checksum'''
|
||||
_len = self.ifc.tx_len() - self.send_msg_ofs
|
||||
struct.pack_into('<H', self.ifc.tx_peek(), self.send_msg_ofs+1,
|
||||
_len-11)
|
||||
check = sum(self.ifc.tx_peek()[
|
||||
self.send_msg_ofs+1:self.send_msg_ofs + _len]) & 0xff
|
||||
self.ifc.tx_add(struct.pack('<BB', check, 0x15)) # crc & stop
|
||||
|
||||
def _timestamp(self):
|
||||
# utc as epoche
|
||||
return int(time.time()) # pragma: no cover
|
||||
|
||||
def _emu_timestamp(self):
|
||||
'''timestamp for an emulated inverter (realtime - 1 day)'''
|
||||
one_day = 24*60*60
|
||||
return self._timestamp()-one_day
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __update_header(self, _forward_buffer):
|
||||
'''update header for message before forwarding,
|
||||
set sequence and checksum'''
|
||||
_len = len(_forward_buffer)
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('<BH', _forward_buffer, ofs)
|
||||
data_len = result[1] # len of variable id string
|
||||
|
||||
struct.pack_into('<H', _forward_buffer, ofs+5,
|
||||
self.seq.get_send())
|
||||
|
||||
check = sum(_forward_buffer[ofs+1:ofs+data_len+11]) & 0xff
|
||||
struct.pack_into('<B', _forward_buffer, ofs+data_len+11, check)
|
||||
ofs += (13 + data_len)
|
||||
|
||||
def __process_complete_received_msg(self):
|
||||
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:')
|
||||
# self._recv_buffer, self.header_len +
|
||||
# self.data_len+2)
|
||||
if self.__trailer_is_ok(self.ifc.rx_peek(), self.header_len
|
||||
+ self.data_len + 2):
|
||||
if self.state == State.init:
|
||||
self.state = State.received
|
||||
self._set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
|
||||
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||
|
||||
if (buf_len < self.header_len): # enough bytes for complete header?
|
||||
return
|
||||
|
||||
result = struct.unpack_from('<BHHHL', buf, 0)
|
||||
|
||||
# store parsed header values in the class
|
||||
start = result[0] # start byte
|
||||
self.data_len = result[1] # len of variable id string
|
||||
self.control = result[2]
|
||||
self.seq.set_recv(result[3])
|
||||
self.snr = result[4]
|
||||
|
||||
if start != 0xA5:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid start byte from'
|
||||
f' {self.addr}:', buf, buf_len)
|
||||
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
return
|
||||
self.header_valid = True
|
||||
|
||||
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
|
||||
crc = buf[self.data_len+11]
|
||||
stop = buf[self.data_len+12]
|
||||
if stop != 0x15:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid stop byte from '
|
||||
f'{self.addr}:', buf, buf_len)
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
if self.ifc.rx_len() > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
if next_start != 0xa5:
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
|
||||
return False
|
||||
|
||||
check = sum(buf[1:buf_len-2]) & 0xff
|
||||
if check != crc:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
|
||||
f' Stop:{int(stop):#02x}')
|
||||
# start & stop byte are valid, discard only this message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
self.ifc.rx_get(self.header_len + self.data_len+2)
|
||||
self.header_valid = False
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
_fnc, _str = self.get_fnc_handler(self.control)
|
||||
if self.unique_id:
|
||||
logger.info(self._flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {_str}')
|
||||
_fnc()
|
||||
else:
|
||||
logger.info(self._flow_str(self.server_side, 'drop') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {_str}')
|
||||
|
||||
'''
|
||||
Message handler methods
|
||||
'''
|
||||
def msg_response(self):
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<BBLL', data, 0)
|
||||
ftype = result[0] # always 2
|
||||
valid = result[1] == 1 # status
|
||||
ts = result[2]
|
||||
set_hb = result[3] # always 60 or 120
|
||||
logger.debug(f'ftype:{ftype} accepted:{valid}'
|
||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
return ftype, valid, ts, set_hb
|
||||
|
||||
|
||||
class SolarmanV5(SolarmanBase):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
MB_START_TIMEOUT = 40
|
||||
'''start delay for Modbus polling in server mode'''
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
'''regular Modbus polling time in server mode'''
|
||||
MB_CLIENT_DATA_UP = 30
|
||||
'''Data up time in client mode'''
|
||||
HDR_FMT = '<BLLL'
|
||||
@@ -64,24 +252,15 @@ class SolarmanV5(Message):
|
||||
|
||||
def __init__(self, addr, ifc: "AsyncIfc",
|
||||
server_side: bool, client_mode: bool):
|
||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=8)
|
||||
ifc.rx_set_cb(self.read)
|
||||
ifc.prot_set_timeout_cb(self._timeout)
|
||||
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
|
||||
ifc.prot_set_update_header_cb(self._update_header)
|
||||
super().__init__(addr, ifc, server_side, self.send_modbus_cb,
|
||||
mb_timeout=8)
|
||||
|
||||
self.addr = addr
|
||||
self.ifc = ifc
|
||||
self.conn_no = ifc.get_conn_no()
|
||||
self.header_len = 11 # overwrite construcor in class Message
|
||||
self.control = 0
|
||||
self.seq = Sequence(server_side)
|
||||
self.snr = 0
|
||||
self.db = InfosG3P(client_mode)
|
||||
self.time_ofs = 0
|
||||
self.forward_at_cmd_resp = False
|
||||
self.no_forwarding = False
|
||||
'''not allowed to connect to TSUN cloud by connection type'''
|
||||
self.establish_inv_emu = False
|
||||
'''create an Solarman EMU instance to send data to the TSUN cloud'''
|
||||
self.switch = {
|
||||
|
||||
0x4210: self.msg_data_ind, # real time data
|
||||
@@ -136,58 +315,44 @@ class SolarmanV5(Message):
|
||||
0x4510: logging.INFO, # from server
|
||||
0x1510: self.get_cmd_rsp_log_lvl,
|
||||
}
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
g3p_cnf = Config.get('gen3plus')
|
||||
|
||||
if 'at_acl' in g3p_cnf: # pragma: no cover
|
||||
self.at_acl = g3p_cnf['at_acl']
|
||||
|
||||
self.node_id = 'G3P' # will be overwritten in __set_serial_no
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
||||
'''timer value for next Modbus polling request'''
|
||||
self.modbus_polling = False
|
||||
self.sensor_list = 0x0000
|
||||
self.sensor_list = 0
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
logging.debug('Solarman.close()')
|
||||
if self.server_side:
|
||||
# set inverter state to offline, if output power is very low
|
||||
logging.debug('close power: '
|
||||
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
|
||||
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
|
||||
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
|
||||
self.new_data['env'] = True
|
||||
|
||||
# we have references to methods of this class in self.switch
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
self.log_lvl.clear()
|
||||
self.state = State.closed
|
||||
self.mb_timer.close()
|
||||
self.ifc.rx_set_cb(None)
|
||||
self.ifc.prot_set_timeout_cb(None)
|
||||
self.ifc.prot_set_init_new_client_conn_cb(None)
|
||||
self.ifc.prot_set_update_header_cb(None)
|
||||
self.ifc = None
|
||||
super().close()
|
||||
|
||||
async def send_start_cmd(self, snr: int, host: str,
|
||||
forward: bool,
|
||||
start_timeout=MB_CLIENT_DATA_UP):
|
||||
self.no_forwarding = True
|
||||
self.establish_inv_emu = forward
|
||||
self.snr = snr
|
||||
self.__set_serial_no(snr)
|
||||
self._set_serial_no(snr)
|
||||
self.mb_timeout = start_timeout
|
||||
self.db.set_db_def_value(Register.IP_ADDRESS, host)
|
||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||
self.mb_timeout)
|
||||
self.db.set_db_def_value(Register.DATA_UP_INTERVAL,
|
||||
300)
|
||||
self.db.set_db_def_value(Register.COLLECT_INTERVAL,
|
||||
1)
|
||||
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
||||
120)
|
||||
self.db.set_db_def_value(Register.SENSOR_LIST,
|
||||
Fmt.hex4((self.sensor_list, )))
|
||||
self.new_data['controller'] = True
|
||||
|
||||
self.state = State.up
|
||||
@@ -202,14 +367,25 @@ class SolarmanV5(Message):
|
||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||
self.mb_timeout)
|
||||
|
||||
def establish_emu(self):
|
||||
_len = 223
|
||||
build_msg = self.db.build(_len, 0x41, 2)
|
||||
struct.pack_into(
|
||||
'<BHHHLBL', build_msg, 0, 0xA5, _len-11, 0x4110,
|
||||
0, self.snr, 2, self._emu_timestamp())
|
||||
self.ifc.fwd_add(build_msg)
|
||||
self.ifc.fwd_add(struct.pack('<BB', 0, 0x15)) # crc & stop
|
||||
|
||||
def __set_config_parms(self, inv: dict):
|
||||
'''init connection with params from the configuration'''
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
self.modbus_polling = inv['modbus_polling']
|
||||
self.sensor_list = inv['sensor_list']
|
||||
if self.mb:
|
||||
self.mb.set_node_id(self.node_id)
|
||||
|
||||
def __set_serial_no(self, snr: int):
|
||||
def _set_serial_no(self, snr: int):
|
||||
'''check the serial number and configure the inverter connection'''
|
||||
serial_no = str(snr)
|
||||
if self.unique_id == serial_no:
|
||||
@@ -226,7 +402,8 @@ class SolarmanV5(Message):
|
||||
self.db.set_pv_module_details(inv)
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, key)
|
||||
self.db.set_db_def_value(Register.COLLECTOR_SNR, snr)
|
||||
self.db.set_db_def_value(Register.SERIAL_NUMBER, key)
|
||||
break
|
||||
else:
|
||||
self.node_id = ''
|
||||
@@ -240,35 +417,6 @@ class SolarmanV5(Message):
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
def read(self) -> float:
|
||||
'''process all received messages in the _recv_buffer'''
|
||||
self._read()
|
||||
while True:
|
||||
if not self.header_valid:
|
||||
self.__parse_header(self.ifc.rx_peek(),
|
||||
self.ifc.rx_len())
|
||||
|
||||
if self.header_valid and self.ifc.rx_len() >= \
|
||||
(self.header_len + self.data_len+2):
|
||||
self.__process_complete_received_msg()
|
||||
self.__flush_recv_msg()
|
||||
else:
|
||||
return 0 # wait 0s before sending a response
|
||||
|
||||
def __process_complete_received_msg(self):
|
||||
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||
if callable(log_lvl):
|
||||
log_lvl = log_lvl()
|
||||
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:')
|
||||
# self._recv_buffer, self.header_len +
|
||||
# self.data_len+2)
|
||||
if self.__trailer_is_ok(self.ifc.rx_peek(), self.header_len
|
||||
+ self.data_len + 2):
|
||||
if self.state == State.init:
|
||||
self.state = State.received
|
||||
self.__set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
|
||||
def forward(self, buffer, buflen) -> None:
|
||||
'''add the actual receive msg to the forwarding queue'''
|
||||
if self.no_forwarding:
|
||||
@@ -278,171 +426,37 @@ class SolarmanV5(Message):
|
||||
self.ifc.fwd_add(buffer[:buflen])
|
||||
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
|
||||
|
||||
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||
_, _str = self.get_fnc_handler(self.control)
|
||||
logger.info(self._flow_str(self.server_side, 'forwrd') +
|
||||
f' Ctl: {int(self.control):#04x}'
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
f' Msg: {_str}')
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
return False
|
||||
|
||||
'''
|
||||
Our private methods
|
||||
'''
|
||||
def __flow_str(self, server_side: bool, type: str): # noqa: F821
|
||||
switch = {
|
||||
'rx': ' <',
|
||||
'tx': ' >',
|
||||
'forwrd': '<< ',
|
||||
'drop': ' xx',
|
||||
'rxS': '> ',
|
||||
'txS': '< ',
|
||||
'forwrdS': ' >>',
|
||||
'dropS': 'xx ',
|
||||
}
|
||||
if server_side:
|
||||
type += 'S'
|
||||
return switch.get(type, '???')
|
||||
|
||||
def _timestamp(self):
|
||||
# utc as epoche
|
||||
return int(time.time()) # pragma: no cover
|
||||
|
||||
def _heartbeat(self) -> int:
|
||||
return 60 # pragma: no cover
|
||||
|
||||
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||
|
||||
if (buf_len < self.header_len): # enough bytes for complete header?
|
||||
return
|
||||
|
||||
result = struct.unpack_from('<BHHHL', buf, 0)
|
||||
|
||||
# store parsed header values in the class
|
||||
start = result[0] # start byte
|
||||
self.data_len = result[1] # len of variable id string
|
||||
self.control = result[2]
|
||||
self.seq.set_recv(result[3])
|
||||
self.snr = result[4]
|
||||
|
||||
if start != 0xA5:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid start byte from'
|
||||
f' {self.addr}:', buf, buf_len)
|
||||
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
return
|
||||
self.header_valid = True
|
||||
|
||||
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
|
||||
crc = buf[self.data_len+11]
|
||||
stop = buf[self.data_len+12]
|
||||
if stop != 0x15:
|
||||
hex_dump_memory(logging.ERROR,
|
||||
'Drop packet w invalid stop byte from '
|
||||
f'{self.addr}:', buf, buf_len)
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
if self.ifc.rx_len() > (self.data_len+13):
|
||||
next_start = buf[self.data_len+13]
|
||||
if next_start != 0xa5:
|
||||
# erase broken recv buffer
|
||||
self.ifc.rx_clear()
|
||||
|
||||
return False
|
||||
|
||||
check = sum(buf[1:buf_len-2]) & 0xff
|
||||
if check != crc:
|
||||
self.inc_counter('Invalid_Msg_Format')
|
||||
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
|
||||
f' Stop:{int(stop):#02x}')
|
||||
# start & stop byte are valid, discard only this message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __build_header(self, ctrl) -> None:
|
||||
'''build header for new transmit message'''
|
||||
self.send_msg_ofs = self.ifc.tx_len()
|
||||
|
||||
self.ifc.tx_add(struct.pack(
|
||||
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr))
|
||||
fnc = self.switch.get(ctrl, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'tx') +
|
||||
f' Ctl: {int(ctrl):#04x} Msg: {fnc.__name__!r}')
|
||||
|
||||
def __finish_send_msg(self) -> None:
|
||||
'''finish the transmit message, set lenght and checksum'''
|
||||
_len = self.ifc.tx_len() - self.send_msg_ofs
|
||||
struct.pack_into('<H', self.ifc.tx_peek(), self.send_msg_ofs+1,
|
||||
_len-11)
|
||||
check = sum(self.ifc.tx_peek()[
|
||||
self.send_msg_ofs+1:self.send_msg_ofs + _len]) & 0xff
|
||||
self.ifc.tx_add(struct.pack('<BB', check, 0x15)) # crc & stop
|
||||
|
||||
def _update_header(self, _forward_buffer):
|
||||
'''update header for message before forwarding,
|
||||
set sequence and checksum'''
|
||||
_len = len(_forward_buffer)
|
||||
ofs = 0
|
||||
while ofs < _len:
|
||||
result = struct.unpack_from('<BH', _forward_buffer, ofs)
|
||||
data_len = result[1] # len of variable id string
|
||||
|
||||
struct.pack_into('<H', _forward_buffer, ofs+5,
|
||||
self.seq.get_send())
|
||||
|
||||
check = sum(_forward_buffer[ofs+1:ofs+data_len+11]) & 0xff
|
||||
struct.pack_into('<B', _forward_buffer, ofs+data_len+11, check)
|
||||
ofs += (13 + data_len)
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||
if self.unique_id:
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
fnc()
|
||||
else:
|
||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||
f' Ctl: {int(self.control):#04x}' +
|
||||
f' Msg: {fnc.__name__!r}')
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
self.ifc.rx_get(self.header_len + self.data_len+2)
|
||||
self.header_valid = False
|
||||
|
||||
def __send_ack_rsp(self, msgtype, ftype, ack=1):
|
||||
self.__build_header(msgtype)
|
||||
self._build_header(msgtype)
|
||||
self.ifc.tx_add(struct.pack('<BBLL', ftype, ack,
|
||||
self._timestamp(),
|
||||
self._heartbeat()))
|
||||
self.__finish_send_msg()
|
||||
self._finish_send_msg()
|
||||
|
||||
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
self.__build_header(0x4510)
|
||||
self._build_header(0x4510)
|
||||
self.ifc.tx_add(struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||
self.sensor_list, 0, 0, 0))
|
||||
self.ifc.tx_add(pdu)
|
||||
self.__finish_send_msg()
|
||||
self._finish_send_msg()
|
||||
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
@@ -472,11 +486,11 @@ class SolarmanV5(Message):
|
||||
return
|
||||
|
||||
self.forward_at_cmd_resp = False
|
||||
self.__build_header(0x4510)
|
||||
self._build_header(0x4510)
|
||||
self.ifc.tx_add(struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
|
||||
0x0002, 0, 0, 0,
|
||||
at_cmd.encode('utf-8'), b'\r'))
|
||||
self.__finish_send_msg()
|
||||
self._finish_send_msg()
|
||||
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
|
||||
try:
|
||||
self.ifc.tx_flush()
|
||||
@@ -643,6 +657,18 @@ class SolarmanV5(Message):
|
||||
return
|
||||
self.__forward_msg()
|
||||
|
||||
def __parse_modbus_rsp(self, data):
|
||||
inv_update = False
|
||||
self.modbus_elms = 0
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, self._timestamp())
|
||||
self.new_data[key] = True
|
||||
return inv_update
|
||||
|
||||
def __modbus_command_rsp(self, data):
|
||||
'''precess MODBUS RTU response'''
|
||||
valid = data[1]
|
||||
@@ -650,19 +676,13 @@ class SolarmanV5(Message):
|
||||
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||
if valid == 1 and modbus_msg_len > 4:
|
||||
# logger.info(f'first byte modbus:{data[14]}')
|
||||
inv_update = False
|
||||
self.modbus_elms = 0
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
||||
self.node_id):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, self._timestamp())
|
||||
self.new_data[key] = True
|
||||
inv_update = self.__parse_modbus_rsp(data)
|
||||
if inv_update:
|
||||
self.__build_model_name()
|
||||
|
||||
if self.establish_inv_emu and not self.ifc.remote.stream:
|
||||
self.establish_emu()
|
||||
|
||||
def msg_hbeat_ind(self):
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<B', data, 0)
|
||||
@@ -684,16 +704,3 @@ class SolarmanV5(Message):
|
||||
|
||||
self.__forward_msg()
|
||||
self.__send_ack_rsp(0x1810, ftype)
|
||||
|
||||
def msg_response(self):
|
||||
data = self.ifc.rx_peek()[self.header_len:]
|
||||
result = struct.unpack_from('<BBLL', data, 0)
|
||||
ftype = result[0] # always 2
|
||||
valid = result[1] == 1 # status
|
||||
ts = result[2]
|
||||
set_hb = result[3] # always 60 or 120
|
||||
logger.debug(f'ftype:{ftype} accepted:{valid}'
|
||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
|
||||
242
app/src/infos.py
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Generator
|
||||
@@ -25,7 +26,11 @@ class Register(Enum):
|
||||
EQUIPMENT_MODEL = 24
|
||||
NO_INPUTS = 25
|
||||
MAX_DESIGNED_POWER = 26
|
||||
OUTPUT_COEFFICIENT = 27
|
||||
RATED_LEVEL = 27
|
||||
INPUT_COEFFICIENT = 28
|
||||
GRID_VOLT_CAL_COEF = 29
|
||||
OUTPUT_COEFFICIENT = 30
|
||||
PROD_COMPL_TYPE = 31
|
||||
INVERTER_CNT = 50
|
||||
UNKNOWN_SNR = 51
|
||||
UNKNOWN_MSG = 52
|
||||
@@ -38,10 +43,13 @@ class Register(Enum):
|
||||
AT_COMMAND = 59
|
||||
MODBUS_COMMAND = 60
|
||||
AT_COMMAND_BLOCKED = 61
|
||||
CLOUD_CONN_CNT = 62
|
||||
OUTPUT_POWER = 83
|
||||
RATED_POWER = 84
|
||||
INVERTER_TEMP = 85
|
||||
INVERTER_STATUS = 86
|
||||
DETECT_STATUS_1 = 87
|
||||
DETECT_STATUS_2 = 88
|
||||
PV1_VOLTAGE = 100
|
||||
PV1_CURRENT = 101
|
||||
PV1_POWER = 102
|
||||
@@ -84,6 +92,12 @@ class Register(Enum):
|
||||
PV5_TOTAL_GENERATION = 241
|
||||
PV6_DAILY_GENERATION = 250
|
||||
PV6_TOTAL_GENERATION = 251
|
||||
INV_UNKNOWN_1 = 252
|
||||
BOOT_STATUS = 253
|
||||
DSP_STATUS = 254
|
||||
WORK_MODE = 255
|
||||
OUTPUT_SHUTDOWN = 256
|
||||
|
||||
GRID_VOLTAGE = 300
|
||||
GRID_CURRENT = 301
|
||||
GRID_FREQUENCY = 302
|
||||
@@ -99,22 +113,11 @@ class Register(Enum):
|
||||
IP_ADDRESS = 407
|
||||
POLLING_INTERVAL = 408
|
||||
SENSOR_LIST = 409
|
||||
EVENT_401 = 500
|
||||
EVENT_402 = 501
|
||||
EVENT_403 = 502
|
||||
EVENT_404 = 503
|
||||
EVENT_405 = 504
|
||||
EVENT_406 = 505
|
||||
EVENT_407 = 506
|
||||
EVENT_408 = 507
|
||||
EVENT_409 = 508
|
||||
EVENT_410 = 509
|
||||
EVENT_411 = 510
|
||||
EVENT_412 = 511
|
||||
EVENT_413 = 512
|
||||
EVENT_414 = 513
|
||||
EVENT_415 = 514
|
||||
EVENT_416 = 515
|
||||
SSID = 410
|
||||
EVENT_ALARM = 500
|
||||
EVENT_FAULT = 501
|
||||
EVENT_BF1 = 502
|
||||
EVENT_BF2 = 503
|
||||
TS_INPUT = 600
|
||||
TS_GRID = 601
|
||||
TS_TOTAL = 602
|
||||
@@ -123,6 +126,76 @@ class Register(Enum):
|
||||
TEST_REG2 = 10001
|
||||
|
||||
|
||||
class Fmt:
|
||||
@staticmethod
|
||||
def get_value(buf: bytes, idx: int, row: dict):
|
||||
'''Get a value from buf and interpret as in row defined'''
|
||||
fmt = row['fmt']
|
||||
res = struct.unpack_from(fmt, buf, idx)
|
||||
result = res[0]
|
||||
if isinstance(result, (bytearray, bytes)):
|
||||
result = result.decode().split('\x00')[0]
|
||||
if 'func' in row:
|
||||
result = row['func'](res)
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
if 'quotient' in row:
|
||||
result = round(result/row['quotient'])
|
||||
if 'offset' in row:
|
||||
result = result + row['offset']
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def hex4(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
return f'{val[0]:04x}'
|
||||
else:
|
||||
return int(val, 16)
|
||||
|
||||
@staticmethod
|
||||
def mac(val: tuple | str, reverse=False) -> str | tuple:
|
||||
if not reverse:
|
||||
return "%02x:%02x:%02x:%02x:%02x:%02x" % val
|
||||
else:
|
||||
return (
|
||||
int(val[0:2], 16), int(val[3:5], 16),
|
||||
int(val[6:8], 16), int(val[9:11], 16),
|
||||
int(val[12:14], 16), int(val[15:], 16))
|
||||
|
||||
@staticmethod
|
||||
def version(val: tuple | str, reverse=False) -> str | int:
|
||||
if not reverse:
|
||||
x = val[0]
|
||||
return f'V{(x >> 12)}.{(x >> 8) & 0xf}' \
|
||||
f'.{(x >> 4) & 0xf}{x & 0xf:1X}'
|
||||
else:
|
||||
arr = val[1:].split('.')
|
||||
return int(arr[0], 10) << 12 | \
|
||||
int(arr[1], 10) << 8 | \
|
||||
int(arr[2][:-1], 10) << 4 | \
|
||||
int(arr[2][-1:], 16)
|
||||
|
||||
@staticmethod
|
||||
def set_value(buf: bytearray, idx: int, row: dict, val):
|
||||
'''Get a value from buf and interpret as in row defined'''
|
||||
fmt = row['fmt']
|
||||
if 'offset' in row:
|
||||
val = val - row['offset']
|
||||
if 'quotient' in row:
|
||||
val = round(val * row['quotient'])
|
||||
if 'ratio' in row:
|
||||
val = round(val / row['ratio'])
|
||||
if 'func' in row:
|
||||
val = row['func'](val, reverse=True)
|
||||
if isinstance(val, str):
|
||||
val = bytes(val, 'UTF8')
|
||||
|
||||
if isinstance(val, tuple):
|
||||
struct.pack_into(fmt, buf, idx, *val)
|
||||
else:
|
||||
struct.pack_into(fmt, buf, idx, val)
|
||||
|
||||
|
||||
class ClrAtMidnight:
|
||||
__clr_at_midnight = [Register.PV1_DAILY_GENERATION, Register.PV2_DAILY_GENERATION, Register.PV3_DAILY_GENERATION, Register.PV4_DAILY_GENERATION, Register.PV5_DAILY_GENERATION, Register.PV6_DAILY_GENERATION, Register.DAILY_GENERATION] # noqa: E501
|
||||
db = {}
|
||||
@@ -203,6 +276,7 @@ class Infos:
|
||||
}
|
||||
|
||||
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
||||
__work_mode_val_tpl = "{%set mode = ['Normal-Mode', 'Aging-Mode', 'ATE-Mode', 'Shielding GFDI', 'DTU-Mode'] %}{{mode[value_json['Work_Mode']|int(0)]|default(value_json['Work_Mode'])}}" # noqa: E501
|
||||
__status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
|
||||
__rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
__designed_power_val_tpl = '''
|
||||
@@ -217,6 +291,100 @@ class Infos:
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_alarm_val_tpl = '''
|
||||
{% if 'Inverter_Alarm' in value_json and
|
||||
value_json['Inverter_Alarm'] != None %}
|
||||
{% set val_int = value_json['Inverter_Alarm'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noAlarm'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'noUtility, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
__inv_fault_val_tpl = '''
|
||||
{% if 'Inverter_Fault' in value_json and
|
||||
value_json['Inverter_Fault'] != None %}
|
||||
{% set val_int = value_json['Inverter_Fault'] | int %}
|
||||
{% if val_int == 0 %}
|
||||
{% set result = 'noFault'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'Bit9, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
{% else %}
|
||||
{{ this.state }}
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
__input_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Input_Coefficient'] != None %}{{value_json['Input_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
__output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
|
||||
|
||||
__info_defs = {
|
||||
@@ -239,6 +407,8 @@ class Infos:
|
||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': 'mdi:power', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INPUT_COEFFICIENT: {'name': ['inverter', 'Input_Coefficient'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'input_coef_', 'val_tpl': __input_coef_val_tpl, 'name': 'Input Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
@@ -252,9 +422,11 @@ class Infos:
|
||||
Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MANUFACTURER: {'name': ['inverter', 'PV6_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.BOOT_STATUS: {'name': ['inverter', 'BOOT_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DSP_STATUS: {'name': ['inverter', 'DSP_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
# proxy:
|
||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
|
||||
Register.CLOUD_CONN_CNT: {'name': ['proxy', 'Cloud_Conn_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'cloud_conn_count_', 'fmt': FMT_INT, 'name': 'Active Cloud Connections', 'icon': COUNTER}}, # noqa: E501
|
||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': FMT_INT, 'name': 'Unknown Msg Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': FMT_INT, 'name': 'Invalid Data Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
@@ -269,22 +441,12 @@ class Infos:
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
||||
|
||||
# events
|
||||
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_411: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_413: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_414: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_ALARM: {'name': ['events', 'Inverter_Alarm'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_alarm_', 'name': 'Inverter Alarm', 'val_tpl': __inv_alarm_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_FAULT: {'name': ['events', 'Inverter_Fault'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_fault_', 'name': 'Inverter Fault', 'val_tpl': __inv_fault_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501
|
||||
Register.EVENT_BF1: {'name': ['events', 'Inverter_Bitfield_1'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.EVENT_BF2: {'name': ['events', 'Inverter_bitfield_2'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
# Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# grid measures:
|
||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -294,6 +456,8 @@ class Infos:
|
||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
||||
Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# input measures:
|
||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
@@ -343,6 +507,14 @@ class Infos:
|
||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.SSID: {'name': ['controller', 'WiFi_SSID'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
Register.OUTPUT_SHUTDOWN: {'name': ['other', 'Output_Shutdown'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.RATED_LEVEL: {'name': ['other', 'Rated_Level'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.GRID_VOLT_CAL_COEF: {'name': ['other', 'Grid_Volt_Cal_Coef'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PROD_COMPL_TYPE: {'name': ['other', 'Prod_Compliance_Type'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -652,6 +824,8 @@ class Infos:
|
||||
|
||||
def get_db_value(self, id: Register, not_found_result: any = None):
|
||||
'''get database value'''
|
||||
if id not in self.info_defs:
|
||||
return not_found_result
|
||||
row = self.info_defs[id]
|
||||
if isinstance(row, dict):
|
||||
keys = row['name']
|
||||
|
||||
@@ -7,22 +7,13 @@ import gc
|
||||
from aiomqtt import MqttCodeError
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
if __name__ == "app.src.inverter_base":
|
||||
from app.src.inverter_ifc import InverterIfc
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.async_stream import StreamPtr
|
||||
from app.src.async_stream import AsyncStreamClient
|
||||
from app.src.async_stream import AsyncStreamServer
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from inverter_ifc import InverterIfc
|
||||
from proxy import Proxy
|
||||
from async_stream import StreamPtr
|
||||
from async_stream import AsyncStreamClient
|
||||
from async_stream import AsyncStreamServer
|
||||
from config import Config
|
||||
from infos import Infos
|
||||
from inverter_ifc import InverterIfc
|
||||
from proxy import Proxy
|
||||
from async_stream import StreamPtr
|
||||
from async_stream import AsyncStreamClient
|
||||
from async_stream import AsyncStreamServer
|
||||
from cnf.config import Config
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
@@ -31,12 +22,16 @@ class InverterBase(InverterIfc, Proxy):
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||
config_id: str, prot_class,
|
||||
client_mode: bool = False):
|
||||
client_mode: bool = False,
|
||||
remote_prot_class=None):
|
||||
Proxy.__init__(self)
|
||||
self._registry.append(weakref.ref(self))
|
||||
self.addr = writer.get_extra_info('peername')
|
||||
self.config_id = config_id
|
||||
self.prot_class = prot_class
|
||||
if remote_prot_class:
|
||||
self.prot_class = remote_prot_class
|
||||
else:
|
||||
self.prot_class = prot_class
|
||||
self.__ha_restarts = -1
|
||||
self.remote = StreamPtr(None)
|
||||
ifc = AsyncStreamServer(reader, writer,
|
||||
@@ -45,7 +40,7 @@ class InverterBase(InverterIfc, Proxy):
|
||||
self.remote)
|
||||
|
||||
self.local = StreamPtr(
|
||||
self.prot_class(self.addr, ifc, True, client_mode), ifc
|
||||
prot_class(self.addr, ifc, True, client_mode), ifc
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
@@ -2,10 +2,7 @@ from abc import abstractmethod
|
||||
import logging
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
if __name__ == "app.src.inverter_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -3,15 +3,11 @@ import weakref
|
||||
from typing import Callable
|
||||
from enum import Enum
|
||||
|
||||
|
||||
if __name__ == "app.src.messages":
|
||||
from app.src.protocol_ifc import ProtocolIfc
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
else: # pragma: no cover
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from async_ifc import AsyncIfc
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -89,26 +85,38 @@ class Message(ProtocolIfc):
|
||||
'''maximum time without a received msg from the inverter in sec'''
|
||||
MAX_DEF_IDLE_TIME = 360
|
||||
'''maximum default time without a received msg in sec'''
|
||||
MB_START_TIMEOUT = 40
|
||||
'''start delay for Modbus polling in server mode'''
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
'''regular Modbus polling time in server mode'''
|
||||
|
||||
def __init__(self, server_side: bool, send_modbus_cb:
|
||||
Callable[[bytes, int, str], None], mb_timeout: int):
|
||||
def __init__(self, node_id, ifc: "AsyncIfc", server_side: bool,
|
||||
send_modbus_cb: Callable[[bytes, int, str], None],
|
||||
mb_timeout: int):
|
||||
self._registry.append(weakref.ref(self))
|
||||
|
||||
self.server_side = server_side
|
||||
self.ifc = ifc
|
||||
self.node_id = node_id
|
||||
if server_side:
|
||||
self.mb = Modbus(send_modbus_cb, mb_timeout)
|
||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||
else:
|
||||
self.mb = None
|
||||
|
||||
self.mb_timer = None
|
||||
self.header_valid = False
|
||||
self.header_len = 0
|
||||
self.data_len = 0
|
||||
self.unique_id = 0
|
||||
self._node_id = ''
|
||||
self.sug_area = ''
|
||||
self.new_data = {}
|
||||
self.state = State.init
|
||||
self.shutdown_started = False
|
||||
self.modbus_elms = 0 # for unit tests
|
||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
||||
'''timer value for next Modbus polling request'''
|
||||
self.modbus_polling = False
|
||||
|
||||
@property
|
||||
def node_id(self):
|
||||
@@ -152,10 +160,35 @@ class Message(ProtocolIfc):
|
||||
to = self.MAX_DEF_IDLE_TIME
|
||||
return to
|
||||
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def close(self) -> None:
|
||||
if self.server_side:
|
||||
# set inverter state to offline, if output power is very low
|
||||
logging.debug('close power: '
|
||||
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
|
||||
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
|
||||
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
|
||||
self.new_data['env'] = True
|
||||
self.mb_timer.close()
|
||||
self.state = State.closed
|
||||
self.ifc.rx_set_cb(None)
|
||||
self.ifc.prot_set_timeout_cb(None)
|
||||
self.ifc.prot_set_init_new_client_conn_cb(None)
|
||||
self.ifc.prot_set_update_header_cb(None)
|
||||
self.ifc = None
|
||||
|
||||
if self.mb:
|
||||
self.mb.close()
|
||||
self.mb = None
|
||||
|
||||
@@ -16,10 +16,7 @@ import logging
|
||||
import asyncio
|
||||
from typing import Generator, Callable
|
||||
|
||||
if __name__ == "app.src.modbus":
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from infos import Register
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
@@ -40,15 +37,30 @@ class Modbus():
|
||||
|
||||
__crc_tab = []
|
||||
mb_reg_mapping = {
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||
0x2006: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2008: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
|
||||
0x2009: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x200a: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x2010: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
|
||||
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf:1X}'"}, # noqa: E501
|
||||
0x3001: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3002: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
|
||||
0x3003: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
|
||||
0x3004: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
|
||||
0x3005: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
|
||||
0x3006: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
|
||||
|
||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
|
||||
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
|
||||
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
|
||||
# 0x300d
|
||||
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||
@@ -74,6 +86,7 @@ class Modbus():
|
||||
0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||
# 0x302a
|
||||
}
|
||||
|
||||
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
|
||||
@@ -117,6 +130,9 @@ class Modbus():
|
||||
while not self.que.empty():
|
||||
self.que.get_nowait()
|
||||
|
||||
def set_node_id(self, node_id: str):
|
||||
self.node_id = node_id
|
||||
|
||||
def build_msg(self, addr: int, func: int, reg: int, val: int,
|
||||
log_lvl=logging.DEBUG) -> None:
|
||||
"""Build MODBUS RTU request frame and add it to the tx queue
|
||||
@@ -160,14 +176,13 @@ class Modbus():
|
||||
|
||||
return True
|
||||
|
||||
def recv_resp(self, info_db, buf: bytes, node_id: str) -> \
|
||||
def recv_resp(self, info_db, buf: bytes) -> \
|
||||
Generator[tuple[str, bool, int | float | str], None, None]:
|
||||
"""Generator which check and parse a received MODBUS response.
|
||||
|
||||
Keyword arguments:
|
||||
info_db: database for info lockups
|
||||
buf: received Modbus RTU response frame
|
||||
node_id: string for logging which identifies the slave
|
||||
|
||||
Returns on error and set Self.err to:
|
||||
1: CRC error
|
||||
@@ -177,7 +192,6 @@ class Modbus():
|
||||
5: No MODBUS request pending
|
||||
"""
|
||||
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||
self.node_id = node_id
|
||||
|
||||
fcode = buf[1]
|
||||
data_available = self.last_addr == self.INV_ADDR and \
|
||||
@@ -228,17 +242,6 @@ class Modbus():
|
||||
|
||||
return False
|
||||
|
||||
def __get_value(self, buf: bytes, idx: int, row: dict):
|
||||
'''get a value from the received buffer'''
|
||||
val = struct.unpack_from(row['fmt'], buf, idx)
|
||||
result = val[0]
|
||||
|
||||
if 'eval' in row:
|
||||
result = eval(row['eval'])
|
||||
if 'ratio' in row:
|
||||
result = round(result * row['ratio'], 2)
|
||||
return result
|
||||
|
||||
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
|
||||
'''Generator over received registers, updates the db'''
|
||||
for i in range(0, elmlen):
|
||||
@@ -248,7 +251,7 @@ class Modbus():
|
||||
info_id = row['reg']
|
||||
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||
if keys:
|
||||
result = self.__get_value(buf, 3+2*i, row)
|
||||
result = Fmt.get_value(buf, 3+2*i, row)
|
||||
name, update = info_db.update_db(keys, must_incr,
|
||||
result)
|
||||
yield keys[0], update, result
|
||||
|
||||
@@ -2,14 +2,9 @@ import logging
|
||||
import traceback
|
||||
import asyncio
|
||||
|
||||
if __name__ == "app.src.modbus_tcp":
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
@@ -57,15 +52,17 @@ class ModbusTcp():
|
||||
# logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
|
||||
loop.create_task(self.modbus_loop(client['host'],
|
||||
client['port'],
|
||||
inv['monitor_sn']))
|
||||
inv['monitor_sn'],
|
||||
client['forward']))
|
||||
|
||||
async def modbus_loop(self, host, port, snr: int) -> None:
|
||||
async def modbus_loop(self, host, port,
|
||||
snr: int, forward: bool) -> None:
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
while True:
|
||||
try:
|
||||
async with ModbusConn(host, port) as inverter:
|
||||
stream = inverter.local.stream
|
||||
await stream.send_start_cmd(snr, host)
|
||||
await stream.send_start_cmd(snr, host, forward)
|
||||
await stream.ifc.loop()
|
||||
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||
f'Connection closed - Shutdown: '
|
||||
|
||||
@@ -2,16 +2,11 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
if __name__ == "app.src.mqtt":
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import Message
|
||||
from app.src.config import Config
|
||||
from app.src.singleton import Singleton
|
||||
else: # pragma: no cover
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from config import Config
|
||||
from singleton import Singleton
|
||||
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from cnf.config import Config
|
||||
from singleton import Singleton
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
if __name__ == "app.src.protocol_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
from async_ifc import AsyncIfc
|
||||
from async_ifc import AsyncIfc
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
|
||||
class ProtocolIfc(metaclass=AbstractIterMeta):
|
||||
|
||||
@@ -2,14 +2,9 @@ import asyncio
|
||||
import logging
|
||||
import json
|
||||
|
||||
if __name__ == "app.src.proxy":
|
||||
from app.src.config import Config
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import asyncio
|
||||
import signal
|
||||
import os
|
||||
import argparse
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
@@ -10,7 +11,10 @@ from inverter_ifc import InverterIfc
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from scheduler import Schedule
|
||||
from config import Config
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
@@ -116,6 +120,8 @@ def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
corresponding logging.LOG_LEVEL'''
|
||||
log_level = os.getenv('LOG_LVL', 'INFO')
|
||||
logging.info(f"LOG_LVL : {log_level}")
|
||||
|
||||
if log_level == 'DEBUG':
|
||||
log_level = logging.DEBUG
|
||||
elif log_level == 'WARN':
|
||||
@@ -125,7 +131,17 @@ def get_log_level() -> int:
|
||||
return log_level
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-p', '--config_path', type=str,
|
||||
default='./config/',
|
||||
help='set path for the configuration files')
|
||||
parser.add_argument('-j', '--json_config', type=str,
|
||||
help='read user config from json-file')
|
||||
parser.add_argument('-t', '--toml_config', type=str,
|
||||
help='read user config from toml-file')
|
||||
parser.add_argument('--add_on', action='store_true')
|
||||
args = parser.parse_args()
|
||||
#
|
||||
# Setup our daily, rotating logger
|
||||
#
|
||||
@@ -134,9 +150,14 @@ if __name__ == "__main__":
|
||||
|
||||
logging.config.fileConfig('logging.ini')
|
||||
logging.info(f'Server "{serv_name} - {version}" will be started')
|
||||
logging.info(f"AddOn: {args.add_on}")
|
||||
logging.info(f"config_path: {args.config_path}")
|
||||
logging.info(f"json_config: {args.json_config}")
|
||||
logging.info(f"toml_config: {args.toml_config}")
|
||||
log_level = get_log_level()
|
||||
logging.info('******')
|
||||
|
||||
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
||||
log_level = get_log_level()
|
||||
logging.getLogger().setLevel(log_level)
|
||||
logging.getLogger('msg').setLevel(log_level)
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
@@ -149,9 +170,18 @@ if __name__ == "__main__":
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# read config file
|
||||
ConfigErr = Config.class_init()
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
ConfigReadEnv()
|
||||
ConfigReadJson(args.config_path + "config.json")
|
||||
ConfigReadToml(args.config_path + "config.toml")
|
||||
ConfigReadJson(args.json_config)
|
||||
ConfigReadToml(args.toml_config)
|
||||
ConfigErr = Config.get_error()
|
||||
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
logging.info('******')
|
||||
|
||||
Proxy.class_init()
|
||||
Schedule.start()
|
||||
ModbusTcp(loop)
|
||||
|
||||
@@ -4,12 +4,13 @@ import asyncio
|
||||
import gc
|
||||
import time
|
||||
|
||||
from app.src.infos import Infos
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from app.src.messages import Message
|
||||
from app.tests.test_modbus_tcp import FakeReader, FakeWriter
|
||||
from app.tests.test_inverter_base import config_conn, patch_open_connection
|
||||
from infos import Infos
|
||||
from inverter_base import InverterBase
|
||||
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from messages import Message
|
||||
|
||||
from test_modbus_tcp import FakeReader, FakeWriter
|
||||
from test_inverter_base import config_conn, patch_open_connection
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -17,10 +18,13 @@ pytest_plugins = ('pytest_asyncio',)
|
||||
Infos.static_init()
|
||||
|
||||
class FakeProto(Message):
|
||||
def __init__(self, server_side):
|
||||
super().__init__(server_side, None, 10)
|
||||
def __init__(self, ifc, server_side):
|
||||
super().__init__('G3F', ifc, server_side, None, 10)
|
||||
self.conn_no = 0
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
pass # empty callback
|
||||
|
||||
def fake_reader_fwd():
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
@@ -337,6 +341,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False):
|
||||
elif test_type == TestType.FWD_RUNTIME_ERROR_NO_STREAM:
|
||||
remote.stream = None
|
||||
raise RuntimeError("Peer closed")
|
||||
return True
|
||||
|
||||
def close():
|
||||
return
|
||||
@@ -349,7 +354,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False):
|
||||
FakeReader(), FakeWriter(), StreamPtr(None), close_hndl)
|
||||
remote.ifc.prot_set_update_header_cb(update_hdr)
|
||||
remote.ifc.prot_set_init_new_client_conn_cb(callback)
|
||||
remote.stream = FakeProto(False)
|
||||
remote.stream = FakeProto(remote.ifc, False)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward():
|
||||
@@ -530,3 +535,39 @@ async def test_forward_runtime_error3():
|
||||
await ifc.server_loop()
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_resp():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb)
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_resp2():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb)
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# test_with_pytest.py
|
||||
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
from byte_fifo import ByteFifo
|
||||
|
||||
def test_fifo():
|
||||
read = ByteFifo()
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
# test_with_pytest.py
|
||||
import tomllib
|
||||
import pytest
|
||||
import json
|
||||
from mock import patch
|
||||
from schema import SchemaMissingKeyError
|
||||
from app.src.config import Config
|
||||
from cnf.config import Config, ConfigIfc
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
class TstConfig(Config):
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
return self.buf.rd.copy()
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="rb"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
class TstConfig(ConfigIfc):
|
||||
|
||||
@classmethod
|
||||
def set(cls, cnf):
|
||||
def __init__(cls, cnf):
|
||||
cls.act_config = cnf
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict:
|
||||
def add_config(cls) -> dict:
|
||||
return cls.act_config
|
||||
|
||||
|
||||
@@ -22,82 +62,9 @@ def test_empty_config():
|
||||
except SchemaMissingKeyError:
|
||||
pass
|
||||
|
||||
def test_default_config():
|
||||
with open("app/config/default_config.toml", "rb") as f:
|
||||
cnf = tomllib.load(f)
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688}}}
|
||||
|
||||
def test_full_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {'modbus_polling': True, 'node_id': '', 'sensor_list': 0, 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'sensor_list': 0x1511, 'suggested_area': ''}}}
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': '', 'sensor_list': 0}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 5393}}}
|
||||
|
||||
def test_mininum_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {}}
|
||||
}
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}}}
|
||||
|
||||
def test_read_empty():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
@pytest.fixture
|
||||
def ConfigDefault():
|
||||
return {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
@@ -128,27 +95,150 @@ def test_read_empty():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigComplete():
|
||||
return {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}
|
||||
}
|
||||
},
|
||||
'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant',
|
||||
'discovery_prefix': 'homeassistant',
|
||||
'entity_prefix': 'tsun',
|
||||
'proxy_node_id': 'proxy',
|
||||
'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {'node_id': 'PV-Garage/',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'suggested_area': 'Garage',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'pv3': {'manufacturer': 'man3',
|
||||
'type': 'type3'},
|
||||
'pv4': {'manufacturer': 'man4',
|
||||
'type': 'type4'},
|
||||
'suggested_area': 'Garage2',
|
||||
'sensor_list': 688}
|
||||
}
|
||||
}
|
||||
|
||||
def test_default_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
validated = Config.def_config
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688}}}
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': ['AT+SUPDATE']}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': False,
|
||||
'R170000000000001': {'modbus_polling': False, 'node_id': 'PV-Garage/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}}}}
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == ConfigComplete
|
||||
|
||||
def test_read_empty(ConfigDefault):
|
||||
test_buffer.rd = ""
|
||||
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_no_file():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('')
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
err = Config.get_error()
|
||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == None
|
||||
|
||||
def test_read_cnf1():
|
||||
cnf = {'solarman' : {'enabled': False}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
def test_no_file2():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
|
||||
def test_invalid_filename():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml(None)
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_read_cnf1():
|
||||
test_buffer.rd = "solarman.enabled = false"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -180,18 +270,22 @@ def test_read_cnf1():
|
||||
}
|
||||
}
|
||||
}
|
||||
cnf = TstConfig.get('solarman')
|
||||
cnf = Config.get('solarman')
|
||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf2():
|
||||
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.enabled = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -223,22 +317,30 @@ def test_read_cnf2():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf3():
|
||||
cnf = {'solarman' : {'port': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'solarman': {'port': 'FALSE'}}
|
||||
def test_read_cnf3(ConfigDefault):
|
||||
test_buffer.rd = "solarman.port = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_read_cnf4():
|
||||
cnf = {'solarman' : {'port': 5000}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 5000"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -270,16 +372,22 @@ def test_read_cnf4():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf5():
|
||||
cnf = {'solarman' : {'port': 1023}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 1023"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
def test_read_cnf6():
|
||||
cnf = {'solarman' : {'port': 65536}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
test_buffer.rd = "solarman.port = 65536"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
53
app/tests/test_config_read_env.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import os
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
|
||||
def patch_getenv():
|
||||
def new_getenv(key: str, defval=None):
|
||||
"""Get an environment variable, return None if it doesn't exist.
|
||||
The optional second argument can specify an alternate default. key,
|
||||
default and the result are str."""
|
||||
if key == 'MQTT_PASSWORD':
|
||||
return 'passwd'
|
||||
elif key == 'MQTT_PORT':
|
||||
return 1234
|
||||
elif key == 'MQTT_HOST':
|
||||
return ""
|
||||
return defval
|
||||
|
||||
with patch.object(os, 'getenv', new_getenv) as conn:
|
||||
yield conn
|
||||
|
||||
def test_extend_key():
|
||||
cnf_rd = ConfigReadEnv()
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt.user", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': {
|
||||
'user': 'testuser',
|
||||
},
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': 'testuser',
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "", "testuser")
|
||||
assert conf == {'': 'testuser'}
|
||||
|
||||
def test_read_env_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}
|
||||
for _ in patch_getenv():
|
||||
|
||||
ConfigReadEnv()
|
||||
assert Config.get_error() == None
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1234, 'user': None, 'passwd': 'passwd'}
|
||||
404
app/tests/test_config_read_json.py
Normal file
@@ -0,0 +1,404 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
from test_config import ConfigDefault, ConfigComplete
|
||||
|
||||
|
||||
class CnfIfc(ConfigReadJson):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
wr = str()
|
||||
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
print(f"Fake.read: bmode:{self.bin_mode}")
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
print(f"Fake.read: str:{self.buf.rd}")
|
||||
return self.buf.rd
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="r"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigTomlEmpty():
|
||||
return {
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant',
|
||||
'discovery_prefix': 'homeassistant',
|
||||
'entity_prefix': 'tsun',
|
||||
'proxy_node_id': 'proxy',
|
||||
'proxy_unique_id': 'P170000000000001'},
|
||||
'solarman': {
|
||||
'enabled': True,
|
||||
'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000,
|
||||
},
|
||||
'tsun': {
|
||||
'enabled': True,
|
||||
'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005,
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': False
|
||||
},
|
||||
'gen3plus': {'at_acl': {'tsun': {'allow': [], 'block': []},
|
||||
'mqtt': {'allow': [], 'block': []}}},
|
||||
}
|
||||
|
||||
|
||||
def test_no_config(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Expecting value: line 1 column 1 (char 0)'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_no_file(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_invalid_filename(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson(None)
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_cnv1():
|
||||
"""test dotted key converting"""
|
||||
tst = {
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE",
|
||||
"AT+"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {
|
||||
'block': [
|
||||
'AT+SUPDATE',
|
||||
"AT+"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv2():
|
||||
"""test a valid list with serials in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv3():
|
||||
"""test the combination of a list and a scalar in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
"inverters.allow_all": False,
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {},
|
||||
'allow_all': False,
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv4():
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": False,
|
||||
"pv1_manufacturer": "man1",
|
||||
"pv1_type": "type1",
|
||||
"pv2_manufacturer": "man2",
|
||||
"pv2_type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": True,
|
||||
"client_mode_host": "InverterIP",
|
||||
"client_mode_port": 1234,
|
||||
"pv1_manufacturer": "man1",
|
||||
"pv1_type": "type1",
|
||||
"pv2_manufacturer": "man2",
|
||||
"pv2_type": "type2",
|
||||
"pv3_manufacturer": "man3",
|
||||
"pv3_type": "type3",
|
||||
"pv4_manufacturer": "man4",
|
||||
"pv4_type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": True,
|
||||
"solarman.enabled": True,
|
||||
"inverters.allow_all": False,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}}},
|
||||
'inverters': {'R170000000000001': {'modbus_polling': False,
|
||||
'node_id': 'PV-Garage/',
|
||||
'pv1_manufacturer': 'man1',
|
||||
'pv1_type': 'type1',
|
||||
'pv2_manufacturer': 'man2',
|
||||
'pv2_type': 'type2',
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage'},
|
||||
'Y170000000000001': {'client_mode_host': 'InverterIP',
|
||||
'client_mode_port': 1234,
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1_manufacturer': 'man1',
|
||||
'pv1_type': 'type1',
|
||||
'pv2_manufacturer': 'man2',
|
||||
'pv2_type': 'type2',
|
||||
'pv3_manufacturer': 'man3',
|
||||
'pv3_type': 'type3',
|
||||
'pv4_manufacturer': 'man4',
|
||||
'pv4_type': 'type4',
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage2'},
|
||||
'allow_all': False},
|
||||
'solarman': {'enabled': True},
|
||||
'tsun': {'enabled': True}
|
||||
}
|
||||
|
||||
def test_cnv5():
|
||||
"""test a invalid list with missing serials"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"node_id": "PV-Garage1/",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}
|
||||
],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv6():
|
||||
"""test overwritting a value in inverters"""
|
||||
tst = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}],
|
||||
}
|
||||
tst2 = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garden/",
|
||||
}],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
conf = {}
|
||||
for key, val in tst.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, val in tst2.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garden/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_empty_config(ConfigDefault):
|
||||
test_buffer.rd = "{}" # empty json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
test_buffer.rd = """
|
||||
{
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": false,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": true,
|
||||
"client_mode_host": "InverterIP",
|
||||
"client_mode_port": 1234,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": true,
|
||||
"solarman.enabled": true,
|
||||
"inverters.allow_all": false,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
"""
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigComplete
|
||||
@@ -2,8 +2,8 @@
|
||||
import pytest
|
||||
import json, math
|
||||
import logging
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.infos import Infos
|
||||
from infos import Register, ClrAtMidnight
|
||||
from infos import Infos, Fmt
|
||||
|
||||
def test_statistic_counter():
|
||||
i = Infos()
|
||||
@@ -17,13 +17,13 @@ def test_statistic_counter():
|
||||
assert val == None or val == 0
|
||||
|
||||
i.static_init() # initialize counter
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
|
||||
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||
assert val == 0
|
||||
|
||||
i.inc_counter('Inverter_Cnt')
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
val = i.dev_value(Register.INVERTER_CNT)
|
||||
assert val == 1
|
||||
|
||||
@@ -256,3 +256,24 @@ def test_key_obj():
|
||||
assert level == logging.DEBUG
|
||||
assert unit == 'kWh'
|
||||
assert must_incr == True
|
||||
|
||||
def test_hex4_cnv():
|
||||
tst_val = (0x12ef, )
|
||||
string = Fmt.hex4(tst_val)
|
||||
assert string == '12ef'
|
||||
val = Fmt.hex4(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
def test_mac_cnv():
|
||||
tst_val = (0x12, 0x34, 0x67, 0x89, 0xcd, 0xef)
|
||||
string = Fmt.mac(tst_val)
|
||||
assert string == '12:34:67:89:cd:ef'
|
||||
val = Fmt.mac(string, reverse=True)
|
||||
assert val == tst_val
|
||||
|
||||
def test_version_cnv():
|
||||
tst_val = (0x123f, )
|
||||
string = Fmt.version(tst_val)
|
||||
assert string == 'V1.2.3F'
|
||||
val = Fmt.version(string, reverse=True)
|
||||
assert val == tst_val[0]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, json, math
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
|
||||
from infos import Register
|
||||
from gen3.infos_g3 import InfosG3, RegisterMap
|
||||
|
||||
@pytest.fixture
|
||||
def contr_data_seq(): # Get Time Request message
|
||||
@@ -421,7 +421,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||
if key == 'total' or key == 'inverter' or key == 'env':
|
||||
assert update == True
|
||||
tests +=1
|
||||
assert tests==8
|
||||
assert tests==12
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||
@@ -435,7 +435,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "Max_Designed_Power": -1, "Output_Coefficient": 100.0, "No_Inputs": 2})
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": -1, "Input_Coefficient": -0.1, "Output_Coefficient": 100.0, "No_Inputs": 2})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (inv_data_seq2_zero):
|
||||
@@ -501,10 +501,10 @@ def test_new_data_types(inv_data_new):
|
||||
else:
|
||||
assert False
|
||||
|
||||
assert tests==15
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0})
|
||||
assert tests==7
|
||||
assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0, "DSP_STATUS": 0})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
||||
assert json.dumps(i.db['events']) == json.dumps({"Inverter_Alarm": 0, "Inverter_Fault": 0})
|
||||
|
||||
def test_invalid_data_type(invalid_data_seq):
|
||||
i = InfosG3()
|
||||
@@ -520,15 +520,3 @@ def test_invalid_data_type(invalid_data_seq):
|
||||
|
||||
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||
assert val == 1
|
||||
|
||||
def test_result_eval(inv_data_seq2: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8'
|
||||
|
||||
i = InfosG3()
|
||||
|
||||
for _, _ in i.parse (inv_data_seq2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
del RegisterMap.map[0x00000514]['eval'] # remove eval
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
# test_with_pytest.py
|
||||
import pytest, json, math, random
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.gen3plus.infos_g3p import RegisterMap
|
||||
from infos import Register
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from gen3plus.infos_g3p import RegisterMap
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def str_test_ip():
|
||||
@@ -57,6 +57,7 @@ def inverter_data(): # 0x4210 ftype: 0x01
|
||||
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||
|
||||
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
@@ -85,10 +86,21 @@ def test_parse_4110(str_test_ip, device_data: bytes):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
|
||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0", "WiFi_SSID": "Allius-Home"},
|
||||
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||
})
|
||||
|
||||
def test_build_4110(str_test_ip, device_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
for key, update in i.parse (device_data, 0x41, 2):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(device_data), 0x41, 2)
|
||||
for i in range(11, 20):
|
||||
build_msg[i] = device_data[i]
|
||||
assert device_data == build_msg
|
||||
|
||||
def test_parse_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
@@ -98,16 +110,31 @@ def test_parse_4210(inverter_data: bytes):
|
||||
|
||||
assert json.dumps(i.db) == json.dumps({
|
||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": 2000, "Input_Coefficient": 100.0, "Output_Coefficient": 100.0},
|
||||
"env": {"Inverter_Status": 1, "Detect_Status_1": 2, "Detect_Status_2": 0, "Inverter_Temp": 14},
|
||||
"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0},
|
||||
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
|
||||
"pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91},
|
||||
"pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89},
|
||||
"pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}},
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36}
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36},
|
||||
"inv_unknown": {"Unknown_1": 512},
|
||||
"other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024, "Prod_Compliance_Type": 6}
|
||||
})
|
||||
|
||||
def test_build_4210(inverter_data: bytes):
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
for i in range(11, 31):
|
||||
build_msg[i] = inverter_data[i]
|
||||
assert inverter_data == build_msg
|
||||
|
||||
def test_build_ha_conf1():
|
||||
i = InfosG3P(client_mode=False)
|
||||
i.static_init() # initialize counter
|
||||
@@ -256,10 +283,12 @@ def test_build_ha_conf4():
|
||||
|
||||
assert tests==1
|
||||
|
||||
def test_exception_and_eval(inverter_data: bytes):
|
||||
def test_exception_and_calc(inverter_data: bytes):
|
||||
|
||||
# add eval to convert temperature from °F to °C
|
||||
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
||||
# patch table to convert temperature from °F to °C
|
||||
ofs = RegisterMap.map[0x420100d8]['offset']
|
||||
RegisterMap.map[0x420100d8]['quotient'] = 1.8
|
||||
RegisterMap.map[0x420100d8]['offset'] = -32/1.8
|
||||
# map PV1_VOLTAGE to invalid register
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
|
||||
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
||||
@@ -267,16 +296,43 @@ def test_exception_and_eval(inverter_data: bytes):
|
||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
||||
|
||||
i = InfosG3P(client_mode=False)
|
||||
# i.db.clear()
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
||||
del RegisterMap.map[0x420100d8]['eval'] # remove eval
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xde] == inverter_data[32:0xde]
|
||||
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
|
||||
# remove a table entry and test parsing and building
|
||||
del RegisterMap.map[0x420100d8]['quotient']
|
||||
del RegisterMap.map[0x420100d8]['offset']
|
||||
|
||||
i.db.clear()
|
||||
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
|
||||
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||
|
||||
# test restore table
|
||||
RegisterMap.map[0x420100d8]['offset'] = ofs
|
||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
||||
|
||||
# test orginial table
|
||||
i.db.clear()
|
||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
||||
pass # side effect is calling generator i.parse()
|
||||
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||
|
||||
build_msg = i.build(len(inverter_data), 0x42, 1)
|
||||
assert build_msg[32:-1] == inverter_data[32:-1]
|
||||
|
||||
@@ -5,14 +5,14 @@ import gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.talent import Talent
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.async_stream import AsyncStream, AsyncStreamClient
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from gen3.talent import Talent
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from async_stream import AsyncStream, AsyncStreamClient
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -69,13 +69,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -85,9 +85,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ import sys,gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.gen3.inverter_g3 import InverterG3
|
||||
from app.src.async_stream import AsyncStream
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from async_stream import AsyncStream
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -70,13 +70,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -86,9 +86,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -144,14 +144,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
test = MockType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
@@ -4,14 +4,14 @@ import asyncio
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -69,13 +69,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class TestType(Enum):
|
||||
class MockType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -85,9 +85,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -121,14 +121,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
test = MockType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
@@ -144,7 +144,7 @@ async def test_mqtt_publish(config_conn, patch_open_connection):
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
await inverter.async_publ_mqtt() # check call with invalid unique_id
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream._set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
@@ -171,7 +171,7 @@ async def test_mqtt_err(config_conn, patch_open_connection, patch_mqtt_err):
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream._set_serial_no(snr= 123344)
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
await inverter.async_publ_mqtt()
|
||||
@@ -188,7 +188,7 @@ async def test_mqtt_except(config_conn, patch_open_connection, patch_mqtt_except
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
stream = inverter.local.stream
|
||||
stream._SolarmanV5__set_serial_no(snr= 123344)
|
||||
stream._set_serial_no(snr= 123344)
|
||||
|
||||
stream.new_data['inverter'] = True
|
||||
stream.db.db['inverter'] = {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from infos import Infos, Register
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -77,9 +77,10 @@ def test_recv_resp_crc_err():
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
mb.set_node_id('test')
|
||||
# check matching response, but with CRC error
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3'):
|
||||
call += 1
|
||||
assert mb.err == 1
|
||||
assert 0 == call
|
||||
@@ -97,10 +98,11 @@ def test_recv_resp_invalid_addr():
|
||||
mb.last_fcode = 3
|
||||
mb.last_reg = 0x300e
|
||||
mb.last_len = 2
|
||||
mb.set_node_id('test')
|
||||
|
||||
# check not matching response, with wrong server addr
|
||||
call = 0
|
||||
for key, update in mb.recv_resp(mb.db, b'\x02\x03\x04\x01\x2c\x00\x46\x88\xf4', 'test'):
|
||||
for key, update in mb.recv_resp(mb.db, b'\x02\x03\x04\x01\x2c\x00\x46\x88\xf4'):
|
||||
call += 1
|
||||
assert mb.err == 2
|
||||
assert 0 == call
|
||||
@@ -120,7 +122,8 @@ def test_recv_recv_fcode():
|
||||
|
||||
# check not matching response, with wrong function code
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 3
|
||||
@@ -142,7 +145,8 @@ def test_recv_resp_len():
|
||||
|
||||
# check not matching response, with wrong data length
|
||||
call = 0
|
||||
for key, update, _ in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, _ in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 4
|
||||
@@ -161,7 +165,8 @@ def test_recv_unexpect_resp():
|
||||
|
||||
# check unexpected response, which must be dropped
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
|
||||
assert mb.err == 5
|
||||
@@ -177,8 +182,9 @@ def test_parse_resp():
|
||||
assert mb.req_pend
|
||||
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -226,8 +232,9 @@ def test_queue2():
|
||||
assert mb.send_calls == 1
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -245,14 +252,14 @@ def test_queue2():
|
||||
assert mb.send_calls == 2
|
||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b'):
|
||||
pass # call generator mb.recv_resp()
|
||||
|
||||
assert mb.que.qsize() == 0
|
||||
assert mb.send_calls == 3
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8'):
|
||||
call += 1
|
||||
assert 0 == mb.err
|
||||
assert 5 == call
|
||||
@@ -276,8 +283,9 @@ def test_queue3():
|
||||
assert mb.recv_responses == 0
|
||||
|
||||
call = 0
|
||||
mb.set_node_id('test')
|
||||
exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8'):
|
||||
if key == 'grid':
|
||||
assert update == True
|
||||
elif key == 'inverter':
|
||||
@@ -296,7 +304,7 @@ def test_queue3():
|
||||
assert mb.send_calls == 2
|
||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b'):
|
||||
pass # no code in loop is OK; calling the generator is the purpose
|
||||
assert 0 == mb.err
|
||||
assert mb.recv_responses == 2
|
||||
@@ -305,7 +313,7 @@ def test_queue3():
|
||||
assert mb.send_calls == 3
|
||||
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8'):
|
||||
call += 1
|
||||
assert 0 == mb.err
|
||||
assert mb.recv_responses == 2
|
||||
@@ -373,7 +381,8 @@ def test_recv_unknown_data():
|
||||
|
||||
# check matching response, but with CRC error
|
||||
call = 0
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'):
|
||||
mb.set_node_id('test')
|
||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'):
|
||||
call += 1
|
||||
assert mb.err == 0
|
||||
assert 0 == call
|
||||
|
||||
@@ -5,14 +5,14 @@ from aiomqtt import MqttCodeError
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.messages import Message, State
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.modbus_tcp import ModbusConn, ModbusTcp
|
||||
from singleton import Singleton
|
||||
from cnf.config import Config
|
||||
from infos import Infos
|
||||
from mqtt import Mqtt
|
||||
from inverter_base import InverterBase
|
||||
from messages import Message, State
|
||||
from proxy import Proxy
|
||||
from modbus_tcp import ModbusConn, ModbusTcp
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -52,6 +52,10 @@ def config_conn(test_hostname, test_port):
|
||||
'proxy_node_id': 'test_1',
|
||||
'proxy_unique_id': ''
|
||||
},
|
||||
'solarman':{
|
||||
'host': 'access1.solarmanpv.com',
|
||||
'port': 10000
|
||||
},
|
||||
'inverters':{
|
||||
'allow_all': True,
|
||||
"R170000000000001":{
|
||||
@@ -65,7 +69,8 @@ def config_conn(test_hostname, test_port):
|
||||
'sensor_list': 0x2b0,
|
||||
'client_mode':{
|
||||
'host': '192.168.0.1',
|
||||
'port': 8899
|
||||
'port': 8899,
|
||||
'forward': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from app.src.async_stream import AsyncIfcImpl
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
from async_stream import AsyncIfcImpl
|
||||
from singleton import Singleton
|
||||
from mqtt import Mqtt
|
||||
from modbus import Modbus
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
|
||||
NO_MOSQUITTO_TEST = False
|
||||
'''disable all tests with connections to test.mosquitto.org'''
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -69,23 +71,79 @@ def spy_modbus_cmd_client():
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server.
|
||||
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
|
||||
all test cases which depends on the test.mosquitto.org server
|
||||
"""
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import threading
|
||||
|
||||
c = mqtt.Client()
|
||||
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
c.loop_start()
|
||||
try:
|
||||
# Just make sure the client connects successfully
|
||||
on_connect = threading.Event()
|
||||
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
||||
c.connect_async(test_hostname, test_port)
|
||||
assert on_connect.wait(5)
|
||||
if not on_connect.wait(3):
|
||||
NO_MOSQUITTO_TEST = True # skip all mosquitto tests
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
finally:
|
||||
c.loop_stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
@@ -110,29 +168,6 @@ async def test_mqtt_no_config(config_no_conn):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
@@ -209,26 +244,6 @@ async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ignore_unknown_func(config_mqtt_conn):
|
||||
'''don't dispatch for unknwon function names'''
|
||||
|
||||
@@ -5,11 +5,11 @@ import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
from singleton import Singleton
|
||||
from proxy import Proxy
|
||||
from mqtt import Mqtt
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
24
app/tests/test_server.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import get_log_level
|
||||
|
||||
def test_get_log_level():
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': ''}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.DEBUG
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.WARNING
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
@@ -1,16 +1,16 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from app.src.singleton import Singleton
|
||||
from singleton import Singleton
|
||||
|
||||
class Test(metaclass=Singleton):
|
||||
class Example(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass # is a dummy test class
|
||||
|
||||
def test_singleton_metaclass():
|
||||
Singleton._instances.clear()
|
||||
a = Test()
|
||||
a = Example()
|
||||
assert 1 == len(Singleton._instances)
|
||||
b = Test()
|
||||
b = Example()
|
||||
assert 1 == len(Singleton._instances)
|
||||
assert a is b
|
||||
del a
|
||||
|
||||
@@ -5,12 +5,12 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
from math import isclose
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State, Message
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State, Message
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -37,6 +37,9 @@ class FakeIfc(AsyncIfcImpl):
|
||||
super().__init__()
|
||||
self.remote = StreamPtr(None)
|
||||
|
||||
async def create_remote(self):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
class MemoryStream(SolarmanV5):
|
||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||
_ifc = FakeIfc()
|
||||
@@ -109,7 +112,7 @@ class MemoryStream(SolarmanV5):
|
||||
c.ifc.remote.stream = self
|
||||
return c
|
||||
|
||||
def _SolarmanV5__flush_recv_msg(self) -> None:
|
||||
def _SolarmanBase__flush_recv_msg(self) -> None:
|
||||
self.msg_recvd.append(
|
||||
{
|
||||
'control': self.control,
|
||||
@@ -117,7 +120,7 @@ class MemoryStream(SolarmanV5):
|
||||
'data_len': self.data_len
|
||||
}
|
||||
)
|
||||
super()._SolarmanV5__flush_recv_msg()
|
||||
super()._SolarmanBase__flush_recv_msg()
|
||||
self.msg_count += 1
|
||||
|
||||
|
||||
@@ -1102,7 +1105,7 @@ def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg
|
||||
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||
|
||||
m.seq.server_side = False # simulate forawding to TSUN cloud
|
||||
m._update_header(m.ifc.fwd_fifo.peek())
|
||||
m._SolarmanBase__update_header(m.ifc.fwd_fifo.peek())
|
||||
assert str(m.seq) == '0d:0e' # value after forwarding indication
|
||||
assert m.ifc.fwd_fifo.get()==sync_start_fwd_msg
|
||||
|
||||
@@ -1768,7 +1771,7 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
||||
assert m.no_forwarding == False
|
||||
assert m.mb_timer.tim == None
|
||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||
await m.send_start_cmd(get_sn_int(), str_test_ip, m.mb_first_timeout)
|
||||
await m.send_start_cmd(get_sn_int(), str_test_ip, False, m.mb_first_timeout)
|
||||
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15')
|
||||
assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
|
||||
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
|
||||
@@ -1803,3 +1806,30 @@ def test_timeout(config_tsun_inv1):
|
||||
assert SolarmanV5.MAX_DEF_IDLE_TIME == m._timeout()
|
||||
m.state = State.closed
|
||||
m.close()
|
||||
|
||||
def test_fnc_dispatch():
|
||||
def msg():
|
||||
return
|
||||
|
||||
_ = config_tsun_inv1
|
||||
m = MemoryStream(b'')
|
||||
m.switch[1] = msg
|
||||
m.switch[2] = "msg"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(1)
|
||||
assert _obj == msg
|
||||
assert _str == "'msg'"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(2)
|
||||
assert _obj == m.msg_unknown
|
||||
assert _str == "'msg'"
|
||||
|
||||
_obj, _str = m.get_fnc_handler(3)
|
||||
assert _obj == m.msg_unknown
|
||||
assert _str == "'msg_unknown'"
|
||||
|
||||
def test_timestamp():
|
||||
m = MemoryStream(b'')
|
||||
ts = m._timestamp()
|
||||
ts_emu = m._emu_timestamp()
|
||||
assert ts == ts_emu + 24*60*60
|
||||
233
app/tests/test_solarman_emu.py
Normal file
@@ -0,0 +1,233 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
from infos import Infos, Register
|
||||
|
||||
from test_solarman import FakeIfc, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
|
||||
from test_infos_g3p import str_test_ip, bytes_test_ip
|
||||
|
||||
timestamp = 0x3224c8bc
|
||||
|
||||
class InvStream(MemoryStream):
|
||||
def __init__(self, msg=b''):
|
||||
super().__init__(msg)
|
||||
|
||||
def _emu_timestamp(self):
|
||||
return timestamp
|
||||
|
||||
class CldStream(SolarmanEmu):
|
||||
def __init__(self, inv: InvStream):
|
||||
_ifc = FakeIfc()
|
||||
_ifc.remote.stream = inv
|
||||
super().__init__(('test.local', 1234), _ifc, server_side=False, client_mode=False)
|
||||
self.__msg = b''
|
||||
self.__msg_len = 0
|
||||
self.__offs = 0
|
||||
self.msg_count = 0
|
||||
self.msg_recvd = []
|
||||
|
||||
def _emu_timestamp(self):
|
||||
return timestamp
|
||||
|
||||
def append_msg(self, msg):
|
||||
self.__msg += msg
|
||||
self.__msg_len += len(msg)
|
||||
|
||||
def _read(self) -> int:
|
||||
copied_bytes = 0
|
||||
try:
|
||||
if (self.__offs < self.__msg_len):
|
||||
self.ifc.rx_fifo += self.__msg[self.__offs:]
|
||||
copied_bytes = self.__msg_len - self.__offs
|
||||
self.__offs = self.__msg_len
|
||||
except Exception:
|
||||
pass # ignore exceptions here
|
||||
return copied_bytes
|
||||
|
||||
def _SolarmanBase__flush_recv_msg(self) -> None:
|
||||
self.msg_recvd.append(
|
||||
{
|
||||
'control': self.control,
|
||||
'seq': str(self.seq),
|
||||
'data_len': self.data_len
|
||||
}
|
||||
)
|
||||
super()._SolarmanBase__flush_recv_msg()
|
||||
self.msg_count += 1
|
||||
|
||||
@pytest.fixture
|
||||
def device_ind_msg(bytes_test_ip): # 0x4110
|
||||
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xbc\xc8\x24\x32'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + bytes_test_ip
|
||||
msg += b'\x0f\x00\x01\xb0'
|
||||
msg += b'\x02\x0f\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_ind_msg(): # 0x4210
|
||||
msg = b'\xa5\x99\x01\x10\x42\x00\x01' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||
msg += b'\x24\x32\x3c\x00\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||
msg += b'\x59\x31\x37\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x31'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00'
|
||||
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||
msg += b'\x00\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def inverter_rsp_msg(): # 0x1210
|
||||
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += b'\x3c\x00\x00\x00'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def heartbeat_ind():
|
||||
msg = b'\xa5\x01\x00\x10G\x00\x01\x00\x00\x00\x00\x00Y\x15'
|
||||
return msg
|
||||
|
||||
def test_emu_init_close():
|
||||
# received a message with wrong start byte plus an valid message
|
||||
# the complete receive buffer must be cleared to
|
||||
# find the next valid message
|
||||
inv = InvStream()
|
||||
cld = CldStream(inv)
|
||||
cld.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emu_start(config_tsun_inv1, msg_modbus_rsp, str_test_ip, device_ind_msg):
|
||||
_ = config_tsun_inv1
|
||||
assert asyncio.get_running_loop()
|
||||
inv = InvStream(msg_modbus_rsp)
|
||||
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, True, inv.mb_first_timeout)
|
||||
inv.read() # read complete msg, and dispatch msg
|
||||
assert not inv.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert inv.msg_count == 1
|
||||
assert inv.control == 0x1510
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld.ifc.update_header_cb(inv.ifc.fwd_fifo.peek())
|
||||
assert inv.ifc.fwd_fifo.peek() == device_ind_msg
|
||||
cld.close()
|
||||
|
||||
def test_snd_hb(config_tsun_inv1, heartbeat_ind):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
cld = CldStream(inv)
|
||||
|
||||
# await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
cld.send_heartbeat_cb(0)
|
||||
assert cld.ifc.tx_fifo.peek() == heartbeat_ind
|
||||
cld.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snd_inv_data(config_tsun_inv1, inverter_ind_msg, inverter_rsp_msg):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
inv.db.set_db_def_value(Register.INVERTER_STATUS, 1)
|
||||
inv.db.set_db_def_value(Register.DETECT_STATUS_1, 2)
|
||||
inv.db.set_db_def_value(Register.VERSION, 'V4.0.10')
|
||||
inv.db.set_db_def_value(Register.GRID_VOLTAGE, 224.8)
|
||||
inv.db.set_db_def_value(Register.GRID_CURRENT, 0.73)
|
||||
inv.db.set_db_def_value(Register.GRID_FREQUENCY, 50.05)
|
||||
inv.db.set_db_def_value(Register.PROD_COMPL_TYPE, 6)
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
inv.db.set_db_def_value(Register.DATA_UP_INTERVAL, 17) # set test value
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld.time_ofs = 0x33e447a0
|
||||
cld.last_sync = cld._emu_timestamp() - 60
|
||||
cld.pkt_cnt = 0x802
|
||||
assert cld.data_up_inv == 17 # check test value
|
||||
cld.data_up_inv = 0.1 # speedup test first data msg
|
||||
cld._init_new_client_conn()
|
||||
cld.data_up_inv = 0.5 # timeout for second data msg
|
||||
await asyncio.sleep(0.2)
|
||||
assert cld.ifc.tx_fifo.get() == inverter_ind_msg
|
||||
|
||||
cld.append_msg(inverter_rsp_msg)
|
||||
cld.read() # read complete msg, and dispatch msg
|
||||
|
||||
assert not cld.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert cld.msg_count == 1
|
||||
assert cld.header_len==11
|
||||
assert cld.snr == 2070233889
|
||||
assert cld.unique_id == '2070233889'
|
||||
assert cld.msg_recvd[0]['control']==0x1210
|
||||
assert cld.msg_recvd[0]['seq']=='02:02'
|
||||
assert cld.msg_recvd[0]['data_len']==0x0a
|
||||
assert '02b0' == cld.db.get_db_value(Register.SENSOR_LIST, None)
|
||||
assert cld.db.stat['proxy']['Unknown_Msg'] == 0
|
||||
|
||||
cld.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rcv_invalid(config_tsun_inv1, inverter_ind_msg, inverter_rsp_msg):
|
||||
_ = config_tsun_inv1
|
||||
inv = InvStream()
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
inv.db.set_db_def_value(Register.DATA_UP_INTERVAL, 17) # set test value
|
||||
|
||||
cld = CldStream(inv)
|
||||
cld._init_new_client_conn()
|
||||
|
||||
cld.append_msg(inverter_ind_msg)
|
||||
cld.read() # read complete msg, and dispatch msg
|
||||
|
||||
assert not cld.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert cld.msg_count == 1
|
||||
assert cld.header_len==11
|
||||
assert cld.snr == 2070233889
|
||||
assert cld.unique_id == '2070233889'
|
||||
assert cld.msg_recvd[0]['control']==0x4210
|
||||
assert cld.msg_recvd[0]['seq']=='00:01'
|
||||
assert cld.msg_recvd[0]['data_len']==0x199
|
||||
assert '02b0' == cld.db.get_db_value(Register.SENSOR_LIST, None)
|
||||
assert cld.db.stat['proxy']['Unknown_Msg'] == 1
|
||||
|
||||
|
||||
cld.close()
|
||||
@@ -1,12 +1,12 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, logging, asyncio
|
||||
from math import isclose
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3.talent import Talent, Control
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3.talent import Talent, Control
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
SHELL = /bin/sh
|
||||
|
||||
# Folders
|
||||
SRC=../app
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
DST=rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
build: rootfs
|
||||
|
||||
clean:
|
||||
rm -r -f $(DST_PROXY)
|
||||
rm -f $(DST)/requirements.txt
|
||||
|
||||
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
|
||||
|
||||
.PHONY: build clean rootfs
|
||||
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
@@ -1,65 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
# Dieses file übernimmt die Add-On Konfiguration und schreibt sie in die
|
||||
# Konfigurationsdatei des tsun-proxy
|
||||
# Die Addon Konfiguration wird in der Datei /data/options.json bereitgestellt
|
||||
# Die Konfiguration wird in der Datei /home/proxy/config/config.toml
|
||||
# gespeichert
|
||||
|
||||
# Übernehme die Umgebungsvariablen
|
||||
# alternativ kann auch auf die homeassistant supervisor API zugegriffen werden
|
||||
|
||||
data = {}
|
||||
data['mqtt.host'] = os.getenv('MQTT_HOST')
|
||||
data['mqtt.port'] = os.getenv('MQTT_PORT')
|
||||
data['mqtt.user'] = os.getenv('MQTT_USER')
|
||||
data['mqtt.passwd'] = os.getenv('MQTT_PASSWORD')
|
||||
|
||||
|
||||
# Lese die Add-On Konfiguration aus der Datei /data/options.json
|
||||
with open('/data/options.json') as json_file:
|
||||
# with open('options.json') as json_file:
|
||||
options_data = json.load(json_file)
|
||||
data.update(options_data)
|
||||
|
||||
|
||||
# Schreibe die Add-On Konfiguration in die Datei /home/proxy/config/config.toml # noqa: E501
|
||||
with open('/home/proxy/config/config.toml', 'w+') as f:
|
||||
# with open('./config/config.toml', 'w+') as f:
|
||||
f.write(f"""
|
||||
mqtt.host = '{data.get('mqtt.host')}' # URL or IP address of the mqtt broker
|
||||
mqtt.port = {data.get('mqtt.port')}
|
||||
mqtt.user = '{data.get('mqtt.user')}'
|
||||
mqtt.passwd = '{data.get('mqtt.passwd')}'
|
||||
|
||||
|
||||
ha.auto_conf_prefix = '{data.get('ha.auto_conf_prefix', 'homeassistant')}' # MQTT prefix for subscribing for homeassistant status updates # noqa: E501
|
||||
ha.discovery_prefix = '{data.get('ha.discovery_prefix', 'homeassistant')}' # MQTT prefix for discovery topic # noqa: E501
|
||||
ha.entity_prefix = '{data.get('ha.entity_prefix', 'tsun')}' # MQTT topic prefix for publishing inverter values # noqa: E501
|
||||
ha.proxy_node_id = '{data.get('ha.proxy_node_id', 'proxy')}' # MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id = '{data.get('ha.proxy_unique_id', 'P170000000000001')}' # MQTT unique id, to identify a proxy instance
|
||||
|
||||
|
||||
tsun.enabled = {str(data.get('tsun.enabled', True)).lower()}
|
||||
tsun.host = '{data.get('tsun.host', 'logger.talent-monitoring.com')}'
|
||||
tsun.port = {data.get('tsun.port', 5005)}
|
||||
|
||||
|
||||
solarman.enabled = {str(data.get('solarman.enabled', True)).lower()}
|
||||
solarman.host = '{data.get('solarman.host', 'iot.talent-monitoring.com')}'
|
||||
solarman.port = {data.get('solarman.port', 10000)}
|
||||
|
||||
|
||||
inverters.allow_all = {str(data.get('inverters.allow_all', False)).lower()}
|
||||
""")
|
||||
|
||||
for inverter in data['inverters']:
|
||||
f.write(f"""
|
||||
[inverters."{inverter['serial']}"]
|
||||
node_id = '{inverter['node_id']}'
|
||||
suggested_area = '{inverter['suggested_area']}'
|
||||
modbus_polling = {str(inverter['modbus_polling']).lower()}
|
||||
pv1 = {{type = '{inverter['pv1_type']}', manufacturer = '{inverter['pv1_manufacturer']}'}} # Optional, PV module descr # noqa: E501
|
||||
pv2 = {{type = '{inverter['pv2_type']}', manufacturer = '{inverter['pv2_manufacturer']}'}} # Optional, PV module descr # noqa: E501
|
||||
""")
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
|
||||
{
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R17E760702080400",
|
||||
"node_id": "PV-Garage",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": false,
|
||||
"pv1_manufacturer": "Shinefar",
|
||||
"pv1_type": "SF-M18/144550",
|
||||
"pv2_manufacturer": "Shinefar",
|
||||
"pv2_type": "SF-M18/144550"
|
||||
}
|
||||
],
|
||||
"tsun.enabled": false,
|
||||
"solarman.enabled": false,
|
||||
"inverters.allow_all": false
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# test_with_pytest.py
|
||||
# import ha_addon.rootfs.home.create_config_toml
|
||||
|
||||
|
||||
def test_config():
|
||||
pass
|
||||
@@ -13,10 +13,7 @@
|
||||
# 1 Build Image #
|
||||
######################
|
||||
|
||||
# opt for suitable build base. I opted for the recommended hassio-addon base
|
||||
|
||||
#ARG BUILD_FROM="ghcr.io/hassio-addons/debian-base:latest"
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:latest"
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:stable"
|
||||
FROM $BUILD_FROM
|
||||
|
||||
|
||||
@@ -70,18 +67,16 @@ COPY rootfs/ /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
|
||||
# no idea whether needed or not
|
||||
ENV SERVICE_NAME="tsun-proxy"
|
||||
ENV UID=1000
|
||||
ENV GID=1000
|
||||
ENV VERSION="0.0"
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 6 run app #
|
||||
#######################
|
||||
|
||||
ARG SERVICE_NAME
|
||||
ARG VERSION
|
||||
ENV SERVICE_NAME=${SERVICE_NAME}
|
||||
|
||||
RUN echo ${VERSION} > /proxy-version.txt
|
||||
|
||||
# command to run on container start
|
||||
CMD [ "/run.sh" ]
|
||||
@@ -90,8 +85,3 @@ CMD [ "/run.sh" ]
|
||||
|
||||
#######################
|
||||
|
||||
# Labels
|
||||
LABEL \
|
||||
io.hass.version="VERSION" \
|
||||
io.hass.type="addon" \
|
||||
io.hass.arch="armhf|aarch64|i386|amd64"
|
||||
74
ha_addons/ha_addon/Makefile
Normal file
@@ -0,0 +1,74 @@
|
||||
#!make
|
||||
include ../../.env
|
||||
|
||||
SHELL = /bin/sh
|
||||
IMAGE = tsun-gen3-addon
|
||||
|
||||
|
||||
# Folders
|
||||
SRC=../../app
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
DST=rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
export BUILD_DATE := ${shell date -Iminutes}
|
||||
VERSION := $(shell cat $(SRC)/.version)
|
||||
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||
|
||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||
|
||||
|
||||
dev debug: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
rc: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
|
||||
build: rootfs
|
||||
|
||||
clean:
|
||||
rm -r -f $(DST_PROXY)
|
||||
rm -f $(DST)/requirements.txt
|
||||
|
||||
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
|
||||
|
||||
.PHONY: debug dev build clean rootfs
|
||||
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
@@ -1,6 +1,8 @@
|
||||
name: "TSUN-Proxy"
|
||||
description: "MQTT Proxy for TSUN Photovoltaic Inverters"
|
||||
version: "0.0.7"
|
||||
version: "dev"
|
||||
image: docker.io/sallius/tsun-gen3-addon
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy
|
||||
slug: "tsun-proxy"
|
||||
init: false
|
||||
arch:
|
||||
@@ -20,24 +22,35 @@ ports:
|
||||
|
||||
# Definition of parameters in the configuration tab of the addon
|
||||
# parameters are available within the container as /data/options.json
|
||||
# and should become picked up by the proxy - current workarround as a transfer script
|
||||
# TODO: add further schema for remaining config parameters
|
||||
# and should become picked up by the proxy - current workaround as a transfer script
|
||||
# TODO: check again for multi hierarchie parameters
|
||||
# TODO: implement direct reading of the configuration file
|
||||
schema:
|
||||
inverters:
|
||||
- serial: str
|
||||
monitor_sn: int?
|
||||
node_id: str
|
||||
suggested_area: str
|
||||
modbus_polling: bool
|
||||
#strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt.
|
||||
client_mode_host: str?
|
||||
client_mode_port: int?
|
||||
#strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt.
|
||||
# - string: str
|
||||
# type: str
|
||||
# manufacturer: str
|
||||
# daher diese variante
|
||||
pv1_manufacturer: str
|
||||
pv1_type: str
|
||||
pv2_manufacturer: str
|
||||
pv2_type: str
|
||||
pv1.manufacturer: str?
|
||||
pv1.type: str?
|
||||
pv2.manufacturer: str?
|
||||
pv2.type: str?
|
||||
pv3.manufacturer: str?
|
||||
pv3.type: str?
|
||||
pv4.manufacturer: str?
|
||||
pv4.type: str?
|
||||
pv5.manufacturer: str?
|
||||
pv5.type: str?
|
||||
pv6.manufacturer: str?
|
||||
pv6.type: str?
|
||||
tsun.enabled: bool
|
||||
solarman.enabled: bool
|
||||
inverters.allow_all: bool
|
||||
@@ -52,6 +65,16 @@ schema:
|
||||
ha.entity_prefix: str? #dito
|
||||
ha.proxy_node_id: str? #dito
|
||||
ha.proxy_unique_id: str? #dito
|
||||
tsun.host: str?
|
||||
solarman.host: str?
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
- str
|
||||
gen3plus.at_acl.tsun.block:
|
||||
- str?
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
- str
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
- str?
|
||||
|
||||
# set default options for mandatory parameters
|
||||
# for optional parameters do not define any default value in the options dictionary.
|
||||
@@ -62,17 +85,19 @@ options:
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: false
|
||||
#strings:
|
||||
# strings:
|
||||
# - string: PV1
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
# - string: PV2
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
pv1_manufacturer: Shinefar
|
||||
pv1_type: SF-M18/144550
|
||||
pv2_manufacturer: Shinefar
|
||||
pv2_type: SF-M18/144550
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
tsun.enabled: true # set default
|
||||
solarman.enabled: true # set default
|
||||
inverters.allow_all: false # set default
|
||||
gen3plus.at_acl.tsun.allow: ["AT+Z", "AT+UPURL", "AT+SUPDATE"]
|
||||
gen3plus.at_acl.mqtt.allow: ["AT+"]
|
||||
99
ha_addons/ha_addon/docker-bake.hcl
Normal file
@@ -0,0 +1,99 @@
|
||||
variable "IMAGE" {
|
||||
default = "tsun-gen3-addon"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = "0.0.0"
|
||||
}
|
||||
variable "MAJOR" {
|
||||
default = "0"
|
||||
}
|
||||
variable "BUILD_DATE" {
|
||||
default = "dev"
|
||||
}
|
||||
variable "BRANCH" {
|
||||
default = ""
|
||||
}
|
||||
variable "DESCRIPTION" {
|
||||
default = "This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations."
|
||||
}
|
||||
|
||||
target "_common" {
|
||||
context = "."
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
environment = "production"
|
||||
}
|
||||
attest = [
|
||||
"type =provenance,mode=max",
|
||||
"type =sbom,generator=docker/scout-sbom-indexer:latest"
|
||||
]
|
||||
annotations = [
|
||||
"index:io.hass.version=${VERSION}",
|
||||
"index:io.hass.type=addon",
|
||||
"index:io.hass.arch=armhf|aarch64|i386|amd64",
|
||||
"index:org.opencontainers.image.title=TSUN-Proxy",
|
||||
"index:org.opencontainers.image.authors=Stefan Allius",
|
||||
"index:org.opencontainers.image.created=${BUILD_DATE}",
|
||||
"index:org.opencontainers.image.version=${VERSION}",
|
||||
"index:org.opencontainers.image.revision=${BRANCH}",
|
||||
"index:org.opencontainers.image.description=${DESCRIPTION}",
|
||||
"index:org.opencontainers.image.licenses=BSD-3-Clause",
|
||||
"index:org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy/ha_addons/ha_addon"
|
||||
]
|
||||
labels = {
|
||||
"io.hass.version" = "${VERSION}"
|
||||
"io.hass.type" = "addon"
|
||||
"io.hass.arch" = "armhf|aarch64|i386|amd64"
|
||||
"org.opencontainers.image.title" = "TSUN-Proxy"
|
||||
"org.opencontainers.image.authors" = "Stefan Allius"
|
||||
"org.opencontainers.image.created" = "${BUILD_DATE}"
|
||||
"org.opencontainers.image.version" = "${VERSION}"
|
||||
"org.opencontainers.image.revision" = "${BRANCH}"
|
||||
"org.opencontainers.image.description" = "${DESCRIPTION}"
|
||||
"org.opencontainers.image.licenses" = "BSD-3-Clause"
|
||||
"org.opencontainers.image.source" = "https://github.com/s-allius/tsun-gen3-proxy/ha_addonsha_addon"
|
||||
}
|
||||
output = [
|
||||
"type=image,push=true"
|
||||
]
|
||||
|
||||
no-cache = false
|
||||
platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
|
||||
}
|
||||
|
||||
target "_debug" {
|
||||
args = {
|
||||
LOG_LVL = "DEBUG"
|
||||
environment = "dev"
|
||||
}
|
||||
}
|
||||
target "_prod" {
|
||||
args = {
|
||||
}
|
||||
}
|
||||
target "debug" {
|
||||
inherits = ["_common", "_debug"]
|
||||
tags = ["${IMAGE}:debug"]
|
||||
}
|
||||
|
||||
target "dev" {
|
||||
inherits = ["_common"]
|
||||
tags = ["${IMAGE}:dev"]
|
||||
}
|
||||
|
||||
target "preview" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:preview", "${IMAGE}:${VERSION}"]
|
||||
}
|
||||
|
||||
target "rc" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:rc", "${IMAGE}:${VERSION}"]
|
||||
}
|
||||
|
||||
target "rel" {
|
||||
inherits = ["_common", "_prod"]
|
||||
tags = ["${IMAGE}:latest", "${IMAGE}:${MAJOR}", "${IMAGE}:${VERSION}"]
|
||||
no-cache = true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@@ -23,12 +23,13 @@ fi
|
||||
|
||||
cd /home || exit
|
||||
|
||||
|
||||
echo "Erstelle config.toml"
|
||||
python3 create_config_toml.py
|
||||
|
||||
# Erstelle Ordner für log und config
|
||||
mkdir -p proxy/log
|
||||
mkdir -p proxy/config
|
||||
|
||||
cd /home/proxy || exit
|
||||
|
||||
echo "Starte Webserver"
|
||||
python3 server.py
|
||||
export VERSION=$(cat /proxy-version.txt)
|
||||
|
||||
echo "Start Proxyserver..."
|
||||
python3 server.py --json_config=/data/options.json
|
||||
@@ -2,7 +2,7 @@
|
||||
configuration:
|
||||
inverters:
|
||||
name: Inverters
|
||||
description: >-
|
||||
description: >+
|
||||
For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
definition. To do this, the corresponding configuration block is started with
|
||||
<16-digit serial number> so that all subsequent parameters are assigned
|
||||
@@ -11,6 +11,7 @@ configuration:
|
||||
|
||||
The serial numbers of all GEN3 inverters start with `R17`!
|
||||
|
||||
monitor_sn # The GEN3PLUS "Monitoring SN:"
|
||||
node_id # MQTT replacement for inverters serial number
|
||||
suggested_area # suggested installation area for home-assistant
|
||||
modbus_polling # Disable optional MODBUS polling
|
||||
@@ -18,21 +19,28 @@ configuration:
|
||||
pv2 # Optional, PV module descr
|
||||
|
||||
tsun.enabled:
|
||||
name: Connection to TSUN Cloud
|
||||
name: Connection to TSUN Cloud - for GEN3 inverter only
|
||||
description: >-
|
||||
disable connecting to the tsun cloud avoids updates.
|
||||
The Inverter become isolated from Internet if switched on.
|
||||
switch on/off connection to the TSUN cloud
|
||||
This connection is only required if you want send data to the TSUN cloud
|
||||
eg. to use the TSUN APPs or receive firmware updates.
|
||||
|
||||
on - normal proxy operation
|
||||
off - The Inverter become isolated from Internet
|
||||
solarman.enabled:
|
||||
name: Connection to Solarman Cloud
|
||||
name: Connection to Solarman Cloud - for GEN3PLUS inverter only
|
||||
description: >-
|
||||
disables connecting to the Solarman cloud avoids updates.
|
||||
The Inverter become isolated from Internet if switched on.
|
||||
switch on/off connection to the Solarman cloud
|
||||
This connection is only required if you want send data to the Solarman cloud
|
||||
eg. to use the Solarman APPs or receive firmware updates.
|
||||
|
||||
on - normal proxy operation
|
||||
off - The Inverter become isolated from Internet
|
||||
inverters.allow_all:
|
||||
name: Allow all connections from all inverters
|
||||
description: >-
|
||||
The proxy only usually accepts connections from known inverters.
|
||||
This can be switched off for test purposes and unknown serial
|
||||
numbers are also accepted.
|
||||
Switch on for test purposes and unknown serial numbers.
|
||||
mqtt.host:
|
||||
name: MQTT Broker Host
|
||||
description: >-
|
||||
@@ -59,6 +67,17 @@ configuration:
|
||||
name: MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id:
|
||||
name: MQTT unique id, to identify a proxy instance
|
||||
tsun.host:
|
||||
name: TSUN Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the TSUN cloud. if not set, the addon will try to connect to the cloud default
|
||||
on logger.talent-monitoring.com
|
||||
solarman.host:
|
||||
name: Solarman Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the Solarman cloud. if not set, the addon will try to connect to the cloud default
|
||||
on iot.talent-monitoring.com
|
||||
|
||||
|
||||
network:
|
||||
8127/tcp: x...
|
||||
3
ha_addons/repository.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: TSUN-Proxy
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy/ha_addons
|
||||
maintainer: Stefan Allius
|
||||
20
proxy.c4
Normal file
@@ -0,0 +1,20 @@
|
||||
model {
|
||||
extend home.logger.proxy {
|
||||
component webserver 'http server'
|
||||
component inverter 'inverter'
|
||||
component local 'local connection'
|
||||
component remote 'remote connection'
|
||||
component r-ifc 'async-ifc'
|
||||
component l-ifc 'async-ifc'
|
||||
component prot 'Protocol' 'SolarmanV5 or Talent'
|
||||
component config 'config' 'reads the file confg.toml'
|
||||
component mqtt
|
||||
inverter -> local
|
||||
inverter -> remote
|
||||
remote -> r-ifc
|
||||
remote -> prot
|
||||
local -> l-ifc
|
||||
local -> prot
|
||||
prot -> mqtt
|
||||
}
|
||||
}
|
||||
8
pytest.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
# pytest.ini or .pytest.ini
|
||||
[pytest]
|
||||
minversion = 8.0
|
||||
addopts = -ra -q --durations=5
|
||||
pythonpath = app/src app/tests ha_addons/ha_addon/rootfs
|
||||
testpaths = app/tests ha_addons/ha_addon/tests
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
asyncio_mode = strict
|
||||