Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2f88423d | ||
|
|
7fe9dcbe60 | ||
|
|
009746a1e4 | ||
|
|
4da8f8f3b2 | ||
|
|
13b1930599 | ||
|
|
a2364115b3 | ||
|
|
8f390b67cb | ||
|
|
fa86dde991 | ||
|
|
6cfc1792ba | ||
|
|
04ba868b37 | ||
|
|
f3842d95d8 | ||
|
|
fbbf698666 | ||
|
|
ef8a461569 | ||
|
|
73c35de3e5 | ||
|
|
80f4dd722a | ||
|
|
f38fea3807 | ||
|
|
db319f6aa3 | ||
|
|
695d8a8906 | ||
|
|
e4b7ef7a0c | ||
|
|
884d4c04e6 | ||
|
|
75bdaedc31 | ||
|
|
dccf0d22e1 | ||
|
|
c4db53bd1e | ||
|
|
f69b02aaeb | ||
|
|
cdc3226adf | ||
|
|
e29c250f39 | ||
|
|
643c0026d8 | ||
|
|
340f7a5127 | ||
|
|
7cbd5f25bb | ||
|
|
27ce61adf4 | ||
|
|
3d375d86be | ||
|
|
71ec0570ac | ||
|
|
e3fdeecf82 | ||
|
|
738dd708ac | ||
|
|
5853518afe | ||
|
|
385a984fd2 | ||
|
|
37cb7cc1a1 | ||
|
|
21e46ae456 | ||
|
|
c52fc990f4 | ||
|
|
5ddc402e3c | ||
|
|
ac81b20ce7 | ||
|
|
ef1fd4f913 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ tsun_proxy/**
|
|||||||
Doku/**
|
Doku/**
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.coverage
|
.coverage
|
||||||
|
.env
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.0] - 2024-04-02
|
||||||
|
|
||||||
|
- Refactoring to support Solarman V5 protocol
|
||||||
|
- Add unittest for Solarman V5 implementation
|
||||||
|
- Handle checksum errors
|
||||||
|
- Handle wrong start or Stop bytes
|
||||||
|
- Watch for AT commands and signal their occurrence to HA
|
||||||
|
- Build inverter type names for MS-1600 .. MS-2000
|
||||||
|
- Build device name for Solarman logger module
|
||||||
|
|
||||||
## [0.5.5] - 2023-12-31
|
## [0.5.5] - 2023-12-31
|
||||||
|
|
||||||
- Fixed [#33](https://github.com/s-allius/tsun-gen3-proxy/issues/33)
|
- Fixed [#33](https://github.com/s-allius/tsun-gen3-proxy/issues/33)
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -40,11 +40,11 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- supports TSUN G3 inverters: TSOL MS-300, MS-350, MS-400, MS-600, MS-700 and MS-800
|
- supports TSUN GEN3 inverters: TSOL MS-300, MS-350, MS-400, MS-600, MS-700 and MS-800
|
||||||
- support for TSUN G3 Plus inverters is in preperation (e.g. MS-2000)
|
- support for TSUN GEN3 PLUS inverters since proxy version 0.6 (e.g. MS-2000)
|
||||||
- `MQTT` support
|
- `MQTT` support
|
||||||
- `Home-Assistant` auto-discovery support
|
- `Home-Assistant` auto-discovery support
|
||||||
- Self-sufficient island operation without internet
|
- Self-sufficient island operation without internet (for TSUN GEN3 PLUS inverters in preparation)
|
||||||
- non-root Docker Container
|
- non-root Docker Container
|
||||||
|
|
||||||
## Home Assistant Screenshots
|
## Home Assistant Screenshots
|
||||||
@@ -67,7 +67,7 @@ docker build https://github.com/s-allius/tsun-gen3-proxy.git#main:app -t tsun-pr
|
|||||||
```
|
```
|
||||||
after that you can run the image:
|
after that you can run the image:
|
||||||
```sh
|
```sh
|
||||||
docker run --dns '8.8.8.8' --env 'UID=1000' -p '5005:5005' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
docker run --dns '8.8.8.8' --env 'UID=1000' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
||||||
```
|
```
|
||||||
You will surely see a message that the configuration file was not found. So that we can create this without admin rights, the `uid` must still be adapted. To do this, simply stop the proxy with ctrl-c and use the `id` command to determine your own UserId:
|
You will surely see a message that the configuration file was not found. So that we can create this without admin rights, the `uid` must still be adapted. To do this, simply stop the proxy with ctrl-c and use the `id` command to determine your own UserId:
|
||||||
```sh
|
```sh
|
||||||
@@ -76,7 +76,7 @@ uid=1050(sallius) gid=20(staff) ...
|
|||||||
```
|
```
|
||||||
With this information we can customize the `docker run`` statement:
|
With this information we can customize the `docker run`` statement:
|
||||||
```sh
|
```sh
|
||||||
docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
###
|
###
|
||||||
@@ -94,11 +94,16 @@ You find more details here: https://toml.io/en/v1.0.0
|
|||||||
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# configuration to reach tsun cloud
|
# configuration for tsun cloud for 'GEN3' inverters
|
||||||
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||||
tsun.host = 'logger.talent-monitoring.com'
|
tsun.host = 'logger.talent-monitoring.com'
|
||||||
tsun.port = 5005
|
tsun.port = 5005
|
||||||
|
|
||||||
|
# configuration for solarman cloud for 'GEN3 PLUS' inverters
|
||||||
|
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||||
|
solarman.host = 'iot.talent-monitoring.com'
|
||||||
|
solarman.port = 10000
|
||||||
|
|
||||||
|
|
||||||
# mqtt broker configuration
|
# mqtt broker configuration
|
||||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||||
@@ -130,6 +135,11 @@ suggested_area = 'roof' # Optional, suggested installation area for home-a
|
|||||||
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
||||||
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
||||||
|
|
||||||
|
[inverters."Y17xxxxxxxxxxxx1"]
|
||||||
|
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
|
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||||
|
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,6 +148,8 @@ suggested_area = 'balcony' # Optional, suggested installation area for home-a
|
|||||||
### Loop the proxy into the connection
|
### Loop the proxy into the connection
|
||||||
To include the proxy in the connection between the inverter and the TSUN Cloud, you must adapt the DNS record of *logger.talent-monitoring.com* within the network that your inverter uses. You need a mapping from logger.talent-monitoring.com to the IP address of the host running the Docker engine.
|
To include the proxy in the connection between the inverter and the TSUN Cloud, you must adapt the DNS record of *logger.talent-monitoring.com* within the network that your inverter uses. You need a mapping from logger.talent-monitoring.com to the IP address of the host running the Docker engine.
|
||||||
|
|
||||||
|
The new GEN3 PLUS inverters use a different URL. Here, *iot.talent-monitoring.com* must be redirected.
|
||||||
|
|
||||||
This can be done, for example, by adding a local DNS record to the Pi-hole if you are using it.
|
This can be done, for example, by adding a local DNS record to the Pi-hole if you are using it.
|
||||||
|
|
||||||
### DNS Rebind Protection
|
### DNS Rebind Protection
|
||||||
@@ -161,11 +173,11 @@ A combination with a red question mark should work, but I have not checked it in
|
|||||||
|
|
||||||
Micro Inverter Model | Fw. 1.00.06 | Fw. 1.00.17 | Fw. 1.00.20| Fw. 1.1.00.0B
|
Micro Inverter Model | Fw. 1.00.06 | Fw. 1.00.17 | Fw. 1.00.20| Fw. 1.1.00.0B
|
||||||
:---|:---:|:---:|:---:|:---:|
|
:---|:---:|:---:|:---:|:---:|
|
||||||
G3 micro inverters (single MPPT):<br>MS-300, MS-350, MS-400| ❓ | ❓ | ❓ |➖
|
GEN3 micro inverters (single MPPT):<br>MS300, MS350,MS-400| ❓ | ❓ | ❓ |➖
|
||||||
G3 micro inverters (dual MPPT):<br>MS-600, MS-700, MS-800| ✔️ | ✔️ | ✔️ |➖
|
GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800| ✔️ | ✔️ | ✔️ |➖
|
||||||
G3 PLUS micro inverters:<br>MS-1600, MS-1800, MS-2000| ➖ |➖ | ➖ | 🚧
|
GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000| ➖ |➖ | ➖ | ✔️
|
||||||
balcony micro inverters:<br>MS-400-D, MS-800-D, MS-2000-D| ❓ | ❓ | ❓| ❓
|
Balcony micro inverters:<br>MS400-D, MS800-D, MS2000-D| ❓ | ❓ | ❓| ❓
|
||||||
|
TITAN micro inverters:<br>TSOL-MP3000, MP2250, MS3000| ❓ | ❓ | ❓| ❓
|
||||||
```
|
```
|
||||||
Legend
|
Legend
|
||||||
➖: Firmware not available for this devices
|
➖: Firmware not available for this devices
|
||||||
@@ -173,7 +185,7 @@ Legend
|
|||||||
❓: proxy support possible but not testet
|
❓: proxy support possible but not testet
|
||||||
🚧: Proxy support in preparation
|
🚧: Proxy support in preparation
|
||||||
```
|
```
|
||||||
❗The new inverters of the G3Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. I already have such an inverter in operation and am working on the integration for the proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E`
|
❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E`
|
||||||
|
|
||||||
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids
|
|||||||
tsun.host = 'logger.talent-monitoring.com'
|
tsun.host = 'logger.talent-monitoring.com'
|
||||||
tsun.port = 5005
|
tsun.port = 5005
|
||||||
|
|
||||||
|
# configuration to reach the new tsun cloud for G3 Plus inverters
|
||||||
|
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
|
||||||
|
solarman.host = 'iot.talent-monitoring.com'
|
||||||
|
solarman.port = 10000
|
||||||
|
|
||||||
# mqtt broker configuration
|
# mqtt broker configuration
|
||||||
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
|
||||||
@@ -32,5 +36,8 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
|
|||||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||||
|
|
||||||
|
[inverters."Y170000000000001"]
|
||||||
|
#monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
|
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
||||||
|
|
||||||
|
|||||||
278
app/proxy.svg
Normal file
278
app/proxy.svg
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?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="520pt" height="1060pt"
|
||||||
|
viewBox="0.00 0.00 519.50 1060.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 1056)">
|
||||||
|
<title>G</title>
|
||||||
|
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1056 515.5,-1056 515.5,4 -4,4"/>
|
||||||
|
<!-- A0 -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>A0</title>
|
||||||
|
<polygon fill="#fff8dc" stroke="#000000" points="113.6964,-1028 5.3036,-1028 5.3036,-992 119.6964,-992 119.6964,-1022 113.6964,-1028"/>
|
||||||
|
<polyline fill="none" stroke="#000000" points="113.6964,-1028 113.6964,-1022 "/>
|
||||||
|
<polyline fill="none" stroke="#000000" points="119.6964,-1022 113.6964,-1022 "/>
|
||||||
|
<text text-anchor="middle" x="62.5" y="-1013" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
|
||||||
|
<text text-anchor="middle" x="62.5" y="-1001" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
|
||||||
|
</g>
|
||||||
|
<!-- A1 -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>A1</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="485.1817,-804 415.8183,-804 415.8183,-768 485.1817,-768 485.1817,-804"/>
|
||||||
|
<text text-anchor="middle" x="450.5" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
|
||||||
|
</g>
|
||||||
|
<!-- A2 -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>A2</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="389.5,-518 389.5,-550 511.5,-550 511.5,-518 389.5,-518"/>
|
||||||
|
<text text-anchor="start" x="440.777" y="-531" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="389.5,-462 389.5,-518 511.5,-518 511.5,-462 389.5,-462"/>
|
||||||
|
<text text-anchor="start" x="407.9875" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>ha_restarts</text>
|
||||||
|
<text text-anchor="start" x="415.7665" y="-487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__client</text>
|
||||||
|
<text text-anchor="start" x="399.3735" y="-475" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><static>__cb_MqttIsUp</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="389.5,-418 389.5,-462 511.5,-462 511.5,-418 389.5,-418"/>
|
||||||
|
<text text-anchor="start" x="412.436" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>publish()</text>
|
||||||
|
<text text-anchor="start" x="416.6045" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A1->A2 -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>A1->A2</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M450.5,-757.4632C450.5,-710.3291 450.5,-615.0013 450.5,-550.3153"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="447.0001,-757.5631 450.5,-767.5632 454.0001,-757.5632 447.0001,-757.5631"/>
|
||||||
|
</g>
|
||||||
|
<!-- A10 -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>A10</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="396.5,-282 396.5,-314 504.5,-314 504.5,-282 396.5,-282"/>
|
||||||
|
<text text-anchor="start" x="433.5535" y="-295" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="396.5,-190 396.5,-282 504.5,-282 504.5,-190 396.5,-190"/>
|
||||||
|
<text text-anchor="start" x="426.604" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
|
||||||
|
<text text-anchor="start" x="419.9405" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
|
||||||
|
<text text-anchor="start" x="410.7755" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
|
||||||
|
<text text-anchor="start" x="410.2115" y="-227" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
|
||||||
|
<text text-anchor="start" x="406.3225" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
|
||||||
|
<text text-anchor="start" x="422.1655" y="-203" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="396.5,-170 396.5,-190 504.5,-190 504.5,-170 396.5,-170"/>
|
||||||
|
</g>
|
||||||
|
<!-- A2->A10 -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>A2->A10</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M450.5,-417.8724C450.5,-385.8251 450.5,-347.2624 450.5,-314.4235"/>
|
||||||
|
</g>
|
||||||
|
<!-- A3 -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>A3</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="138.5,-1020 138.5,-1052 209.5,-1052 209.5,-1020 138.5,-1020"/>
|
||||||
|
<text text-anchor="start" x="148.445" y="-1033" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="138.5,-1000 138.5,-1020 209.5,-1020 209.5,-1000 138.5,-1000"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="138.5,-968 138.5,-1000 209.5,-1000 209.5,-968 138.5,-968"/>
|
||||||
|
<text text-anchor="start" x="155.939" y="-981" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
|
||||||
|
</g>
|
||||||
|
<!-- A4 -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>A4</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="106.5,-886 106.5,-918 240.5,-918 240.5,-886 106.5,-886"/>
|
||||||
|
<text text-anchor="start" x="153.2175" y="-899" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="106.5,-722 106.5,-886 240.5,-886 240.5,-722 106.5,-722"/>
|
||||||
|
<text text-anchor="start" x="136.8265" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
|
||||||
|
<text text-anchor="start" x="134.043" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
|
||||||
|
<text text-anchor="start" x="126.814" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
|
||||||
|
<text text-anchor="start" x="132.648" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
|
||||||
|
<text text-anchor="start" x="151.8245" y="-819" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
|
||||||
|
<text text-anchor="start" x="155.7135" y="-807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
|
||||||
|
<text text-anchor="start" x="152.6585" y="-795" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
|
||||||
|
<text text-anchor="start" x="123.489" y="-783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="122.0945" y="-771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="116.2665" y="-759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
|
||||||
|
<text text-anchor="start" x="155.7135" y="-747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
|
||||||
|
<text text-anchor="start" x="144.326" y="-735" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="106.5,-654 106.5,-722 240.5,-722 240.5,-654 106.5,-654"/>
|
||||||
|
<text text-anchor="start" x="123.2095" y="-703" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void<abstract></text>
|
||||||
|
<text text-anchor="start" x="147.9445" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
|
||||||
|
<text text-anchor="start" x="133.7725" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
|
||||||
|
<text text-anchor="start" x="132.1025" y="-667" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
|
||||||
|
</g>
|
||||||
|
<!-- A3->A4 -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>A3->A4</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M173.5,-957.5789C173.5,-945.4616 173.5,-932.0319 173.5,-918.1761"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="170.0001,-957.8673 173.5,-967.8673 177.0001,-957.8673 170.0001,-957.8673"/>
|
||||||
|
</g>
|
||||||
|
<!-- A5 -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>A5</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="243.5,-566 243.5,-598 357.5,-598 357.5,-566 243.5,-566"/>
|
||||||
|
<text text-anchor="start" x="286.608" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="243.5,-486 243.5,-566 357.5,-566 357.5,-486 243.5,-486"/>
|
||||||
|
<text text-anchor="start" x="253.263" y="-547" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
|
||||||
|
<text text-anchor="start" x="288.2775" y="-535" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
|
||||||
|
<text text-anchor="start" x="269.1" y="-523" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
|
||||||
|
<text text-anchor="start" x="272.44" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
|
||||||
|
<text text-anchor="start" x="286.612" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="243.5,-370 243.5,-486 357.5,-486 357.5,-370 243.5,-370"/>
|
||||||
|
<text text-anchor="start" x="257.9925" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
|
||||||
|
<text text-anchor="start" x="259.9325" y="-455" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
|
||||||
|
<text text-anchor="start" x="265.7765" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
|
||||||
|
<text text-anchor="start" x="253.8285" y="-431" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
|
||||||
|
<text text-anchor="start" x="255.7735" y="-419" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
|
||||||
|
<text text-anchor="start" x="264.9405" y="-407" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||||
|
<text text-anchor="start" x="285.5025" y="-383" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A4->A5 -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>A4->A5</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M233.058,-644.3739C239.5598,-628.913 246.1169,-613.3205 252.4553,-598.2481"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="229.6695,-643.4029 229.0193,-653.9777 236.1222,-646.1164 229.6695,-643.4029"/>
|
||||||
|
</g>
|
||||||
|
<!-- A6 -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>A6</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points=".5,-530 .5,-562 91.5,-562 91.5,-530 .5,-530"/>
|
||||||
|
<text text-anchor="start" x="18.495" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points=".5,-462 .5,-530 91.5,-530 91.5,-462 .5,-462"/>
|
||||||
|
<text text-anchor="start" x="30.998" y="-511" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
|
||||||
|
<text text-anchor="start" x="34.0575" y="-499" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
|
||||||
|
<text text-anchor="start" x="39.056" y="-487" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
|
||||||
|
<text text-anchor="start" x="32.112" y="-475" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points=".5,-406 .5,-462 91.5,-462 91.5,-406 .5,-406"/>
|
||||||
|
<text text-anchor="start" x="10.4405" y="-443" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
|
||||||
|
<text text-anchor="start" x="31.0025" y="-419" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A4->A6 -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>A4->A6</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M113.4937,-644.4225C101.5234,-616.1802 89.3661,-587.4965 78.7055,-562.3442"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="110.4186,-646.1364 117.5435,-653.9777 116.8636,-643.4047 110.4186,-646.1364"/>
|
||||||
|
</g>
|
||||||
|
<!-- A7 -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>A7</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="210.5,-258 210.5,-290 360.5,-290 360.5,-258 210.5,-258"/>
|
||||||
|
<text text-anchor="start" x="253.5455" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="210.5,-226 210.5,-258 360.5,-258 360.5,-226 210.5,-226"/>
|
||||||
|
<text text-anchor="start" x="220.487" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="210.5,-194 210.5,-226 360.5,-226 360.5,-194 210.5,-194"/>
|
||||||
|
<text text-anchor="start" x="270.5025" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A5->A7 -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>A5->A7</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M292.7856,-359.5407C291.2573,-334.8843 289.7409,-310.4196 288.4905,-290.2462"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="289.3053,-359.968 293.4173,-369.7323 296.2918,-359.5349 289.3053,-359.968"/>
|
||||||
|
</g>
|
||||||
|
<!-- A8 -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>A8</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="18.5,-258 18.5,-290 174.5,-290 174.5,-258 18.5,-258"/>
|
||||||
|
<text text-anchor="start" x="61.211" y="-271" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="18.5,-226 18.5,-258 174.5,-258 174.5,-226 18.5,-226"/>
|
||||||
|
<text text-anchor="start" x="28.1525" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="18.5,-194 18.5,-226 174.5,-226 174.5,-194 18.5,-194"/>
|
||||||
|
<text text-anchor="start" x="81.5025" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A6->A8 -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>A6->A8</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M64.0865,-395.8051C71.6179,-360.0682 80.0195,-320.2015 86.3817,-290.0125"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="60.6253,-395.2569 61.9878,-405.7637 67.4748,-396.7004 60.6253,-395.2569"/>
|
||||||
|
</g>
|
||||||
|
<!-- A7->A7 -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>A7->A7</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M360.6684,-272.6238C371.3394,-267.6708 378.5,-257.4629 378.5,-242 378.5,-231.1277 374.9599,-222.8533 369.1486,-217.1769"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="360.6684,-211.3762 371.4628,-213.3079 364.7953,-214.1991 368.9222,-217.0221 368.9222,-217.0221 368.9222,-217.0221 364.7953,-214.1991 366.3816,-220.7363 360.6684,-211.3762 360.6684,-211.3762"/>
|
||||||
|
<text text-anchor="middle" x="380.4014" y="-211.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||||
|
<text text-anchor="middle" x="372.7075" y="-253.6532" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
|
</g>
|
||||||
|
<!-- A11 -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>A11</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="306.5,-88 306.5,-120 428.5,-120 428.5,-88 306.5,-88"/>
|
||||||
|
<text text-anchor="start" x="343.8845" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="306.5,-56 306.5,-88 428.5,-88 428.5,-56 306.5,-56"/>
|
||||||
|
<text text-anchor="start" x="336.9355" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="306.5,0 306.5,-56 428.5,-56 428.5,0 306.5,0"/>
|
||||||
|
<text text-anchor="start" x="316.1035" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||||
|
<text text-anchor="start" x="352.5025" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A7->A11 -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>A7->A11</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M311.4044,-184.5048C320.6254,-164.0387 331.0304,-140.9447 340.3527,-120.2539"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="308.176,-183.1501 307.2592,-193.7052 314.5582,-186.0256 308.176,-183.1501"/>
|
||||||
|
</g>
|
||||||
|
<!-- A8->A8 -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>A8->A8</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M174.8471,-272.2739C185.4443,-267.1987 192.5,-257.1074 192.5,-242 192.5,-231.3776 189.0118,-223.2351 183.2569,-217.5725"/>
|
||||||
|
<polygon fill="#000000" stroke="#000000" points="174.8471,-211.7261 185.6266,-213.7393 178.9525,-214.5802 183.0579,-217.4342 183.0579,-217.4342 183.0579,-217.4342 178.9525,-214.5802 180.4893,-221.1291 174.8471,-211.7261 174.8471,-211.7261"/>
|
||||||
|
<text text-anchor="middle" x="194.5548" y="-212.1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
|
||||||
|
<text text-anchor="middle" x="186.7174" y="-253.1774" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
|
||||||
|
</g>
|
||||||
|
<!-- A12 -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>A12</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.5,-88 142.5,-120 264.5,-120 264.5,-88 142.5,-88"/>
|
||||||
|
<text text-anchor="start" x="176.55" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.5,-56 142.5,-88 264.5,-88 264.5,-56 142.5,-56"/>
|
||||||
|
<text text-anchor="start" x="172.9355" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="142.5,0 142.5,-56 264.5,-56 264.5,0 142.5,0"/>
|
||||||
|
<text text-anchor="start" x="152.1035" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
|
||||||
|
<text text-anchor="start" x="188.5025" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A8->A12 -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>A8->A12</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M129.9773,-185.0573C142.0896,-164.4551 155.802,-141.1311 168.076,-120.2539"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="126.9441,-183.3107 124.8931,-193.7052 132.9785,-186.8585 126.9441,-183.3107"/>
|
||||||
|
</g>
|
||||||
|
<!-- A9 -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>A9</title>
|
||||||
|
<polygon fill="none" stroke="#000000" points="109.5,-572 109.5,-604 225.5,-604 225.5,-572 109.5,-572"/>
|
||||||
|
<text text-anchor="start" x="137.774" y="-585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="109.5,-492 109.5,-572 225.5,-572 225.5,-492 109.5,-492"/>
|
||||||
|
<text text-anchor="start" x="153.053" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
|
||||||
|
<text text-anchor="start" x="155.283" y="-541" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
|
||||||
|
<text text-anchor="start" x="157.497" y="-529" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
|
||||||
|
<text text-anchor="start" x="153.053" y="-517" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
|
||||||
|
<text text-anchor="start" x="153.608" y="-505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
|
||||||
|
<polygon fill="none" stroke="#000000" points="109.5,-364 109.5,-492 225.5,-492 225.5,-364 109.5,-364"/>
|
||||||
|
<text text-anchor="start" x="119.1575" y="-473" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>server_loop()</text>
|
||||||
|
<text text-anchor="start" x="121.378" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>client_loop()</text>
|
||||||
|
<text text-anchor="start" x="139.154" y="-449" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"><async>loop</text>
|
||||||
|
<text text-anchor="start" x="155.282" y="-437" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
|
||||||
|
<text text-anchor="start" x="152.5025" y="-425" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
<text text-anchor="start" x="132.7705" y="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
|
||||||
|
<text text-anchor="start" x="132.221" y="-389" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
|
||||||
|
<text text-anchor="start" x="126.107" y="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
|
||||||
|
</g>
|
||||||
|
<!-- A9->A7 -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>A9->A7</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M230.0233,-355.7743C241.4579,-332.3236 252.7154,-309.2362 262.0696,-290.0521"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="226.8727,-354.25 225.6358,-364.7724 233.1645,-357.318 226.8727,-354.25"/>
|
||||||
|
</g>
|
||||||
|
<!-- A9->A8 -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>A9->A8</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M129.3389,-353.9299C122.6561,-331.1516 116.0996,-308.8044 110.6242,-290.1415"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="126.0343,-355.0988 132.208,-363.709 132.7512,-353.1281 126.0343,-355.0988"/>
|
||||||
|
</g>
|
||||||
|
<!-- A10->A11 -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>A10->A11</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M413.3164,-160.4648C407.1223,-146.8826 400.7985,-133.016 394.9051,-120.0931"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="410.2431,-162.1611 417.577,-169.8074 416.6121,-159.2566 410.2431,-162.1611"/>
|
||||||
|
</g>
|
||||||
|
<!-- A10->A12 -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>A10->A12</title>
|
||||||
|
<path fill="none" stroke="#000000" d="M388.6131,-170.9072C388.2425,-170.6025 387.8715,-170.3001 387.5,-170 351.9061,-141.2441 336.8037,-143.4318 297.5,-120 286.7739,-113.6054 275.4857,-106.6243 264.5994,-99.7581"/>
|
||||||
|
<polygon fill="none" stroke="#000000" points="386.4756,-173.6866 396.3191,-177.6053 391.0678,-168.4034 386.4756,-173.6866"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 22 KiB |
21
app/proxy.yuml
Normal file
21
app/proxy.yuml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// {type:class}
|
||||||
|
// {direction:topDown}
|
||||||
|
// {generate:true}
|
||||||
|
|
||||||
|
[note: You can stick notes on diagrams too!{bg:cornsilk}]
|
||||||
|
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
|
||||||
|
|
||||||
|
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
||||||
|
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
||||||
|
[Message]^[SolarmanV5|control;serial;snr;switch|msg_unknown();;close()]
|
||||||
|
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
|
||||||
|
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
|
||||||
|
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();__async_write();__async_forward()]^[ConnectionG3]
|
||||||
|
[AsyncStream]^[ConnectionG3P]
|
||||||
|
[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
|
||||||
|
[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
|
||||||
|
[Mqtt]-[Inverter]
|
||||||
|
[ConnectionG3]^[InverterG3]
|
||||||
|
[ConnectionG3]has-0..1>[ConnectionG3]
|
||||||
|
[ConnectionG3P]^[InverterG3P]
|
||||||
|
[ConnectionG3P]has-0..1>[ConnectionG3P]
|
||||||
@@ -1,27 +1,57 @@
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
# from config import Config
|
from messages import hex_dump_memory
|
||||||
# import gc
|
|
||||||
from messages import Message, hex_dump_memory
|
|
||||||
|
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
|
|
||||||
class AsyncStream(Message):
|
class AsyncStream():
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr, remote_stream, server_side: bool,
|
def __init__(self, reader, writer, addr) -> None:
|
||||||
id_str=b'') -> None:
|
logger.debug('AsyncStream.__init__')
|
||||||
super().__init__(server_side, id_str)
|
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
self.writer = writer
|
self.writer = writer
|
||||||
self.remoteStream = remote_stream
|
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.r_addr = ''
|
self.r_addr = ''
|
||||||
self.l_addr = ''
|
self.l_addr = ''
|
||||||
|
|
||||||
'''
|
async def server_loop(self, addr):
|
||||||
Our puplic methods
|
'''Loop for receiving messages from the inverter (server-side)'''
|
||||||
'''
|
logging.info(f'Accept connection from {addr}')
|
||||||
|
self.inc_counter('Inverter_Cnt')
|
||||||
|
await self.loop()
|
||||||
|
self.dec_counter('Inverter_Cnt')
|
||||||
|
logging.info(f'Server loop stopped for r{self.r_addr}')
|
||||||
|
|
||||||
|
# if the server connection closes, we also have to disconnect
|
||||||
|
# the connection to te TSUN cloud
|
||||||
|
if self.remoteStream:
|
||||||
|
logging.debug("disconnect client connection")
|
||||||
|
self.remoteStream.disc()
|
||||||
|
try:
|
||||||
|
await self._async_publ_mqtt_proxy_stat('proxy')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def client_loop(self, addr):
|
||||||
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
|
clientStream = await self.remoteStream.loop()
|
||||||
|
logging.info(f'Client loop stopped for l{clientStream.l_addr}')
|
||||||
|
|
||||||
|
# if the client connection closes, we don't touch the server
|
||||||
|
# connection. Instead we erase the client connection stream,
|
||||||
|
# thus on the next received packet from the inverter, we can
|
||||||
|
# establish a new connection to the TSUN cloud
|
||||||
|
|
||||||
|
# erase backlink to inverter
|
||||||
|
clientStream.remoteStream = None
|
||||||
|
|
||||||
|
if self.remoteStream == clientStream:
|
||||||
|
# logging.debug(f'Client l{clientStream.l_addr} refs:'
|
||||||
|
# f' {gc.get_referrers(clientStream)}')
|
||||||
|
# than erase client connection
|
||||||
|
self.remoteStream = None
|
||||||
|
|
||||||
async def loop(self):
|
async def loop(self):
|
||||||
self.r_addr = self.writer.get_extra_info('peername')
|
self.r_addr = self.writer.get_extra_info('peername')
|
||||||
self.l_addr = self.writer.get_extra_info('sockname')
|
self.l_addr = self.writer.get_extra_info('sockname')
|
||||||
@@ -52,15 +82,12 @@ class AsyncStream(Message):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def disc(self) -> None:
|
def disc(self) -> None:
|
||||||
logger.debug(f'in AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
|
||||||
self.writer.close()
|
self.writer.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
logger.debug(f'in AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
|
||||||
self.writer.close()
|
self.writer.close()
|
||||||
super().close() # call close handler in the parent class
|
|
||||||
|
|
||||||
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our private methods
|
Our private methods
|
||||||
@@ -86,8 +113,7 @@ class AsyncStream(Message):
|
|||||||
if not self.remoteStream:
|
if not self.remoteStream:
|
||||||
await self.async_create_remote()
|
await self.async_create_remote()
|
||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
self.remoteStream._init_new_client_conn(self.contact_name,
|
if self.remoteStream._init_new_client_conn():
|
||||||
self.contact_mail)
|
|
||||||
await self.remoteStream.__async_write()
|
await self.remoteStream.__async_write()
|
||||||
|
|
||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
@@ -99,11 +125,6 @@ class AsyncStream(Message):
|
|||||||
await self.remoteStream.writer.drain()
|
await self.remoteStream.writer.drain()
|
||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
|
|
||||||
async def async_create_remote(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def async_publ_mqtt(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
logging.debug(f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
logger.debug(
|
||||||
|
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class Config():
|
|||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
},
|
},
|
||||||
|
'solarman': {
|
||||||
|
'enabled': Use(bool),
|
||||||
|
'host': Use(str),
|
||||||
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
|
},
|
||||||
'mqtt': {
|
'mqtt': {
|
||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||||
@@ -34,6 +39,7 @@ class Config():
|
|||||||
},
|
},
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
||||||
|
Optional('monitor_sn', default=0): Use(int),
|
||||||
Optional('node_id', default=""): And(Use(str),
|
Optional('node_id', default=""): And(Use(str),
|
||||||
Use(lambda s: s + '/'
|
Use(lambda s: s + '/'
|
||||||
if len(s) > 0 and
|
if len(s) > 0 and
|
||||||
@@ -67,6 +73,8 @@ class Config():
|
|||||||
usr_config = tomllib.load(f)
|
usr_config = tomllib.load(f)
|
||||||
|
|
||||||
config['tsun'] = def_config['tsun'] | usr_config['tsun']
|
config['tsun'] = def_config['tsun'] | usr_config['tsun']
|
||||||
|
config['solarman'] = def_config['solarman'] | \
|
||||||
|
usr_config['solarman']
|
||||||
config['mqtt'] = def_config['mqtt'] | usr_config['mqtt']
|
config['mqtt'] = def_config['mqtt'] | usr_config['mqtt']
|
||||||
config['ha'] = def_config['ha'] | usr_config['ha']
|
config['ha'] = def_config['ha'] | usr_config['ha']
|
||||||
config['inverters'] = def_config['inverters'] | \
|
config['inverters'] = def_config['inverters'] | \
|
||||||
|
|||||||
36
app/src/gen3/connection_g3.py
Normal file
36
app/src/gen3/connection_g3.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
# import gc
|
||||||
|
from async_stream import AsyncStream
|
||||||
|
from gen3.talent import Talent
|
||||||
|
|
||||||
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionG3(AsyncStream, Talent):
|
||||||
|
|
||||||
|
def __init__(self, reader, writer, addr, remote_stream, server_side: bool,
|
||||||
|
id_str=b'') -> None:
|
||||||
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
|
Talent.__init__(self, server_side, id_str)
|
||||||
|
|
||||||
|
self.remoteStream = remote_stream
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our puplic methods
|
||||||
|
'''
|
||||||
|
def close(self):
|
||||||
|
AsyncStream.close(self)
|
||||||
|
Talent.close(self)
|
||||||
|
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
|
async def async_create_remote(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def async_publ_mqtt(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our private methods
|
||||||
|
'''
|
||||||
|
def __del__(self):
|
||||||
|
super().__del__()
|
||||||
167
app/src/gen3/infos_g3.py
Normal file
167
app/src/gen3/infos_g3.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterMap:
|
||||||
|
map = {
|
||||||
|
0x00092ba8: Register.COLLECTOR_FW_VERSION,
|
||||||
|
0x000927c0: Register.CHIP_TYPE,
|
||||||
|
0x00092f90: Register.CHIP_MODEL,
|
||||||
|
0x00095a88: Register.TRACE_URL,
|
||||||
|
0x00095aec: Register.LOGGER_URL,
|
||||||
|
0x0000000a: Register.PRODUCT_NAME,
|
||||||
|
0x00000014: Register.MANUFACTURER,
|
||||||
|
0x0000001e: Register.VERSION,
|
||||||
|
0x00000028: Register.SERIAL_NUMBER,
|
||||||
|
0x00000032: Register.EQUIPMENT_MODEL,
|
||||||
|
0x00013880: Register.NO_INPUTS,
|
||||||
|
0xffffff00: Register.INVERTER_CNT,
|
||||||
|
0xffffff01: Register.UNKNOWN_SNR,
|
||||||
|
0xffffff02: Register.UNKNOWN_MSG,
|
||||||
|
0xffffff03: Register.INVALID_DATA_TYPE,
|
||||||
|
0xffffff04: Register.INTERNAL_ERROR,
|
||||||
|
0xffffff05: Register.UNKNOWN_CTRL,
|
||||||
|
0xffffff06: Register.OTA_START_MSG,
|
||||||
|
0xffffff07: Register.SW_EXCEPTION,
|
||||||
|
0xfffffffe: Register.TEST_REG1,
|
||||||
|
0xffffffff: Register.TEST_REG2,
|
||||||
|
0x00000640: Register.OUTPUT_POWER,
|
||||||
|
0x000005dc: Register.RATED_POWER,
|
||||||
|
0x00000514: Register.INVERTER_TEMP,
|
||||||
|
0x000006a4: Register.PV1_VOLTAGE,
|
||||||
|
0x00000708: Register.PV1_CURRENT,
|
||||||
|
0x0000076c: Register.PV1_POWER,
|
||||||
|
0x000007d0: Register.PV2_VOLTAGE,
|
||||||
|
0x00000834: Register.PV2_CURRENT,
|
||||||
|
0x00000898: Register.PV2_POWER,
|
||||||
|
0x000008fc: Register.PV3_VOLTAGE,
|
||||||
|
0x00000960: Register.PV3_CURRENT,
|
||||||
|
0x000009c4: Register.PV3_POWER,
|
||||||
|
0x00000a28: Register.PV4_VOLTAGE,
|
||||||
|
0x00000a8c: Register.PV4_CURRENT,
|
||||||
|
0x00000af0: Register.PV4_POWER,
|
||||||
|
0x00000c1c: Register.PV1_DAILY_GENERATION,
|
||||||
|
0x00000c80: Register.PV1_TOTAL_GENERATION,
|
||||||
|
0x00000ce4: Register.PV2_DAILY_GENERATION,
|
||||||
|
0x00000d48: Register.PV2_TOTAL_GENERATION,
|
||||||
|
0x00000dac: Register.PV3_DAILY_GENERATION,
|
||||||
|
0x00000e10: Register.PV3_TOTAL_GENERATION,
|
||||||
|
0x00000e74: Register.PV4_DAILY_GENERATION,
|
||||||
|
0x00000ed8: Register.PV4_TOTAL_GENERATION,
|
||||||
|
0x00000b54: Register.DAILY_GENERATION,
|
||||||
|
0x00000bb8: Register.TOTAL_GENERATION,
|
||||||
|
0x000003e8: Register.GRID_VOLTAGE,
|
||||||
|
0x0000044c: Register.GRID_CURRENT,
|
||||||
|
0x000004b0: Register.GRID_FREQUENCY,
|
||||||
|
0x000cfc38: Register.CONNECT_COUNT,
|
||||||
|
0x000c3500: Register.SIGNAL_STRENGTH,
|
||||||
|
0x000c96a8: Register.POWER_ON_TIME,
|
||||||
|
0x000d0020: Register.COLLECT_INTERVAL,
|
||||||
|
0x000cf850: Register.DATA_UP_INTERVAL,
|
||||||
|
0x000c7f38: Register.COMMUNICATION_TYPE,
|
||||||
|
0x00000191: Register.EVENT_401,
|
||||||
|
0x00000192: Register.EVENT_402,
|
||||||
|
0x00000193: Register.EVENT_403,
|
||||||
|
0x00000194: Register.EVENT_404,
|
||||||
|
0x00000195: Register.EVENT_405,
|
||||||
|
0x00000196: Register.EVENT_406,
|
||||||
|
0x00000197: Register.EVENT_407,
|
||||||
|
0x00000198: Register.EVENT_408,
|
||||||
|
0x00000199: Register.EVENT_409,
|
||||||
|
0x0000019a: Register.EVENT_410,
|
||||||
|
0x0000019b: Register.EVENT_411,
|
||||||
|
0x0000019c: Register.EVENT_412,
|
||||||
|
0x0000019d: Register.EVENT_413,
|
||||||
|
0x0000019e: Register.EVENT_414,
|
||||||
|
0x0000019f: Register.EVENT_415,
|
||||||
|
0x000001a0: Register.EVENT_416,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InfosG3(Infos):
|
||||||
|
|
||||||
|
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
|
||||||
|
sug_area: str = '') \
|
||||||
|
-> Generator[tuple[dict, str], None, None]:
|
||||||
|
'''Generator function yields a json register struct for home-assistant
|
||||||
|
auto configuration and a unique entity string
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
prfx:str ==> MQTT prefix for the home assistant 'stat_t string
|
||||||
|
snr:str ==> serial number of the inverter, used to build unique
|
||||||
|
entity strings
|
||||||
|
sug_area:str ==> suggested area string from the config file'''
|
||||||
|
# iterate over RegisterMap.map and get the register values
|
||||||
|
for reg in RegisterMap.map.values():
|
||||||
|
res = self.ha_conf(reg, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||||
|
if res:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
def parse(self, buf, ind=0) -> Generator[tuple[str, bool], None, None]:
|
||||||
|
'''parse a data sequence received from the inverter and
|
||||||
|
stores the values in Infos.db
|
||||||
|
|
||||||
|
buf: buffer of the sequence to parse'''
|
||||||
|
result = struct.unpack_from('!l', buf, ind)
|
||||||
|
elms = result[0]
|
||||||
|
i = 0
|
||||||
|
ind += 4
|
||||||
|
while i < elms:
|
||||||
|
result = struct.unpack_from('!lB', buf, ind)
|
||||||
|
addr = result[0]
|
||||||
|
if addr not in RegisterMap.map:
|
||||||
|
info_id = -1
|
||||||
|
else:
|
||||||
|
info_id = RegisterMap.map[addr]
|
||||||
|
data_type = result[1]
|
||||||
|
ind += 5
|
||||||
|
|
||||||
|
if data_type == 0x54: # 'T' -> Pascal-String
|
||||||
|
str_len = buf[ind]
|
||||||
|
result = struct.unpack_from(f'!{str_len+1}p', buf,
|
||||||
|
ind)[0].decode(encoding='ascii',
|
||||||
|
errors='replace')
|
||||||
|
ind += str_len+1
|
||||||
|
|
||||||
|
elif data_type == 0x49: # 'I' -> int32
|
||||||
|
result = struct.unpack_from('!l', buf, ind)[0]
|
||||||
|
ind += 4
|
||||||
|
|
||||||
|
elif data_type == 0x53: # 'S' -> short
|
||||||
|
result = struct.unpack_from('!h', buf, ind)[0]
|
||||||
|
ind += 2
|
||||||
|
|
||||||
|
elif data_type == 0x46: # 'F' -> float32
|
||||||
|
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
||||||
|
ind += 4
|
||||||
|
|
||||||
|
elif data_type == 0x4c: # 'L' -> int64
|
||||||
|
result = struct.unpack_from('!q', buf, ind)[0]
|
||||||
|
ind += 8
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.inc_counter('Invalid_Data_Type')
|
||||||
|
logging.error(f"Infos.parse: data_type: {data_type}"
|
||||||
|
" not supported")
|
||||||
|
return
|
||||||
|
|
||||||
|
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||||
|
|
||||||
|
if keys:
|
||||||
|
name, update = self.update_db(keys, must_incr, result)
|
||||||
|
yield keys[0], update
|
||||||
|
else:
|
||||||
|
update = False
|
||||||
|
name = str(f'info-id.0x{addr:x}')
|
||||||
|
|
||||||
|
self.tracer.log(level, f'{name} : {result}{unit}'
|
||||||
|
f' update: {update}')
|
||||||
|
|
||||||
|
i += 1
|
||||||
126
app/src/gen3/inverter_g3.py
Normal file
126
app/src/gen3/inverter_g3.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import json
|
||||||
|
from config import Config
|
||||||
|
from inverter import Inverter
|
||||||
|
from gen3.connection_g3 import ConnectionG3
|
||||||
|
from aiomqtt import MqttCodeError
|
||||||
|
from infos import Infos
|
||||||
|
|
||||||
|
# import gc
|
||||||
|
|
||||||
|
# logger = logging.getLogger('conn')
|
||||||
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
|
class InverterG3(Inverter, ConnectionG3):
|
||||||
|
'''class Inverter is a derivation of an Async_Stream
|
||||||
|
|
||||||
|
The class has some class method for managing common resources like a
|
||||||
|
connection to the MQTT broker or proxy error counter which are common
|
||||||
|
for all inverter connection
|
||||||
|
|
||||||
|
Instances of the class are connections to an inverter and can have an
|
||||||
|
optional link to an remote connection to the TSUN cloud. A remote
|
||||||
|
connection dies with the inverter connection.
|
||||||
|
|
||||||
|
class methods:
|
||||||
|
class_init(): initialize the common resources of the proxy (MQTT
|
||||||
|
broker, Proxy DB, etc). Must be called before the
|
||||||
|
first inverter instance can be created
|
||||||
|
class_close(): release the common resources of the proxy. Should not
|
||||||
|
be called before any instances of the class are
|
||||||
|
destroyed
|
||||||
|
|
||||||
|
methods:
|
||||||
|
server_loop(addr): Async loop method for receiving messages from the
|
||||||
|
inverter (server-side)
|
||||||
|
client_loop(addr): Async loop method for receiving messages from the
|
||||||
|
TSUN cloud (client-side)
|
||||||
|
async_create_remote(): Establish a client connection to the TSUN cloud
|
||||||
|
async_publ_mqtt(): Publish data to MQTT broker
|
||||||
|
close(): Release method which must be called before a instance can be
|
||||||
|
destroyed
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, reader, writer, addr):
|
||||||
|
super().__init__(reader, writer, addr, None, True)
|
||||||
|
self.__ha_restarts = -1
|
||||||
|
|
||||||
|
async def async_create_remote(self) -> None:
|
||||||
|
'''Establish a client connection to the TSUN cloud'''
|
||||||
|
tsun = Config.get('tsun')
|
||||||
|
host = tsun['host']
|
||||||
|
port = tsun['port']
|
||||||
|
addr = (host, port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f'Connected to {addr}')
|
||||||
|
connect = asyncio.open_connection(host, port)
|
||||||
|
reader, writer = await connect
|
||||||
|
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
||||||
|
False, self.id_str)
|
||||||
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
|
except (ConnectionRefusedError, TimeoutError) as error:
|
||||||
|
logging.info(f'{error}')
|
||||||
|
except Exception:
|
||||||
|
self.inc_counter('SW_Exception')
|
||||||
|
logging.error(
|
||||||
|
f"Inverter: Exception for {addr}:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
async def async_publ_mqtt(self) -> None:
|
||||||
|
'''publish data to MQTT broker'''
|
||||||
|
# check if new inverter or collector infos are available or when the
|
||||||
|
# home assistant has changed the status back to online
|
||||||
|
try:
|
||||||
|
if (('inverter' in self.new_data and self.new_data['inverter'])
|
||||||
|
or ('collector' in self.new_data and
|
||||||
|
self.new_data['collector'])
|
||||||
|
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||||
|
await self._register_proxy_stat_home_assistant()
|
||||||
|
await self.__register_home_assistant()
|
||||||
|
self.__ha_restarts = self.mqtt.ha_restarts
|
||||||
|
|
||||||
|
for key in self.new_data:
|
||||||
|
await self.__async_publ_mqtt_packet(key)
|
||||||
|
for key in Infos.new_stat_data:
|
||||||
|
await self._async_publ_mqtt_proxy_stat(key)
|
||||||
|
|
||||||
|
except MqttCodeError as error:
|
||||||
|
logging.error(f'Mqtt except: {error}')
|
||||||
|
except Exception:
|
||||||
|
self.inc_counter('SW_Exception')
|
||||||
|
logging.error(
|
||||||
|
f"Inverter: Exception:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
async def __async_publ_mqtt_packet(self, key):
|
||||||
|
db = self.db.db
|
||||||
|
if key in db and self.new_data[key]:
|
||||||
|
data_json = json.dumps(db[key])
|
||||||
|
node_id = self.node_id
|
||||||
|
logger_mqtt.debug(f'{key}: {data_json}')
|
||||||
|
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
|
self.new_data[key] = False
|
||||||
|
|
||||||
|
async def __register_home_assistant(self) -> None:
|
||||||
|
'''register all our topics at home assistant'''
|
||||||
|
for data_json, component, node_id, id in self.db.ha_confs(
|
||||||
|
self.entity_prfx, self.node_id, self.unique_id,
|
||||||
|
self.sug_area):
|
||||||
|
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
||||||
|
f" node_id:'{node_id}' {data_json}")
|
||||||
|
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
||||||
|
f"/{node_id}{id}/config", data_json)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
|
||||||
|
super().close() # call close handler in the parent class
|
||||||
|
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
logging.debug("InverterG3.__del__")
|
||||||
|
super().__del__()
|
||||||
353
app/src/gen3/talent.py
Normal file
353
app/src/gen3/talent.py
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import struct
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
if __name__ == "app.src.gen3.talent":
|
||||||
|
from app.src.messages import hex_dump_memory, Message
|
||||||
|
from app.src.config import Config
|
||||||
|
from app.src.gen3.infos_g3 import InfosG3
|
||||||
|
else: # pragma: no cover
|
||||||
|
from messages import hex_dump_memory, Message
|
||||||
|
from config import Config
|
||||||
|
from gen3.infos_g3 import InfosG3
|
||||||
|
|
||||||
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
|
|
||||||
|
class Control:
|
||||||
|
def __init__(self, ctrl: int):
|
||||||
|
self.ctrl = ctrl
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return self.ctrl
|
||||||
|
|
||||||
|
def is_ind(self) -> bool:
|
||||||
|
return (self.ctrl == 0x91)
|
||||||
|
|
||||||
|
def is_req(self) -> bool:
|
||||||
|
return (self.ctrl == 0x70)
|
||||||
|
|
||||||
|
def is_resp(self) -> bool:
|
||||||
|
return (self.ctrl == 0x99)
|
||||||
|
|
||||||
|
|
||||||
|
class Talent(Message):
|
||||||
|
|
||||||
|
def __init__(self, server_side: bool, id_str=b''):
|
||||||
|
super().__init__(server_side)
|
||||||
|
self.await_conn_resp_cnt = 0
|
||||||
|
self.id_str = id_str
|
||||||
|
self.contact_name = b''
|
||||||
|
self.contact_mail = b''
|
||||||
|
self.db = InfosG3()
|
||||||
|
self.switch = {
|
||||||
|
0x00: self.msg_contact_info,
|
||||||
|
0x13: self.msg_ota_update,
|
||||||
|
0x22: self.msg_get_time,
|
||||||
|
0x71: self.msg_collector_data,
|
||||||
|
0x04: self.msg_inverter_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our puplic methods
|
||||||
|
'''
|
||||||
|
def close(self) -> None:
|
||||||
|
logging.debug('Talent.close()')
|
||||||
|
# we have refernces 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()
|
||||||
|
|
||||||
|
def set_serial_no(self, serial_no: str):
|
||||||
|
|
||||||
|
if self.unique_id == serial_no:
|
||||||
|
logger.debug(f'SerialNo: {serial_no}')
|
||||||
|
else:
|
||||||
|
inverters = Config.get('inverters')
|
||||||
|
# logger.debug(f'Inverters: {inverters}')
|
||||||
|
|
||||||
|
if serial_no in inverters:
|
||||||
|
inv = inverters[serial_no]
|
||||||
|
self.node_id = inv['node_id']
|
||||||
|
self.sug_area = inv['suggested_area']
|
||||||
|
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||||
|
else:
|
||||||
|
self.node_id = ''
|
||||||
|
self.sug_area = ''
|
||||||
|
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||||
|
self.inc_counter('Unknown_SNR')
|
||||||
|
self.unique_id = None
|
||||||
|
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
||||||
|
return
|
||||||
|
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||||
|
|
||||||
|
self.unique_id = serial_no
|
||||||
|
|
||||||
|
def read(self) -> None:
|
||||||
|
self._read()
|
||||||
|
|
||||||
|
if not self.header_valid:
|
||||||
|
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
||||||
|
|
||||||
|
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||||
|
self.data_len):
|
||||||
|
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
||||||
|
self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
self.set_serial_no(self.id_str.decode("utf-8"))
|
||||||
|
self.__dispatch_msg()
|
||||||
|
self.__flush_recv_msg()
|
||||||
|
return
|
||||||
|
|
||||||
|
def forward(self, buffer, buflen) -> None:
|
||||||
|
tsun = Config.get('tsun')
|
||||||
|
if tsun['enabled']:
|
||||||
|
self._forward_buffer = buffer[:buflen]
|
||||||
|
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
|
||||||
|
buffer, buflen)
|
||||||
|
|
||||||
|
self.__parse_header(self._forward_buffer,
|
||||||
|
len(self._forward_buffer))
|
||||||
|
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||||
|
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||||
|
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
return
|
||||||
|
|
||||||
|
def _init_new_client_conn(self) -> bool:
|
||||||
|
contact_name = self.contact_name
|
||||||
|
contact_mail = self.contact_mail
|
||||||
|
logger.info(f'name: {contact_name} mail: {contact_mail}')
|
||||||
|
self.msg_id = 0
|
||||||
|
self.await_conn_resp_cnt += 1
|
||||||
|
self.__build_header(0x91)
|
||||||
|
self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
|
||||||
|
f'{len(contact_mail)+1}p',
|
||||||
|
contact_name, contact_mail)
|
||||||
|
|
||||||
|
self.__finish_send_msg()
|
||||||
|
return True
|
||||||
|
|
||||||
|
'''
|
||||||
|
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): # pragma: no cover
|
||||||
|
if False:
|
||||||
|
# utc as epoche
|
||||||
|
ts = time.time()
|
||||||
|
else:
|
||||||
|
# convert localtime in epoche
|
||||||
|
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||||
|
return round(ts*1000)
|
||||||
|
|
||||||
|
# check if there is a complete header in the buffer, parse it
|
||||||
|
# and set
|
||||||
|
# self.header_len
|
||||||
|
# self.data_len
|
||||||
|
# self.id_str
|
||||||
|
# self.ctrl
|
||||||
|
# self.msg_id
|
||||||
|
#
|
||||||
|
# if the header is incomplete, than self.header_len is still 0
|
||||||
|
#
|
||||||
|
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
||||||
|
|
||||||
|
if (buf_len < 5): # enough bytes to read len and id_len?
|
||||||
|
return
|
||||||
|
result = struct.unpack_from('!lB', buf, 0)
|
||||||
|
len = result[0] # len of complete message
|
||||||
|
id_len = result[1] # len of variable id string
|
||||||
|
|
||||||
|
hdr_len = 5+id_len+2
|
||||||
|
|
||||||
|
if (buf_len < hdr_len): # enough bytes for complete header?
|
||||||
|
return
|
||||||
|
|
||||||
|
result = struct.unpack_from(f'!{id_len+1}pBB', buf, 4)
|
||||||
|
|
||||||
|
# store parsed header values in the class
|
||||||
|
self.id_str = result[0]
|
||||||
|
self.ctrl = Control(result[1])
|
||||||
|
self.msg_id = result[2]
|
||||||
|
self.data_len = len-id_len-3
|
||||||
|
self.header_len = hdr_len
|
||||||
|
self.header_valid = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def __build_header(self, ctrl) -> None:
|
||||||
|
self.send_msg_ofs = len(self._send_buffer)
|
||||||
|
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
|
||||||
|
0, self.id_str, ctrl, self.msg_id)
|
||||||
|
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||||
|
logger.info(self.__flow_str(self.server_side, 'tx') +
|
||||||
|
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
|
||||||
|
def __finish_send_msg(self) -> None:
|
||||||
|
_len = len(self._send_buffer) - self.send_msg_ofs
|
||||||
|
struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4)
|
||||||
|
|
||||||
|
def __dispatch_msg(self) -> None:
|
||||||
|
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||||
|
if self.unique_id:
|
||||||
|
logger.info(self.__flow_str(self.server_side, 'rx') +
|
||||||
|
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
fnc()
|
||||||
|
else:
|
||||||
|
logger.info(self.__flow_str(self.server_side, 'drop') +
|
||||||
|
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
|
||||||
|
def __flush_recv_msg(self) -> None:
|
||||||
|
self._recv_buffer = self._recv_buffer[(self.header_len+self.data_len):]
|
||||||
|
self.header_valid = False
|
||||||
|
|
||||||
|
'''
|
||||||
|
Message handler methods
|
||||||
|
'''
|
||||||
|
def msg_contact_info(self):
|
||||||
|
if self.ctrl.is_ind():
|
||||||
|
if self.server_side and self.__process_contact_info():
|
||||||
|
self.__build_header(0x91)
|
||||||
|
self._send_buffer += b'\x01'
|
||||||
|
self.__finish_send_msg()
|
||||||
|
# don't forward this contact info here, we will build one
|
||||||
|
# when the remote connection is established
|
||||||
|
elif self.await_conn_resp_cnt > 0:
|
||||||
|
self.await_conn_resp_cnt -= 1
|
||||||
|
else:
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def __process_contact_info(self) -> bool:
|
||||||
|
result = struct.unpack_from('!B', self._recv_buffer, self.header_len)
|
||||||
|
name_len = result[0]
|
||||||
|
if self.data_len < name_len+2:
|
||||||
|
return False
|
||||||
|
result = struct.unpack_from(f'!{name_len+1}pB', self._recv_buffer,
|
||||||
|
self.header_len)
|
||||||
|
self.contact_name = result[0]
|
||||||
|
mail_len = result[1]
|
||||||
|
logger.info(f'name: {self.contact_name}')
|
||||||
|
|
||||||
|
result = struct.unpack_from(f'!{mail_len+1}p', self._recv_buffer,
|
||||||
|
self.header_len+name_len+1)
|
||||||
|
self.contact_mail = result[0]
|
||||||
|
logger.info(f'mail: {self.contact_mail}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def msg_get_time(self):
|
||||||
|
tsun = Config.get('tsun')
|
||||||
|
if tsun['enabled']:
|
||||||
|
if self.ctrl.is_ind():
|
||||||
|
if self.data_len >= 8:
|
||||||
|
ts = self._timestamp()
|
||||||
|
result = struct.unpack_from('!q', self._recv_buffer,
|
||||||
|
self.header_len)
|
||||||
|
logger.debug(f'tsun-time: {result[0]:08x}'
|
||||||
|
f' proxy-time: {ts:08x}')
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
else:
|
||||||
|
if self.ctrl.is_ind():
|
||||||
|
if self.data_len == 0:
|
||||||
|
ts = self._timestamp()
|
||||||
|
logger.debug(f'time: {ts:08x}')
|
||||||
|
|
||||||
|
self.__build_header(0x91)
|
||||||
|
self._send_buffer += struct.pack('!q', ts)
|
||||||
|
self.__finish_send_msg()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
|
def parse_msg_header(self):
|
||||||
|
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
||||||
|
|
||||||
|
data_id = result[0] # len of complete message
|
||||||
|
id_len = result[1] # len of variable id string
|
||||||
|
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
|
||||||
|
|
||||||
|
msg_hdr_len = 5+id_len+9
|
||||||
|
|
||||||
|
result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer,
|
||||||
|
self.header_len + 4)
|
||||||
|
|
||||||
|
logger.debug(f'ID: {result[0]} B: {result[1]}')
|
||||||
|
logger.debug(f'time: {result[2]:08x}')
|
||||||
|
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
|
||||||
|
# "%Y-%m-%d %H:%M:%S")}')
|
||||||
|
return msg_hdr_len
|
||||||
|
|
||||||
|
def msg_collector_data(self):
|
||||||
|
if self.ctrl.is_ind():
|
||||||
|
self.__build_header(0x99)
|
||||||
|
self._send_buffer += b'\x01'
|
||||||
|
self.__finish_send_msg()
|
||||||
|
self.__process_data()
|
||||||
|
|
||||||
|
elif self.ctrl.is_resp():
|
||||||
|
return # ignore received response
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def msg_inverter_data(self):
|
||||||
|
if self.ctrl.is_ind():
|
||||||
|
self.__build_header(0x99)
|
||||||
|
self._send_buffer += b'\x01'
|
||||||
|
self.__finish_send_msg()
|
||||||
|
self.__process_data()
|
||||||
|
|
||||||
|
elif self.ctrl.is_resp():
|
||||||
|
return # ignore received response
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def __process_data(self):
|
||||||
|
msg_hdr_len = self.parse_msg_header()
|
||||||
|
|
||||||
|
for key, update in self.db.parse(self._recv_buffer, self.header_len
|
||||||
|
+ msg_hdr_len):
|
||||||
|
if update:
|
||||||
|
self.new_data[key] = True
|
||||||
|
|
||||||
|
def msg_ota_update(self):
|
||||||
|
if self.ctrl.is_req():
|
||||||
|
self.inc_counter('OTA_Start_Msg')
|
||||||
|
elif self.ctrl.is_ind():
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown Ctrl')
|
||||||
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
|
|
||||||
|
def msg_unknown(self):
|
||||||
|
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
|
||||||
|
self.inc_counter('Unknown_Msg')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||||
36
app/src/gen3plus/connection_g3p.py
Normal file
36
app/src/gen3plus/connection_g3p.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
# import gc
|
||||||
|
from async_stream import AsyncStream
|
||||||
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
|
|
||||||
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionG3P(AsyncStream, SolarmanV5):
|
||||||
|
|
||||||
|
def __init__(self, reader, writer, addr, remote_stream,
|
||||||
|
server_side: bool) -> None:
|
||||||
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
|
SolarmanV5.__init__(self, server_side)
|
||||||
|
|
||||||
|
self.remoteStream = remote_stream
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our puplic methods
|
||||||
|
'''
|
||||||
|
def close(self):
|
||||||
|
AsyncStream.close(self)
|
||||||
|
SolarmanV5.close(self)
|
||||||
|
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
|
async def async_create_remote(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def async_publ_mqtt(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our private methods
|
||||||
|
'''
|
||||||
|
def __del__(self):
|
||||||
|
super().__del__()
|
||||||
121
app/src/gen3plus/infos_g3p.py
Normal file
121
app/src/gen3plus/infos_g3p.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
if __name__ == "app.src.gen3plus.infos_g3p":
|
||||||
|
from app.src.infos import Infos, Register
|
||||||
|
else: # pragma: no cover
|
||||||
|
from infos import Infos, Register
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterMap:
|
||||||
|
# make the class read/only by using __slots__
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
map = {
|
||||||
|
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
|
||||||
|
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '!B', 'ratio': 60}, # noqa: E501
|
||||||
|
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '!B', 'ratio': 1}, # noqa: E501
|
||||||
|
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '!B', 'ratio': 1}, # noqa: E501
|
||||||
|
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '!B', 'ratio': 1}, # noqa: E501
|
||||||
|
0x4102001e: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||||
|
0x4102004c: {'reg': Register.IP_ADRESS, 'fmt': '!16s'}, # noqa: E501
|
||||||
|
0x41020064: {'reg': Register.VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||||
|
|
||||||
|
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
|
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # 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-32)/1.8'}, # noqa: E501
|
||||||
|
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
|
||||||
|
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
|
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100e2: {'reg': Register.PV1_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100e4: {'reg': Register.PV1_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100e6: {'reg': Register.PV2_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100e8: {'reg': Register.PV2_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100ea: {'reg': Register.PV2_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100ec: {'reg': Register.PV3_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100ee: {'reg': Register.PV3_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100f0: {'reg': Register.PV3_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100f2: {'reg': Register.PV4_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100f4: {'reg': Register.PV4_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100f6: {'reg': Register.PV4_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
|
0x420100f8: {'reg': Register.DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100fa: {'reg': Register.TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x420100fe: {'reg': Register.PV1_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010100: {'reg': Register.PV1_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010104: {'reg': Register.PV2_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010106: {'reg': Register.PV2_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x4201010a: {'reg': Register.PV3_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
|
||||||
|
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
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
|
||||||
|
0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InfosG3P(Infos):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.set_db_def_value(Register.MANUFACTURER, 'TSUN')
|
||||||
|
self.set_db_def_value(Register.EQUIPMENT_MODEL, 'TSOL-MSxx00')
|
||||||
|
self.set_db_def_value(Register.CHIP_TYPE, 'IGEN TECH')
|
||||||
|
|
||||||
|
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
|
||||||
|
sug_area: str = '') \
|
||||||
|
-> Generator[tuple[dict, str], None, None]:
|
||||||
|
'''Generator function yields a json register struct for home-assistant
|
||||||
|
auto configuration and a unique entity string
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
prfx:str ==> MQTT prefix for the home assistant 'stat_t string
|
||||||
|
snr:str ==> serial number of the inverter, used to build unique
|
||||||
|
entity strings
|
||||||
|
sug_area:str ==> suggested area string from the config file'''
|
||||||
|
# iterate over RegisterMap.map and get the register values
|
||||||
|
for row in RegisterMap.map.values():
|
||||||
|
info_id = row['reg']
|
||||||
|
res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
|
||||||
|
if res:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
def parse(self, buf, msg_type: int, rcv_ftype: int) \
|
||||||
|
-> Generator[tuple[str, bool], None, None]:
|
||||||
|
'''parse a data sequence received from the inverter and
|
||||||
|
stores the values in Infos.db
|
||||||
|
|
||||||
|
buf: buffer of the sequence to parse'''
|
||||||
|
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 isinstance(row, dict):
|
||||||
|
info_id = row['reg']
|
||||||
|
fmt = row['fmt']
|
||||||
|
res = struct.unpack_from(fmt, buf, addr)
|
||||||
|
result = res[0]
|
||||||
|
if isinstance(result, (bytearray, bytes)):
|
||||||
|
result = result.decode('utf-8')
|
||||||
|
if 'eval' in row:
|
||||||
|
result = eval(row['eval'])
|
||||||
|
if 'ratio' in row:
|
||||||
|
result = round(result * row['ratio'], 2)
|
||||||
|
|
||||||
|
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||||
|
|
||||||
|
if keys:
|
||||||
|
name, update = self.update_db(keys, must_incr, result)
|
||||||
|
yield keys[0], update
|
||||||
|
else:
|
||||||
|
name = str(f'info-id.0x{addr:x}')
|
||||||
|
update = False
|
||||||
|
|
||||||
|
self.tracer.log(level, f'{name} : {result}{unit}'
|
||||||
|
f' update: {update}')
|
||||||
126
app/src/gen3plus/inverter_g3p.py
Normal file
126
app/src/gen3plus/inverter_g3p.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import json
|
||||||
|
from config import Config
|
||||||
|
from inverter import Inverter
|
||||||
|
from gen3plus.connection_g3p import ConnectionG3P
|
||||||
|
from aiomqtt import MqttCodeError
|
||||||
|
from infos import Infos
|
||||||
|
|
||||||
|
# import gc
|
||||||
|
|
||||||
|
# logger = logging.getLogger('conn')
|
||||||
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
|
class InverterG3P(Inverter, ConnectionG3P):
|
||||||
|
'''class Inverter is a derivation of an Async_Stream
|
||||||
|
|
||||||
|
The class has some class method for managing common resources like a
|
||||||
|
connection to the MQTT broker or proxy error counter which are common
|
||||||
|
for all inverter connection
|
||||||
|
|
||||||
|
Instances of the class are connections to an inverter and can have an
|
||||||
|
optional link to an remote connection to the TSUN cloud. A remote
|
||||||
|
connection dies with the inverter connection.
|
||||||
|
|
||||||
|
class methods:
|
||||||
|
class_init(): initialize the common resources of the proxy (MQTT
|
||||||
|
broker, Proxy DB, etc). Must be called before the
|
||||||
|
first inverter instance can be created
|
||||||
|
class_close(): release the common resources of the proxy. Should not
|
||||||
|
be called before any instances of the class are
|
||||||
|
destroyed
|
||||||
|
|
||||||
|
methods:
|
||||||
|
server_loop(addr): Async loop method for receiving messages from the
|
||||||
|
inverter (server-side)
|
||||||
|
client_loop(addr): Async loop method for receiving messages from the
|
||||||
|
TSUN cloud (client-side)
|
||||||
|
async_create_remote(): Establish a client connection to the TSUN cloud
|
||||||
|
async_publ_mqtt(): Publish data to MQTT broker
|
||||||
|
close(): Release method which must be called before a instance can be
|
||||||
|
destroyed
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, reader, writer, addr):
|
||||||
|
super().__init__(reader, writer, addr, None, True)
|
||||||
|
self.__ha_restarts = -1
|
||||||
|
|
||||||
|
async def async_create_remote(self) -> None:
|
||||||
|
'''Establish a client connection to the TSUN cloud'''
|
||||||
|
tsun = Config.get('solarman')
|
||||||
|
host = tsun['host']
|
||||||
|
port = tsun['port']
|
||||||
|
addr = (host, port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f'Connected to {addr}')
|
||||||
|
connect = asyncio.open_connection(host, port)
|
||||||
|
reader, writer = await connect
|
||||||
|
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
||||||
|
False)
|
||||||
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
|
except (ConnectionRefusedError, TimeoutError) as error:
|
||||||
|
logging.info(f'{error}')
|
||||||
|
except Exception:
|
||||||
|
self.inc_counter('SW_Exception')
|
||||||
|
logging.error(
|
||||||
|
f"Inverter: Exception for {addr}:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
async def async_publ_mqtt(self) -> None:
|
||||||
|
'''publish data to MQTT broker'''
|
||||||
|
# check if new inverter or collector infos are available or when the
|
||||||
|
# home assistant has changed the status back to online
|
||||||
|
try:
|
||||||
|
if (('inverter' in self.new_data and self.new_data['inverter'])
|
||||||
|
or ('collector' in self.new_data and
|
||||||
|
self.new_data['collector'])
|
||||||
|
or self.mqtt.ha_restarts != self.__ha_restarts):
|
||||||
|
await self._register_proxy_stat_home_assistant()
|
||||||
|
await self.__register_home_assistant()
|
||||||
|
self.__ha_restarts = self.mqtt.ha_restarts
|
||||||
|
|
||||||
|
for key in self.new_data:
|
||||||
|
await self.__async_publ_mqtt_packet(key)
|
||||||
|
for key in Infos.new_stat_data:
|
||||||
|
await self._async_publ_mqtt_proxy_stat(key)
|
||||||
|
|
||||||
|
except MqttCodeError as error:
|
||||||
|
logging.error(f'Mqtt except: {error}')
|
||||||
|
except Exception:
|
||||||
|
self.inc_counter('SW_Exception')
|
||||||
|
logging.error(
|
||||||
|
f"Inverter: Exception:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
async def __async_publ_mqtt_packet(self, key):
|
||||||
|
db = self.db.db
|
||||||
|
if key in db and self.new_data[key]:
|
||||||
|
data_json = json.dumps(db[key])
|
||||||
|
node_id = self.node_id
|
||||||
|
logger_mqtt.debug(f'{key}: {data_json}')
|
||||||
|
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
|
self.new_data[key] = False
|
||||||
|
|
||||||
|
async def __register_home_assistant(self) -> None:
|
||||||
|
'''register all our topics at home assistant'''
|
||||||
|
for data_json, component, node_id, id in self.db.ha_confs(
|
||||||
|
self.entity_prfx, self.node_id, self.unique_id,
|
||||||
|
self.sug_area):
|
||||||
|
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
||||||
|
f" node_id:'{node_id}' {data_json}")
|
||||||
|
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
||||||
|
f"/{node_id}{id}/config", data_json)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
logging.debug(f'InverterG3P.close() l{self.l_addr} | r{self.r_addr}')
|
||||||
|
super().close() # call close handler in the parent class
|
||||||
|
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
logging.debug("InverterG3P.__del__")
|
||||||
|
super().__del__()
|
||||||
365
app/src/gen3plus/solarman_v5.py
Normal file
365
app/src/gen3plus/solarman_v5.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import struct
|
||||||
|
# import json
|
||||||
|
import logging
|
||||||
|
# import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||||
|
from app.src.messages import hex_dump_memory, Message
|
||||||
|
from app.src.config import Config
|
||||||
|
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||||
|
from app.src.infos import Register
|
||||||
|
else: # pragma: no cover
|
||||||
|
from messages import hex_dump_memory, Message
|
||||||
|
from config import Config
|
||||||
|
from gen3plus.infos_g3p import InfosG3P
|
||||||
|
from infos import Register
|
||||||
|
# import traceback
|
||||||
|
|
||||||
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
|
|
||||||
|
class SolarmanV5(Message):
|
||||||
|
|
||||||
|
def __init__(self, server_side: bool):
|
||||||
|
super().__init__(server_side)
|
||||||
|
|
||||||
|
self.header_len = 11 # overwrite construcor in class Message
|
||||||
|
self.control = 0
|
||||||
|
self.serial = 0
|
||||||
|
self.snr = 0
|
||||||
|
self.db = InfosG3P()
|
||||||
|
self.switch = {
|
||||||
|
|
||||||
|
0x4210: self.msg_data_ind, # real time data
|
||||||
|
0x1210: self.msg_data_rsp, # at least every 5 minutes
|
||||||
|
|
||||||
|
0x4710: self.msg_hbeat_ind, # heatbeat
|
||||||
|
0x1710: self.msg_hbeat_rsp, # every 2 minutes
|
||||||
|
|
||||||
|
# every 3 hours comes a sync seuqence:
|
||||||
|
# 00:00:00 0x4110 device data ftype: 0x02
|
||||||
|
# 00:00:02 0x4210 real time data ftype: 0x01
|
||||||
|
# 00:00:03 0x4210 real time data ftype: 0x81
|
||||||
|
# 00:00:05 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 0c # noqa: E501
|
||||||
|
# 00:00:06 0x4310 wifi data ftype: 0x81 sub-id 0x0018: 1c # noqa: E501
|
||||||
|
# 00:00:07 0x4310 wifi data ftype: 0x01 sub-id 0x0018: 0c # noqa: E501
|
||||||
|
# 00:00:08 0x4810 options? ftype: 0x01
|
||||||
|
|
||||||
|
0x4110: self.msg_dev_ind, # device data, sync start
|
||||||
|
0x1110: self.msg_dev_rsp, # every 3 hours
|
||||||
|
|
||||||
|
0x4310: self.msg_forward, # regulary after 3-6 hours
|
||||||
|
0x1310: self.msg_forward,
|
||||||
|
0x4810: self.msg_forward, # sync end
|
||||||
|
0x1810: self.msg_forward,
|
||||||
|
|
||||||
|
#
|
||||||
|
# AT cmd
|
||||||
|
0x4510: self.at_command_ind, # from server
|
||||||
|
0x1510: self.msg_forward, # from inverter
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
Our puplic methods
|
||||||
|
'''
|
||||||
|
def close(self) -> None:
|
||||||
|
logging.debug('Solarman.close()')
|
||||||
|
# we have refernces 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()
|
||||||
|
|
||||||
|
def set_serial_no(self, snr: int):
|
||||||
|
serial_no = str(snr)
|
||||||
|
if self.unique_id == serial_no:
|
||||||
|
logger.debug(f'SerialNo: {serial_no}')
|
||||||
|
else:
|
||||||
|
found = False
|
||||||
|
inverters = Config.get('inverters')
|
||||||
|
# logger.debug(f'Inverters: {inverters}')
|
||||||
|
|
||||||
|
for inv in inverters.values():
|
||||||
|
# logger.debug(f'key: {key} -> {inv}')
|
||||||
|
if (type(inv) is dict and 'monitor_sn' in inv
|
||||||
|
and inv['monitor_sn'] == snr):
|
||||||
|
found = True
|
||||||
|
self.node_id = inv['node_id']
|
||||||
|
self.sug_area = inv['suggested_area']
|
||||||
|
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
self.node_id = ''
|
||||||
|
self.sug_area = ''
|
||||||
|
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||||
|
self.inc_counter('Unknown_SNR')
|
||||||
|
self.unique_id = None
|
||||||
|
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
||||||
|
return
|
||||||
|
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||||
|
|
||||||
|
self.unique_id = serial_no
|
||||||
|
|
||||||
|
def read(self) -> None:
|
||||||
|
self._read()
|
||||||
|
|
||||||
|
if not self.header_valid:
|
||||||
|
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
||||||
|
|
||||||
|
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
||||||
|
self.data_len+2):
|
||||||
|
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
||||||
|
self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||||
|
+ self.data_len + 2):
|
||||||
|
self.set_serial_no(self.snr)
|
||||||
|
self.__dispatch_msg()
|
||||||
|
self.__flush_recv_msg()
|
||||||
|
return
|
||||||
|
|
||||||
|
def forward(self, buffer, buflen) -> None:
|
||||||
|
tsun = Config.get('solarman')
|
||||||
|
if tsun['enabled']:
|
||||||
|
self._forward_buffer = buffer[:buflen]
|
||||||
|
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
|
||||||
|
buffer, buflen)
|
||||||
|
|
||||||
|
self.__parse_header(self._forward_buffer,
|
||||||
|
len(self._forward_buffer))
|
||||||
|
fnc = self.switch.get(self.control, self.msg_unknown)
|
||||||
|
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||||
|
f' Ctl: {int(self.control):#04x}'
|
||||||
|
f' Msg: {fnc.__name__!r}')
|
||||||
|
return
|
||||||
|
|
||||||
|
def _init_new_client_conn(self) -> bool:
|
||||||
|
# self.__build_header(0x91)
|
||||||
|
# self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
|
||||||
|
# f'{len(contact_mail)+1}p',
|
||||||
|
# contact_name, contact_mail)
|
||||||
|
|
||||||
|
# self.__finish_send_msg()
|
||||||
|
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 __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] # len of complete message
|
||||||
|
self.data_len = result[1] # len of variable id string
|
||||||
|
self.control = result[2]
|
||||||
|
self.serial = result[3]
|
||||||
|
self.snr = result[4]
|
||||||
|
|
||||||
|
if start != 0xA5:
|
||||||
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
|
# erase broken recv buffer
|
||||||
|
self._recv_buffer = bytearray()
|
||||||
|
return
|
||||||
|
self.header_valid = True
|
||||||
|
return
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
|
if len(self._recv_buffer) > (self.data_len+13):
|
||||||
|
next_start = buf[self.data_len+13]
|
||||||
|
if next_start != 0xa5:
|
||||||
|
# erase broken recv buffer
|
||||||
|
self._recv_buffer = bytearray()
|
||||||
|
|
||||||
|
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 __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._recv_buffer = self._recv_buffer[(self.header_len +
|
||||||
|
self.data_len+2):]
|
||||||
|
self.header_valid = False
|
||||||
|
'''
|
||||||
|
def modbus(self, data):
|
||||||
|
POLY = 0xA001
|
||||||
|
|
||||||
|
crc = 0xFFFF
|
||||||
|
for byte in data:
|
||||||
|
crc ^= byte
|
||||||
|
for _ in range(8):
|
||||||
|
crc = ((crc >> 1) ^ POLY
|
||||||
|
if (crc & 0x0001)
|
||||||
|
else crc >> 1)
|
||||||
|
return crc
|
||||||
|
|
||||||
|
def validate_modbus_crc(self, frame):
|
||||||
|
# Calculate crc with all but the last 2 bytes of
|
||||||
|
# the frame (they contain the crc)
|
||||||
|
calc_crc = 0xFFFF
|
||||||
|
for pos in frame[:-2]:
|
||||||
|
calc_crc ^= pos
|
||||||
|
for i in range(8):
|
||||||
|
if (calc_crc & 1) != 0:
|
||||||
|
calc_crc >>= 1
|
||||||
|
calc_crc ^= 0xA001 # bitwise 'or' with modbus magic
|
||||||
|
# number (0xa001 == bitwise
|
||||||
|
# reverse of 0x8005)
|
||||||
|
else:
|
||||||
|
calc_crc >>= 1
|
||||||
|
|
||||||
|
# Compare calculated crc with the one supplied in the frame....
|
||||||
|
frame_crc, = struct.unpack('<H', frame[-2:])
|
||||||
|
if calc_crc == frame_crc:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
Message handler methods
|
||||||
|
'''
|
||||||
|
def msg_unknown(self):
|
||||||
|
logger.warning(f"Unknow Msg: ID:{int(self.control):#04x}")
|
||||||
|
self.inc_counter('Unknown_Msg')
|
||||||
|
self.msg_forward()
|
||||||
|
|
||||||
|
def msg_forward(self):
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def msg_dev_ind(self):
|
||||||
|
data = self._recv_buffer[self.header_len:]
|
||||||
|
result = struct.unpack_from('<BLLL', data, 0)
|
||||||
|
ftype = result[0] # always 2
|
||||||
|
total = result[1]
|
||||||
|
tim = result[2]
|
||||||
|
res = result[3] # always zero
|
||||||
|
logger.info(f'frame type:{ftype:02x} total:{total}s'
|
||||||
|
f' timer:{tim:08x}s null:{res}')
|
||||||
|
dt = datetime.fromtimestamp(total)
|
||||||
|
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
|
||||||
|
self.__process_data(ftype)
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def msg_dev_rsp(self):
|
||||||
|
self.msg_response()
|
||||||
|
|
||||||
|
def msg_data_ind(self):
|
||||||
|
data = self._recv_buffer
|
||||||
|
result = struct.unpack_from('<BLLLLL', data, self.header_len)
|
||||||
|
ftype = result[0] # 1 or 0x81
|
||||||
|
total = result[1]
|
||||||
|
tim = result[2]
|
||||||
|
offset = result[3]
|
||||||
|
unkn = result[4]
|
||||||
|
cnt = result[5]
|
||||||
|
logger.info(f'ftype:{ftype:02x} total:{total}s'
|
||||||
|
f' timer:{tim:08x}s ofs:{offset}'
|
||||||
|
f' ??: {unkn:08x} cnt:{cnt}')
|
||||||
|
dt = datetime.fromtimestamp(total)
|
||||||
|
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
|
||||||
|
ftype &= 0x7f # mask bit 7 (0x80)
|
||||||
|
self.__process_data(ftype)
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def __process_data(self, ftype):
|
||||||
|
inv_update = False
|
||||||
|
ctrl_update = False
|
||||||
|
msg_type = self.control >> 8
|
||||||
|
for key, update in self.db.parse(self._recv_buffer, msg_type, ftype):
|
||||||
|
if update:
|
||||||
|
if key == 'inverter':
|
||||||
|
inv_update = True
|
||||||
|
if key == 'controller':
|
||||||
|
ctrl_update = True
|
||||||
|
self.new_data[key] = True
|
||||||
|
|
||||||
|
if inv_update:
|
||||||
|
db = self.db
|
||||||
|
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
Rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
Model = None
|
||||||
|
if MaxPow == 2000:
|
||||||
|
if Rated == 800 or Rated == 600:
|
||||||
|
Model = f'TSOL-MS{MaxPow}({Rated})'
|
||||||
|
else:
|
||||||
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
|
elif MaxPow == 1800 or MaxPow == 1600:
|
||||||
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
|
if Model:
|
||||||
|
logger.info(f'Model: {Model}')
|
||||||
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
|
||||||
|
|
||||||
|
if ctrl_update:
|
||||||
|
db = self.db
|
||||||
|
Version = db.get_db_value(Register.COLLECTOR_FW_VERSION, 0)
|
||||||
|
if isinstance(Version, str):
|
||||||
|
Model = Version.split('_')[0]
|
||||||
|
self.db.set_db_def_value(Register.CHIP_MODEL, Model)
|
||||||
|
|
||||||
|
def msg_data_rsp(self):
|
||||||
|
self.msg_response()
|
||||||
|
|
||||||
|
def msg_hbeat_ind(self):
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def msg_hbeat_rsp(self):
|
||||||
|
self.msg_response()
|
||||||
|
|
||||||
|
def msg_response(self):
|
||||||
|
data = self._recv_buffer[self.header_len:]
|
||||||
|
result = struct.unpack_from('<BBLL', data, 0)
|
||||||
|
ftype = result[0] # always 2
|
||||||
|
valid = result[1] == 1 # status
|
||||||
|
ts = result[2]
|
||||||
|
repeat = result[3] # always 60
|
||||||
|
logger.info(f'ftype:{ftype} accepted:{valid}'
|
||||||
|
f' ts:{ts:08x} repeat:{repeat}s')
|
||||||
|
|
||||||
|
dt = datetime.fromtimestamp(ts)
|
||||||
|
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
|
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
|
||||||
|
|
||||||
|
def at_command_ind(self):
|
||||||
|
self.inc_counter('AT_Command')
|
||||||
|
self.msg_forward()
|
||||||
692
app/src/infos.py
692
app/src/infos.py
@@ -1,7 +1,98 @@
|
|||||||
import struct
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
|
||||||
|
class Register(Enum):
|
||||||
|
COLLECTOR_FW_VERSION = 1
|
||||||
|
CHIP_TYPE = 2
|
||||||
|
CHIP_MODEL = 3
|
||||||
|
TRACE_URL = 4
|
||||||
|
LOGGER_URL = 5
|
||||||
|
PRODUCT_NAME = 20
|
||||||
|
MANUFACTURER = 21
|
||||||
|
VERSION = 22
|
||||||
|
SERIAL_NUMBER = 23
|
||||||
|
EQUIPMENT_MODEL = 24
|
||||||
|
NO_INPUTS = 25
|
||||||
|
MAX_DESIGNED_POWER = 26
|
||||||
|
INVERTER_CNT = 50
|
||||||
|
UNKNOWN_SNR = 51
|
||||||
|
UNKNOWN_MSG = 52
|
||||||
|
INVALID_DATA_TYPE = 53
|
||||||
|
INTERNAL_ERROR = 54
|
||||||
|
UNKNOWN_CTRL = 55
|
||||||
|
OTA_START_MSG = 56
|
||||||
|
SW_EXCEPTION = 57
|
||||||
|
INVALID_MSG_FMT = 58
|
||||||
|
AT_COMMAND = 59
|
||||||
|
OUTPUT_POWER = 83
|
||||||
|
RATED_POWER = 84
|
||||||
|
INVERTER_TEMP = 85
|
||||||
|
PV1_VOLTAGE = 100
|
||||||
|
PV1_CURRENT = 101
|
||||||
|
PV1_POWER = 102
|
||||||
|
PV2_VOLTAGE = 110
|
||||||
|
PV2_CURRENT = 111
|
||||||
|
PV2_POWER = 112
|
||||||
|
PV3_VOLTAGE = 120
|
||||||
|
PV3_CURRENT = 121
|
||||||
|
PV3_POWER = 122
|
||||||
|
PV4_VOLTAGE = 130
|
||||||
|
PV4_CURRENT = 131
|
||||||
|
PV4_POWER = 132
|
||||||
|
PV5_VOLTAGE = 140
|
||||||
|
PV5_CURRENT = 141
|
||||||
|
PV5_POWER = 142
|
||||||
|
PV6_VOLTAGE = 150
|
||||||
|
PV6_CURRENT = 151
|
||||||
|
PV6_POWER = 152
|
||||||
|
PV1_DAILY_GENERATION = 200
|
||||||
|
PV1_TOTAL_GENERATION = 201
|
||||||
|
PV2_DAILY_GENERATION = 210
|
||||||
|
PV2_TOTAL_GENERATION = 211
|
||||||
|
PV3_DAILY_GENERATION = 220
|
||||||
|
PV3_TOTAL_GENERATION = 221
|
||||||
|
PV4_DAILY_GENERATION = 230
|
||||||
|
PV4_TOTAL_GENERATION = 231
|
||||||
|
PV5_DAILY_GENERATION = 240
|
||||||
|
PV5_TOTAL_GENERATION = 241
|
||||||
|
PV6_DAILY_GENERATION = 250
|
||||||
|
PV6_TOTAL_GENERATION = 251
|
||||||
|
GRID_VOLTAGE = 300
|
||||||
|
GRID_CURRENT = 301
|
||||||
|
GRID_FREQUENCY = 302
|
||||||
|
DAILY_GENERATION = 303
|
||||||
|
TOTAL_GENERATION = 304
|
||||||
|
COMMUNICATION_TYPE = 400
|
||||||
|
SIGNAL_STRENGTH = 401
|
||||||
|
POWER_ON_TIME = 402
|
||||||
|
COLLECT_INTERVAL = 403
|
||||||
|
DATA_UP_INTERVAL = 404
|
||||||
|
CONNECT_COUNT = 405
|
||||||
|
HEARTBEAT_INTERVAL = 406
|
||||||
|
IP_ADRESS = 407
|
||||||
|
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
|
||||||
|
VALUE_1 = 9000
|
||||||
|
TEST_REG1 = 10000
|
||||||
|
TEST_REG2 = 10001
|
||||||
|
|
||||||
|
|
||||||
class Infos:
|
class Infos:
|
||||||
@@ -9,6 +100,8 @@ class Infos:
|
|||||||
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||||
version = os.getenv('VERSION', 'unknown')
|
version = os.getenv('VERSION', 'unknown')
|
||||||
|
|
||||||
|
new_stat_data = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def static_init(cls):
|
def static_init(cls):
|
||||||
logging.info('Initialize proxy statistics')
|
logging.info('Initialize proxy statistics')
|
||||||
@@ -30,105 +123,127 @@ class Infos:
|
|||||||
|
|
||||||
__info_devs = {
|
__info_devs = {
|
||||||
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
||||||
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': 0x00092f90, 'mf': 0x000927c0, 'sw': 0x00092ba8}, # noqa: E501
|
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION}, # noqa: E501
|
||||||
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': 0x00000032, 'mf': 0x00000014, 'sw': 0x0000001e}, # noqa: E501
|
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION}, # noqa: E501
|
||||||
'input_pv1': {'via': 'inverter', 'name': 'Module PV1'},
|
'input_pv1': {'via': 'inverter', 'name': 'Module PV1'},
|
||||||
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': 0x00013880, 'gte': 2}}, # noqa: E501
|
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
||||||
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': 0x00013880, 'gte': 3}}, # noqa: E501
|
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501
|
||||||
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': 0x00013880, 'gte': 4}}, # noqa: E501
|
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
||||||
|
|
||||||
__info_defs = {
|
__info_defs = {
|
||||||
# collector values used for device registration:
|
# collector values used for device registration:
|
||||||
0x00092ba8: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.COLLECTOR_FW_VERSION: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
0x000927c0: {'name': ['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00092f90: {'name': ['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.CHIP_MODEL: {'name': ['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00095a88: {'name': ['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.TRACE_URL: {'name': ['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00095aec: {'name': ['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
|
||||||
# inverter values used for device registration:
|
# inverter values used for device registration:
|
||||||
0x0000000a: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PRODUCT_NAME: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000014: {'name': ['inverter', 'Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.MANUFACTURER: {'name': ['inverter', 'Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000001e: {'name': ['inverter', 'Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.VERSION: {'name': ['inverter', 'Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
0x00000028: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000032: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00013880: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
|
||||||
# proxy:
|
# proxy:
|
||||||
0xffffff00: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
||||||
0xffffff01: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0xffffff02: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0xffffff03: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0xffffff04: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
||||||
0xffffff05: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0xffffff06: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0xffffff07: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
||||||
|
|
||||||
# events
|
# events
|
||||||
0x00000191: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000192: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000193: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000194: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000195: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000196: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000197: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000198: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x00000199: {'name': ['events', '409_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_409: {'name': ['events', '409_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019a: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019b: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_411: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019c: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019d: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_413: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019e: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_414: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x0000019f: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
0x000001a0: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
|
||||||
# grid measures:
|
# grid measures:
|
||||||
0x000003e8: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x0000044c: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000004b0: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000640: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
||||||
0x000005dc: {'name': ['env', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
||||||
0x00000514: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
Register.VALUE_1: {'name': ['env', 'Value_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'value_1_', 'fmt': '| int', 'name': 'Value 1', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
|
||||||
# input measures:
|
# input measures:
|
||||||
0x000006a4: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000708: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x0000076c: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
||||||
0x000007d0: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000834: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000898: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
||||||
0x000008fc: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000960: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000009c4: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
||||||
0x00000a28: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000a8c: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x00000af0: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
||||||
0x00000c1c: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000c80: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000ce4: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000d48: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000dac: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000e10: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000e74: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000ed8: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
# total:
|
# total:
|
||||||
0x00000b54: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
0x00000bb8: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
|
|
||||||
# controller:
|
# controller:
|
||||||
0x000c3500: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
0x000c96a8: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| float', 'name': 'Power on Time', 'nat_prc': '3', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| float', 'name': 'Power on Time', 'nat_prc': '3', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000d0020: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " s"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " s"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000cfc38: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000c7f38: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
# 0x000c7f38: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': 's', 'new_value': 5}, # noqa: E501
|
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
0x000cf850: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
|
Register.IP_ADRESS: {'name': ['controller', 'IP_Adress'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_adress_', 'fmt': '| string', 'name': 'IP Adress', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_devs(self) -> dict:
|
||||||
|
return self.__info_devs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_defs(self) -> dict:
|
||||||
|
return self.__info_defs
|
||||||
|
'''
|
||||||
|
if __name__ == "app.src.messages":
|
||||||
|
@info_defs.setter
|
||||||
|
def info_defs(self, value: dict) -> None:
|
||||||
|
self.__info_defs = value
|
||||||
|
|
||||||
|
@info_devs.setter
|
||||||
|
def info_devs(self, value: dict) -> None:
|
||||||
|
self.__info_devs = value
|
||||||
|
'''
|
||||||
|
|
||||||
def dev_value(self, idx: str | int) -> str | int | float | None:
|
def dev_value(self, idx: str | int) -> str | int | float | None:
|
||||||
'''returns the stored device value from our database
|
'''returns the stored device value from our database
|
||||||
|
|
||||||
@@ -139,8 +254,8 @@ class Infos:
|
|||||||
'''
|
'''
|
||||||
if type(idx) is str:
|
if type(idx) is str:
|
||||||
return idx # return idx as a fixed value
|
return idx # return idx as a fixed value
|
||||||
elif idx in self.__info_defs:
|
elif idx in self.info_defs:
|
||||||
row = self.__info_defs[idx]
|
row = self.info_defs[idx]
|
||||||
if 'singleton' in row and row['singleton']:
|
if 'singleton' in row and row['singleton']:
|
||||||
dict = self.stat
|
dict = self.stat
|
||||||
else:
|
else:
|
||||||
@@ -154,7 +269,192 @@ class Infos:
|
|||||||
dict = dict[key]
|
dict = dict[key]
|
||||||
return dict # value of the reqeusted entry
|
return dict # value of the reqeusted entry
|
||||||
|
|
||||||
return None # unknwon idx, not in __info_defs
|
return None # unknwon idx, not in info_defs
|
||||||
|
|
||||||
|
def inc_counter(self, counter: str) -> None:
|
||||||
|
'''inc proxy statistic counter'''
|
||||||
|
dict = self.stat['proxy']
|
||||||
|
dict[counter] += 1
|
||||||
|
|
||||||
|
def dec_counter(self, counter: str) -> None:
|
||||||
|
'''dec proxy statistic counter'''
|
||||||
|
dict = self.stat['proxy']
|
||||||
|
dict[counter] -= 1
|
||||||
|
|
||||||
|
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
||||||
|
-> Generator[tuple[dict, str], None, None]:
|
||||||
|
'''Generator function yields json register struct for home-assistant
|
||||||
|
auto configuration and the unique entity string, for all proxy
|
||||||
|
registers
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
ha_prfx:str ==> MQTT prefix for the home assistant 'stat_t string
|
||||||
|
node_id:str ==> node id of the inverter, used to build unique entity
|
||||||
|
snr:str ==> serial number of the inverter, used to build unique
|
||||||
|
entity strings
|
||||||
|
'''
|
||||||
|
# iterate over RegisterMap.map and get the register values for entries
|
||||||
|
# with Singleton=True, which means that this is a proxy register
|
||||||
|
for reg in self.info_defs.keys():
|
||||||
|
res = self.ha_conf(reg, ha_prfx, node_id, snr, True) # noqa: E501
|
||||||
|
if res:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
def ha_conf(self, key, ha_prfx, node_id, snr, singleton: bool, sug_area: str = '') -> tuple[str, str, str, str]: # noqa: E501
|
||||||
|
if key not in self.info_defs:
|
||||||
|
return None
|
||||||
|
row = self.info_defs[key]
|
||||||
|
|
||||||
|
if 'singleton' in row:
|
||||||
|
if singleton != row['singleton']:
|
||||||
|
return None
|
||||||
|
elif singleton:
|
||||||
|
return None
|
||||||
|
prfx = ha_prfx + node_id
|
||||||
|
|
||||||
|
# check if we have details for home assistant
|
||||||
|
if 'ha' in row:
|
||||||
|
ha = row['ha']
|
||||||
|
if 'comp' in ha:
|
||||||
|
component = ha['comp']
|
||||||
|
else:
|
||||||
|
component = 'sensor'
|
||||||
|
attr = {}
|
||||||
|
if 'name' in ha:
|
||||||
|
attr['name'] = ha['name']
|
||||||
|
else:
|
||||||
|
attr['name'] = row['name'][-1]
|
||||||
|
attr['stat_t'] = prfx + row['name'][0]
|
||||||
|
attr['dev_cla'] = ha['dev_cla']
|
||||||
|
attr['stat_cla'] = ha['stat_cla']
|
||||||
|
attr['uniq_id'] = ha['id']+snr
|
||||||
|
if 'val_tpl' in ha:
|
||||||
|
attr['val_tpl'] = ha['val_tpl']
|
||||||
|
elif 'fmt' in ha:
|
||||||
|
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
||||||
|
else:
|
||||||
|
self.inc_counter('Internal_Error')
|
||||||
|
logging.error(f"Infos.info_defs: the row for {key} do"
|
||||||
|
" not have a 'val_tpl' nor a 'fmt' value")
|
||||||
|
# add unit_of_meas only, if status_class isn't none. If
|
||||||
|
# status_cla is None we want a number format and not line
|
||||||
|
# graph in home assistant. A unit will change the number
|
||||||
|
# format to a line graph
|
||||||
|
if 'unit' in row and attr['stat_cla'] is not None:
|
||||||
|
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
||||||
|
if 'icon' in ha:
|
||||||
|
attr['ic'] = ha['icon'] # icon for the entity
|
||||||
|
if 'nat_prc' in ha:
|
||||||
|
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
||||||
|
if 'ent_cat' in ha:
|
||||||
|
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||||
|
# enabled_by_default is deactivated, since it avoid the via
|
||||||
|
# setup of the devices. It seems, that there is a bug in home
|
||||||
|
# assistant. tested with 'Home Assistant 2023.10.4'
|
||||||
|
# if 'en' in ha: # enabled_by_default
|
||||||
|
# attr['en'] = ha['en']
|
||||||
|
if 'dev' in ha:
|
||||||
|
device = self.info_devs[ha['dev']]
|
||||||
|
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
||||||
|
return None
|
||||||
|
dev = {}
|
||||||
|
# the same name for 'name' and 'suggested area', so we get
|
||||||
|
# dedicated devices in home assistant with short value
|
||||||
|
# name and headline
|
||||||
|
if (sug_area == '' or
|
||||||
|
('singleton' in device and device['singleton'])):
|
||||||
|
dev['name'] = device['name']
|
||||||
|
dev['sa'] = device['name']
|
||||||
|
else:
|
||||||
|
dev['name'] = device['name']+' - '+sug_area
|
||||||
|
dev['sa'] = device['name']+' - '+sug_area
|
||||||
|
if 'via' in device: # add the link to the parent device
|
||||||
|
via = device['via']
|
||||||
|
if via in self.info_devs:
|
||||||
|
via_dev = self.info_devs[via]
|
||||||
|
if 'singleton' in via_dev and via_dev['singleton']:
|
||||||
|
dev['via_device'] = via
|
||||||
|
else:
|
||||||
|
dev['via_device'] = f"{via}_{snr}"
|
||||||
|
else:
|
||||||
|
self.inc_counter('Internal_Error')
|
||||||
|
logging.error(f"Infos.info_defs: the row for "
|
||||||
|
f"{key} has an invalid via value: "
|
||||||
|
f"{via}")
|
||||||
|
for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
|
||||||
|
# values fpr 'modell', 'manufacturer', 'sw version' and
|
||||||
|
# 'hw version'
|
||||||
|
if key in device:
|
||||||
|
data = self.dev_value(device[key])
|
||||||
|
if data is not None:
|
||||||
|
dev[key] = data
|
||||||
|
if 'singleton' in device and device['singleton']:
|
||||||
|
dev['ids'] = [f"{ha['dev']}"]
|
||||||
|
else:
|
||||||
|
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||||
|
attr['dev'] = dev
|
||||||
|
origin = {}
|
||||||
|
origin['name'] = self.app_name
|
||||||
|
origin['sw'] = self.version
|
||||||
|
attr['o'] = origin
|
||||||
|
else:
|
||||||
|
self.inc_counter('Internal_Error')
|
||||||
|
logging.error(f"Infos.info_defs: the row for {key} "
|
||||||
|
"missing 'dev' value for ha register")
|
||||||
|
return json.dumps(attr), component, node_id, attr['uniq_id']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _key_obj(self, id) -> list:
|
||||||
|
d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG,
|
||||||
|
'unit': ''})
|
||||||
|
if 'ha' in d and 'must_incr' in d['ha']:
|
||||||
|
must_incr = d['ha']['must_incr']
|
||||||
|
else:
|
||||||
|
must_incr = False
|
||||||
|
|
||||||
|
return d['name'], d['level'], d['unit'], must_incr
|
||||||
|
|
||||||
|
def update_db(self, keys, must_incr, result):
|
||||||
|
name = ''
|
||||||
|
dict = self.db
|
||||||
|
for key in keys[:-1]:
|
||||||
|
if key not in dict:
|
||||||
|
dict[key] = {}
|
||||||
|
dict = dict[key]
|
||||||
|
name += key + '.'
|
||||||
|
if keys[-1] not in dict:
|
||||||
|
update = (not must_incr or result > 0)
|
||||||
|
else:
|
||||||
|
if must_incr:
|
||||||
|
update = dict[keys[-1]] < result
|
||||||
|
else:
|
||||||
|
update = dict[keys[-1]] != result
|
||||||
|
if update:
|
||||||
|
dict[keys[-1]] = result
|
||||||
|
name += keys[-1]
|
||||||
|
return name, update
|
||||||
|
|
||||||
|
def set_db_def_value(self, id, value):
|
||||||
|
'''set default value'''
|
||||||
|
row = self.info_defs[id]
|
||||||
|
if isinstance(row, dict): # pragma: no cover
|
||||||
|
keys = row['name']
|
||||||
|
self.update_db(keys, False, value)
|
||||||
|
|
||||||
|
def get_db_value(self, id, not_found_result=None):
|
||||||
|
'''get database value'''
|
||||||
|
row = self.info_defs[id]
|
||||||
|
if isinstance(row, dict): # pragma: no cover
|
||||||
|
keys = row['name']
|
||||||
|
elm = self.db
|
||||||
|
for key in keys[:-1]:
|
||||||
|
if key not in elm:
|
||||||
|
return not_found_result
|
||||||
|
elm = elm[key]
|
||||||
|
|
||||||
|
if keys[-1] in elm:
|
||||||
|
return elm[keys[-1]]
|
||||||
|
return not_found_result
|
||||||
|
|
||||||
def ignore_this_device(self, dep: dict) -> bool:
|
def ignore_this_device(self, dep: dict) -> bool:
|
||||||
'''Checks the equation in the dep dict
|
'''Checks the equation in the dep dict
|
||||||
@@ -171,233 +471,3 @@ class Infos:
|
|||||||
elif 'less_eq' in dep:
|
elif 'less_eq' in dep:
|
||||||
return not value <= dep['less_eq']
|
return not value <= dep['less_eq']
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def ha_confs(self, ha_prfx, node_id, snr, singleton: bool, sug_area=''):
|
|
||||||
'''Generator function yields a json register struct for home-assistant
|
|
||||||
auto configuration and a unique entity string
|
|
||||||
|
|
||||||
arguments:
|
|
||||||
prfx:str ==> MQTT prefix for the home assistant 'stat_t string
|
|
||||||
snr:str ==> serial number of the inverter, used to build unique
|
|
||||||
entity strings
|
|
||||||
sug_area:str ==> suggested area string from the config file'''
|
|
||||||
tab = self.__info_defs
|
|
||||||
for key in tab:
|
|
||||||
row = tab[key]
|
|
||||||
if 'singleton' in row:
|
|
||||||
if singleton != row['singleton']:
|
|
||||||
continue
|
|
||||||
elif singleton:
|
|
||||||
continue
|
|
||||||
prfx = ha_prfx + node_id
|
|
||||||
|
|
||||||
# check if we have details for home assistant
|
|
||||||
if 'ha' in row:
|
|
||||||
ha = row['ha']
|
|
||||||
if 'comp' in ha:
|
|
||||||
component = ha['comp']
|
|
||||||
else:
|
|
||||||
component = 'sensor'
|
|
||||||
attr = {}
|
|
||||||
if 'name' in ha:
|
|
||||||
attr['name'] = ha['name']
|
|
||||||
else:
|
|
||||||
attr['name'] = row['name'][-1]
|
|
||||||
|
|
||||||
attr['stat_t'] = prfx + row['name'][0]
|
|
||||||
attr['dev_cla'] = ha['dev_cla']
|
|
||||||
attr['stat_cla'] = ha['stat_cla']
|
|
||||||
attr['uniq_id'] = ha['id']+snr
|
|
||||||
if 'val_tpl' in ha:
|
|
||||||
attr['val_tpl'] = ha['val_tpl']
|
|
||||||
elif 'fmt' in ha:
|
|
||||||
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
|
||||||
else:
|
|
||||||
self.inc_counter('Internal_Error')
|
|
||||||
logging.error(f"Infos.__info_defs: the row for {key} do"
|
|
||||||
" not have a 'val_tpl' nor a 'fmt' value")
|
|
||||||
|
|
||||||
# add unit_of_meas only, if status_class isn't none. If
|
|
||||||
# status_cla is None we want a number format and not line
|
|
||||||
# graph in home assistant. A unit will change the number
|
|
||||||
# format to a line graph
|
|
||||||
if 'unit' in row and attr['stat_cla'] is not None:
|
|
||||||
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
|
||||||
if 'icon' in ha:
|
|
||||||
attr['ic'] = ha['icon'] # icon for the entity
|
|
||||||
if 'nat_prc' in ha:
|
|
||||||
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
|
||||||
if 'ent_cat' in ha:
|
|
||||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
|
||||||
|
|
||||||
# enabled_by_default is deactivated, since it avoid the via
|
|
||||||
# setup of the devices. It seems, that there is a bug in home
|
|
||||||
# assistant. tested with 'Home Assistant 2023.10.4'
|
|
||||||
# if 'en' in ha: # enabled_by_default
|
|
||||||
# attr['en'] = ha['en']
|
|
||||||
|
|
||||||
if 'dev' in ha:
|
|
||||||
device = self.__info_devs[ha['dev']]
|
|
||||||
|
|
||||||
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
|
||||||
continue
|
|
||||||
|
|
||||||
dev = {}
|
|
||||||
|
|
||||||
# the same name for 'name' and 'suggested area', so we get
|
|
||||||
# dedicated devices in home assistant with short value
|
|
||||||
# name and headline
|
|
||||||
if (sug_area == '' or
|
|
||||||
('singleton' in device and device['singleton'])):
|
|
||||||
dev['name'] = device['name']
|
|
||||||
dev['sa'] = device['name']
|
|
||||||
else:
|
|
||||||
dev['name'] = device['name']+' - '+sug_area
|
|
||||||
dev['sa'] = device['name']+' - '+sug_area
|
|
||||||
|
|
||||||
if 'via' in device: # add the link to the parent device
|
|
||||||
via = device['via']
|
|
||||||
if via in self.__info_devs:
|
|
||||||
via_dev = self.__info_devs[via]
|
|
||||||
if 'singleton' in via_dev and via_dev['singleton']:
|
|
||||||
dev['via_device'] = via
|
|
||||||
else:
|
|
||||||
dev['via_device'] = f"{via}_{snr}"
|
|
||||||
else:
|
|
||||||
self.inc_counter('Internal_Error')
|
|
||||||
logging.error(f"Infos.__info_defs: the row for "
|
|
||||||
f"{key} has an invalid via value: "
|
|
||||||
f"{via}")
|
|
||||||
|
|
||||||
for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
|
|
||||||
# values fpr 'modell', 'manufacturer', 'sw version' and
|
|
||||||
# 'hw version'
|
|
||||||
if key in device:
|
|
||||||
data = self.dev_value(device[key])
|
|
||||||
if data is not None:
|
|
||||||
dev[key] = data
|
|
||||||
|
|
||||||
if 'singleton' in device and device['singleton']:
|
|
||||||
dev['ids'] = [f"{ha['dev']}"]
|
|
||||||
else:
|
|
||||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
|
||||||
|
|
||||||
attr['dev'] = dev
|
|
||||||
|
|
||||||
origin = {}
|
|
||||||
origin['name'] = self.app_name
|
|
||||||
origin['sw'] = self.version
|
|
||||||
attr['o'] = origin
|
|
||||||
else:
|
|
||||||
self.inc_counter('Internal_Error')
|
|
||||||
logging.error(f"Infos.__info_defs: the row for {key} "
|
|
||||||
"missing 'dev' value for ha register")
|
|
||||||
|
|
||||||
yield json.dumps(attr), component, node_id, attr['uniq_id']
|
|
||||||
|
|
||||||
def inc_counter(self, counter: str) -> None:
|
|
||||||
'''inc proxy statistic counter'''
|
|
||||||
dict = self.stat['proxy']
|
|
||||||
dict[counter] += 1
|
|
||||||
|
|
||||||
def dec_counter(self, counter: str) -> None:
|
|
||||||
'''dec proxy statistic counter'''
|
|
||||||
dict = self.stat['proxy']
|
|
||||||
dict[counter] -= 1
|
|
||||||
|
|
||||||
def __key_obj(self, id) -> list:
|
|
||||||
d = self.__info_defs.get(id, {'name': None, 'level': logging.DEBUG,
|
|
||||||
'unit': ''})
|
|
||||||
if 'ha' in d and 'must_incr' in d['ha']:
|
|
||||||
must_incr = d['ha']['must_incr']
|
|
||||||
else:
|
|
||||||
must_incr = False
|
|
||||||
new_val = None
|
|
||||||
# if 'new_value' in d:
|
|
||||||
# new_val = d['new_value']
|
|
||||||
|
|
||||||
return d['name'], d['level'], d['unit'], must_incr, new_val
|
|
||||||
|
|
||||||
def parse(self, buf, ind=0) -> None:
|
|
||||||
'''parse a data sequence received from the inverter and
|
|
||||||
stores the values in Infos.db
|
|
||||||
|
|
||||||
buf: buffer of the sequence to parse'''
|
|
||||||
result = struct.unpack_from('!l', buf, ind)
|
|
||||||
elms = result[0]
|
|
||||||
i = 0
|
|
||||||
ind += 4
|
|
||||||
while i < elms:
|
|
||||||
result = struct.unpack_from('!lB', buf, ind)
|
|
||||||
info_id = result[0]
|
|
||||||
data_type = result[1]
|
|
||||||
ind += 5
|
|
||||||
keys, level, unit, must_incr, new_val = self.__key_obj(info_id)
|
|
||||||
|
|
||||||
if data_type == 0x54: # 'T' -> Pascal-String
|
|
||||||
str_len = buf[ind]
|
|
||||||
result = struct.unpack_from(f'!{str_len+1}p', buf,
|
|
||||||
ind)[0].decode(encoding='ascii',
|
|
||||||
errors='replace')
|
|
||||||
ind += str_len+1
|
|
||||||
|
|
||||||
elif data_type == 0x49: # 'I' -> int32
|
|
||||||
# if new_val:
|
|
||||||
# struct.pack_into('!l', buf, ind, new_val)
|
|
||||||
result = struct.unpack_from('!l', buf, ind)[0]
|
|
||||||
ind += 4
|
|
||||||
|
|
||||||
elif data_type == 0x53: # 'S' -> short
|
|
||||||
# if new_val:
|
|
||||||
# struct.pack_into('!h', buf, ind, new_val)
|
|
||||||
result = struct.unpack_from('!h', buf, ind)[0]
|
|
||||||
ind += 2
|
|
||||||
|
|
||||||
elif data_type == 0x46: # 'F' -> float32
|
|
||||||
# if new_val:
|
|
||||||
# struct.pack_into('!f', buf, ind, new_val)
|
|
||||||
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
|
|
||||||
ind += 4
|
|
||||||
|
|
||||||
elif data_type == 0x4c: # 'L' -> int64
|
|
||||||
# if new_val:
|
|
||||||
# struct.pack_into('!q', buf, ind, new_val)
|
|
||||||
result = struct.unpack_from('!q', buf, ind)[0]
|
|
||||||
ind += 8
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.inc_counter('Invalid_Data_Type')
|
|
||||||
logging.error(f"Infos.parse: data_type: {data_type}"
|
|
||||||
" not supported")
|
|
||||||
return
|
|
||||||
|
|
||||||
if keys:
|
|
||||||
dict = self.db
|
|
||||||
name = ''
|
|
||||||
|
|
||||||
for key in keys[:-1]:
|
|
||||||
if key not in dict:
|
|
||||||
dict[key] = {}
|
|
||||||
dict = dict[key]
|
|
||||||
name += key + '.'
|
|
||||||
|
|
||||||
if keys[-1] not in dict:
|
|
||||||
update = (not must_incr or result > 0)
|
|
||||||
else:
|
|
||||||
if must_incr:
|
|
||||||
update = dict[keys[-1]] < result
|
|
||||||
else:
|
|
||||||
update = dict[keys[-1]] != result
|
|
||||||
|
|
||||||
if update:
|
|
||||||
dict[keys[-1]] = result
|
|
||||||
name += keys[-1]
|
|
||||||
yield keys[0], update
|
|
||||||
else:
|
|
||||||
update = False
|
|
||||||
name = str(f'info-id.0x{info_id:x}')
|
|
||||||
|
|
||||||
self.tracer.log(level, f'{name} : {result}{unit}'
|
|
||||||
f' update: {update}')
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|||||||
@@ -1,48 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
|
||||||
import json
|
import json
|
||||||
from config import Config
|
from config import Config
|
||||||
from async_stream import AsyncStream
|
|
||||||
from mqtt import Mqtt
|
from mqtt import Mqtt
|
||||||
from aiomqtt import MqttCodeError
|
|
||||||
from infos import Infos
|
from infos import Infos
|
||||||
|
|
||||||
# import gc
|
|
||||||
|
|
||||||
# logger = logging.getLogger('conn')
|
# logger = logging.getLogger('conn')
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
class Inverter(AsyncStream):
|
class Inverter():
|
||||||
'''class Inverter is a derivation of an Async_Stream
|
|
||||||
|
|
||||||
The class has some class method for managing common resources like a
|
|
||||||
connection to the MQTT broker or proxy error counter which are common
|
|
||||||
for all inverter connection
|
|
||||||
|
|
||||||
Instances of the class are connections to an inverter and can have an
|
|
||||||
optional link to an remote connection to the TSUN cloud. A remote
|
|
||||||
connection dies with the inverter connection.
|
|
||||||
|
|
||||||
class methods:
|
|
||||||
class_init(): initialize the common resources of the proxy (MQTT
|
|
||||||
broker, Proxy DB, etc). Must be called before the
|
|
||||||
first inverter instance can be created
|
|
||||||
class_close(): release the common resources of the proxy. Should not
|
|
||||||
be called before any instances of the class are
|
|
||||||
destroyed
|
|
||||||
|
|
||||||
methods:
|
|
||||||
server_loop(addr): Async loop method for receiving messages from the
|
|
||||||
inverter (server-side)
|
|
||||||
client_loop(addr): Async loop method for receiving messages from the
|
|
||||||
TSUN cloud (client-side)
|
|
||||||
async_create_remote(): Establish a client connection to the TSUN cloud
|
|
||||||
async_publ_mqtt(): Publish data to MQTT broker
|
|
||||||
close(): Release method which must be called before a instance can be
|
|
||||||
destroyed
|
|
||||||
'''
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def class_init(cls) -> None:
|
def class_init(cls) -> None:
|
||||||
logging.debug('Inverter.class_init')
|
logging.debug('Inverter.class_init')
|
||||||
@@ -57,38 +24,37 @@ class Inverter(AsyncStream):
|
|||||||
cls.proxy_unique_id = ha['proxy_unique_id']
|
cls.proxy_unique_id = ha['proxy_unique_id']
|
||||||
|
|
||||||
# call Mqtt singleton to establisch the connection to the mqtt broker
|
# call Mqtt singleton to establisch the connection to the mqtt broker
|
||||||
cls.mqtt = Mqtt(cls.__cb_mqtt_is_up)
|
cls.mqtt = Mqtt(cls._cb_mqtt_is_up)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def __cb_mqtt_is_up(cls) -> None:
|
async def _cb_mqtt_is_up(cls) -> None:
|
||||||
logging.info('Initialize proxy device on home assistant')
|
logging.info('Initialize proxy device on home assistant')
|
||||||
# register proxy status counters at home assistant
|
# register proxy status counters at home assistant
|
||||||
await cls.__register_proxy_stat_home_assistant()
|
await cls._register_proxy_stat_home_assistant()
|
||||||
|
|
||||||
# send values of the proxy status counters
|
# send values of the proxy status counters
|
||||||
await asyncio.sleep(0.5) # wait a bit, before sending data
|
await asyncio.sleep(0.5) # wait a bit, before sending data
|
||||||
cls.new_stat_data['proxy'] = True # force sending data to sync ha
|
Infos.new_stat_data['proxy'] = True # force sending data to sync ha
|
||||||
await cls.__async_publ_mqtt_proxy_stat('proxy')
|
await cls._async_publ_mqtt_proxy_stat('proxy')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def __register_proxy_stat_home_assistant(cls) -> None:
|
async def _register_proxy_stat_home_assistant(cls) -> None:
|
||||||
'''register all our topics at home assistant'''
|
'''register all our topics at home assistant'''
|
||||||
for data_json, component, node_id, id in cls.db_stat.ha_confs(
|
for data_json, component, node_id, id in cls.db_stat.ha_proxy_confs(
|
||||||
cls.entity_prfx, cls.proxy_node_id,
|
cls.entity_prfx, cls.proxy_node_id, cls.proxy_unique_id):
|
||||||
cls.proxy_unique_id, True):
|
|
||||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}") # noqa: E501
|
logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}") # noqa: E501
|
||||||
await cls.mqtt.publish(f'{cls.discovery_prfx}{component}/{node_id}{id}/config', data_json) # noqa: E501
|
await cls.mqtt.publish(f'{cls.discovery_prfx}{component}/{node_id}{id}/config', data_json) # noqa: E501
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def __async_publ_mqtt_proxy_stat(cls, key) -> None:
|
async def _async_publ_mqtt_proxy_stat(cls, key) -> None:
|
||||||
stat = Infos.stat
|
stat = Infos.stat
|
||||||
if key in stat and cls.new_stat_data[key]:
|
if key in stat and Infos.new_stat_data[key]:
|
||||||
data_json = json.dumps(stat[key])
|
data_json = json.dumps(stat[key])
|
||||||
node_id = cls.proxy_node_id
|
node_id = cls.proxy_node_id
|
||||||
logger_mqtt.debug(f'{key}: {data_json}')
|
logger_mqtt.debug(f'{key}: {data_json}')
|
||||||
await cls.mqtt.publish(f"{cls.entity_prfx}{node_id}{key}",
|
await cls.mqtt.publish(f"{cls.entity_prfx}{node_id}{key}",
|
||||||
data_json)
|
data_json)
|
||||||
cls.new_stat_data[key] = False
|
Infos.new_stat_data[key] = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def class_close(cls, loop) -> None:
|
def class_close(cls, loop) -> None:
|
||||||
@@ -96,121 +62,3 @@ class Inverter(AsyncStream):
|
|||||||
logging.info('Close MQTT Task')
|
logging.info('Close MQTT Task')
|
||||||
loop.run_until_complete(cls.mqtt.close())
|
loop.run_until_complete(cls.mqtt.close())
|
||||||
cls.mqtt = None
|
cls.mqtt = None
|
||||||
|
|
||||||
def __init__(self, reader, writer, addr):
|
|
||||||
super().__init__(reader, writer, addr, None, True)
|
|
||||||
self.ha_restarts = -1
|
|
||||||
|
|
||||||
async def server_loop(self, addr):
|
|
||||||
'''Loop for receiving messages from the inverter (server-side)'''
|
|
||||||
logging.info(f'Accept connection from {addr}')
|
|
||||||
self.inc_counter('Inverter_Cnt')
|
|
||||||
await self.loop()
|
|
||||||
self.dec_counter('Inverter_Cnt')
|
|
||||||
logging.info(f'Server loop stopped for r{self.r_addr}')
|
|
||||||
|
|
||||||
# if the server connection closes, we also have to disconnect
|
|
||||||
# the connection to te TSUN cloud
|
|
||||||
if self.remoteStream:
|
|
||||||
logging.debug("disconnect client connection")
|
|
||||||
self.remoteStream.disc()
|
|
||||||
try:
|
|
||||||
await self.__async_publ_mqtt_proxy_stat('proxy')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def client_loop(self, addr):
|
|
||||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
|
||||||
clientStream = await self.remoteStream.loop()
|
|
||||||
logging.info(f'Client loop stopped for l{clientStream.l_addr}')
|
|
||||||
|
|
||||||
# if the client connection closes, we don't touch the server
|
|
||||||
# connection. Instead we erase the client connection stream,
|
|
||||||
# thus on the next received packet from the inverter, we can
|
|
||||||
# establish a new connection to the TSUN cloud
|
|
||||||
|
|
||||||
# erase backlink to inverter
|
|
||||||
clientStream.remoteStream = None
|
|
||||||
|
|
||||||
if self.remoteStream == clientStream:
|
|
||||||
# logging.debug(f'Client l{clientStream.l_addr} refs:'
|
|
||||||
# f' {gc.get_referrers(clientStream)}')
|
|
||||||
# than erase client connection
|
|
||||||
self.remoteStream = None
|
|
||||||
|
|
||||||
async def async_create_remote(self) -> None:
|
|
||||||
'''Establish a client connection to the TSUN cloud'''
|
|
||||||
tsun = Config.get('tsun')
|
|
||||||
host = tsun['host']
|
|
||||||
port = tsun['port']
|
|
||||||
addr = (host, port)
|
|
||||||
|
|
||||||
try:
|
|
||||||
logging.info(f'Connected to {addr}')
|
|
||||||
connect = asyncio.open_connection(host, port)
|
|
||||||
reader, writer = await connect
|
|
||||||
self.remoteStream = AsyncStream(reader, writer, addr, self,
|
|
||||||
False, self.id_str)
|
|
||||||
asyncio.create_task(self.client_loop(addr))
|
|
||||||
|
|
||||||
except ConnectionRefusedError as error:
|
|
||||||
logging.info(f'{error}')
|
|
||||||
except Exception:
|
|
||||||
self.inc_counter('SW_Exception')
|
|
||||||
logging.error(
|
|
||||||
f"Inverter: Exception for {addr}:\n"
|
|
||||||
f"{traceback.format_exc()}")
|
|
||||||
|
|
||||||
async def async_publ_mqtt(self) -> None:
|
|
||||||
'''publish data to MQTT broker'''
|
|
||||||
# check if new inverter or collector infos are available or when the
|
|
||||||
# home assistant has changed the status back to online
|
|
||||||
try:
|
|
||||||
if (('inverter' in self.new_data and self.new_data['inverter'])
|
|
||||||
or ('collector' in self.new_data and
|
|
||||||
self.new_data['collector'])
|
|
||||||
or self.mqtt.ha_restarts != self.ha_restarts):
|
|
||||||
await self.__register_proxy_stat_home_assistant()
|
|
||||||
await self.__register_home_assistant()
|
|
||||||
self.ha_restarts = self.mqtt.ha_restarts
|
|
||||||
|
|
||||||
for key in self.new_data:
|
|
||||||
await self.__async_publ_mqtt_packet(key)
|
|
||||||
for key in self.new_stat_data:
|
|
||||||
await self.__async_publ_mqtt_proxy_stat(key)
|
|
||||||
|
|
||||||
except MqttCodeError as error:
|
|
||||||
logging.error(f'Mqtt except: {error}')
|
|
||||||
except Exception:
|
|
||||||
self.inc_counter('SW_Exception')
|
|
||||||
logging.error(
|
|
||||||
f"Inverter: Exception:\n"
|
|
||||||
f"{traceback.format_exc()}")
|
|
||||||
|
|
||||||
async def __async_publ_mqtt_packet(self, key):
|
|
||||||
db = self.db.db
|
|
||||||
if key in db and self.new_data[key]:
|
|
||||||
data_json = json.dumps(db[key])
|
|
||||||
node_id = self.node_id
|
|
||||||
logger_mqtt.debug(f'{key}: {data_json}')
|
|
||||||
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
|
||||||
self.new_data[key] = False
|
|
||||||
|
|
||||||
async def __register_home_assistant(self) -> None:
|
|
||||||
'''register all our topics at home assistant'''
|
|
||||||
for data_json, component, node_id, id in self.db.ha_confs(
|
|
||||||
self.entity_prfx, self.node_id, self.unique_id,
|
|
||||||
False, self.sug_area):
|
|
||||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
|
|
||||||
f" node_id:'{node_id}' {data_json}")
|
|
||||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
|
|
||||||
f"/{node_id}{id}/config", data_json)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
logging.debug(f'Inverter.close() l{self.l_addr} | r{self.r_addr}')
|
|
||||||
super().close() # call close handler in the parent class
|
|
||||||
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
logging.debug("Inverter.__del__")
|
|
||||||
super().__del__()
|
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import struct
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
if __name__ == "app.src.messages":
|
if __name__ == "app.src.messages":
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Infos
|
||||||
from app.src.config import Config
|
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
from infos import Infos
|
from infos import Infos
|
||||||
from config import Config
|
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -45,23 +40,6 @@ def hex_dump_memory(level, info, data, num):
|
|||||||
tracer.log(level, '\n'.join(lines))
|
tracer.log(level, '\n'.join(lines))
|
||||||
|
|
||||||
|
|
||||||
class Control:
|
|
||||||
def __init__(self, ctrl: int):
|
|
||||||
self.ctrl = ctrl
|
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.ctrl
|
|
||||||
|
|
||||||
def is_ind(self) -> bool:
|
|
||||||
return (self.ctrl == 0x91)
|
|
||||||
|
|
||||||
def is_req(self) -> bool:
|
|
||||||
return (self.ctrl == 0x70)
|
|
||||||
|
|
||||||
def is_resp(self) -> bool:
|
|
||||||
return (self.ctrl == 0x99)
|
|
||||||
|
|
||||||
|
|
||||||
class IterRegistry(type):
|
class IterRegistry(type):
|
||||||
def __iter__(cls):
|
def __iter__(cls):
|
||||||
for ref in cls._registry:
|
for ref in cls._registry:
|
||||||
@@ -72,10 +50,10 @@ class IterRegistry(type):
|
|||||||
|
|
||||||
class Message(metaclass=IterRegistry):
|
class Message(metaclass=IterRegistry):
|
||||||
_registry = []
|
_registry = []
|
||||||
new_stat_data = {}
|
|
||||||
|
|
||||||
def __init__(self, server_side: bool, id_str=b''):
|
def __init__(self, server_side: bool):
|
||||||
self._registry.append(weakref.ref(self))
|
self._registry.append(weakref.ref(self))
|
||||||
|
|
||||||
self.server_side = server_side
|
self.server_side = server_side
|
||||||
self.header_valid = False
|
self.header_valid = False
|
||||||
self.header_len = 0
|
self.header_len = 0
|
||||||
@@ -83,22 +61,10 @@ class Message(metaclass=IterRegistry):
|
|||||||
self.unique_id = 0
|
self.unique_id = 0
|
||||||
self.node_id = ''
|
self.node_id = ''
|
||||||
self.sug_area = ''
|
self.sug_area = ''
|
||||||
self.await_conn_resp_cnt = 0
|
|
||||||
self.id_str = id_str
|
|
||||||
self.contact_name = b''
|
|
||||||
self.contact_mail = b''
|
|
||||||
self._recv_buffer = bytearray(0)
|
self._recv_buffer = bytearray(0)
|
||||||
self._send_buffer = bytearray(0)
|
self._send_buffer = bytearray(0)
|
||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
self.db = Infos()
|
|
||||||
self.new_data = {}
|
self.new_data = {}
|
||||||
self.switch = {
|
|
||||||
0x00: self.msg_contact_info,
|
|
||||||
0x13: self.msg_ota_update,
|
|
||||||
0x22: self.msg_get_time,
|
|
||||||
0x71: self.msg_collector_data,
|
|
||||||
0x04: self.msg_inverter_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Empty methods, that have to be implemented in any child class which
|
Empty methods, that have to be implemented in any child class which
|
||||||
@@ -112,306 +78,12 @@ class Message(metaclass=IterRegistry):
|
|||||||
Our puplic methods
|
Our puplic methods
|
||||||
'''
|
'''
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
# we have refernces to methods of this class in self.switch
|
pass # pragma: no cover
|
||||||
# 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()
|
|
||||||
|
|
||||||
def inc_counter(self, counter: str) -> None:
|
def inc_counter(self, counter: str) -> None:
|
||||||
self.db.inc_counter(counter)
|
self.db.inc_counter(counter)
|
||||||
self.new_stat_data['proxy'] = True
|
Infos.new_stat_data['proxy'] = True
|
||||||
|
|
||||||
def dec_counter(self, counter: str) -> None:
|
def dec_counter(self, counter: str) -> None:
|
||||||
self.db.dec_counter(counter)
|
self.db.dec_counter(counter)
|
||||||
self.new_stat_data['proxy'] = True
|
Infos.new_stat_data['proxy'] = True
|
||||||
|
|
||||||
def set_serial_no(self, serial_no: str):
|
|
||||||
|
|
||||||
if self.unique_id == serial_no:
|
|
||||||
logger.debug(f'SerialNo: {serial_no}')
|
|
||||||
else:
|
|
||||||
inverters = Config.get('inverters')
|
|
||||||
# logger.debug(f'Inverters: {inverters}')
|
|
||||||
|
|
||||||
if serial_no in inverters:
|
|
||||||
inv = inverters[serial_no]
|
|
||||||
self.node_id = inv['node_id']
|
|
||||||
self.sug_area = inv['suggested_area']
|
|
||||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
|
||||||
else:
|
|
||||||
self.node_id = ''
|
|
||||||
self.sug_area = ''
|
|
||||||
if 'allow_all' not in inverters or not inverters['allow_all']:
|
|
||||||
self.inc_counter('Unknown_SNR')
|
|
||||||
self.unique_id = None
|
|
||||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
|
||||||
return
|
|
||||||
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
|
||||||
|
|
||||||
self.unique_id = serial_no
|
|
||||||
|
|
||||||
def read(self) -> None:
|
|
||||||
self._read()
|
|
||||||
|
|
||||||
if not self.header_valid:
|
|
||||||
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
|
||||||
|
|
||||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
|
|
||||||
self.data_len):
|
|
||||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
|
||||||
self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|
||||||
self.set_serial_no(self.id_str.decode("utf-8"))
|
|
||||||
self.__dispatch_msg()
|
|
||||||
self.__flush_recv_msg()
|
|
||||||
return
|
|
||||||
|
|
||||||
def forward(self, buffer, buflen) -> None:
|
|
||||||
tsun = Config.get('tsun')
|
|
||||||
if tsun['enabled']:
|
|
||||||
self._forward_buffer = buffer[:buflen]
|
|
||||||
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
|
|
||||||
buffer, buflen)
|
|
||||||
|
|
||||||
self.__parse_header(self._forward_buffer,
|
|
||||||
len(self._forward_buffer))
|
|
||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
|
||||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
|
||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
|
||||||
return
|
|
||||||
|
|
||||||
def _init_new_client_conn(self, contact_name, contact_mail) -> None:
|
|
||||||
logger.info(f'name: {contact_name} mail: {contact_mail}')
|
|
||||||
self.msg_id = 0
|
|
||||||
self.await_conn_resp_cnt += 1
|
|
||||||
self.__build_header(0x91)
|
|
||||||
self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
|
|
||||||
f'{len(contact_mail)+1}p',
|
|
||||||
contact_name, contact_mail)
|
|
||||||
|
|
||||||
self.__finish_send_msg()
|
|
||||||
|
|
||||||
'''
|
|
||||||
Our private methods
|
|
||||||
'''
|
|
||||||
def __flow_str(self, server_side: bool, type:
|
|
||||||
('rx', 'tx', 'forwrd', 'drop')): # 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): # pragma: no cover
|
|
||||||
if False:
|
|
||||||
# utc as epoche
|
|
||||||
ts = time.time()
|
|
||||||
else:
|
|
||||||
# convert localtime in epoche
|
|
||||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
|
||||||
return round(ts*1000)
|
|
||||||
|
|
||||||
# check if there is a complete header in the buffer, parse it
|
|
||||||
# and set
|
|
||||||
# self.header_len
|
|
||||||
# self.data_len
|
|
||||||
# self.id_str
|
|
||||||
# self.ctrl
|
|
||||||
# self.msg_id
|
|
||||||
#
|
|
||||||
# if the header is incomplete, than self.header_len is still 0
|
|
||||||
#
|
|
||||||
def __parse_header(self, buf: bytes, buf_len: int) -> None:
|
|
||||||
|
|
||||||
if (buf_len < 5): # enough bytes to read len and id_len?
|
|
||||||
return
|
|
||||||
result = struct.unpack_from('!lB', buf, 0)
|
|
||||||
len = result[0] # len of complete message
|
|
||||||
id_len = result[1] # len of variable id string
|
|
||||||
|
|
||||||
hdr_len = 5+id_len+2
|
|
||||||
|
|
||||||
if (buf_len < hdr_len): # enough bytes for complete header?
|
|
||||||
return
|
|
||||||
|
|
||||||
result = struct.unpack_from(f'!{id_len+1}pBB', buf, 4)
|
|
||||||
|
|
||||||
# store parsed header values in the class
|
|
||||||
self.id_str = result[0]
|
|
||||||
self.ctrl = Control(result[1])
|
|
||||||
self.msg_id = result[2]
|
|
||||||
self.data_len = len-id_len-3
|
|
||||||
self.header_len = hdr_len
|
|
||||||
self.header_valid = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def __build_header(self, ctrl) -> None:
|
|
||||||
self.send_msg_ofs = len(self._send_buffer)
|
|
||||||
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
|
|
||||||
0, self.id_str, ctrl, self.msg_id)
|
|
||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
|
||||||
logger.info(self.__flow_str(self.server_side, 'tx') +
|
|
||||||
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
|
|
||||||
|
|
||||||
def __finish_send_msg(self) -> None:
|
|
||||||
_len = len(self._send_buffer) - self.send_msg_ofs
|
|
||||||
struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4)
|
|
||||||
|
|
||||||
def __dispatch_msg(self) -> None:
|
|
||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
|
||||||
if self.unique_id:
|
|
||||||
logger.info(self.__flow_str(self.server_side, 'rx') +
|
|
||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
|
||||||
fnc()
|
|
||||||
else:
|
|
||||||
logger.info(self.__flow_str(self.server_side, 'drop') +
|
|
||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
|
||||||
|
|
||||||
def __flush_recv_msg(self) -> None:
|
|
||||||
self._recv_buffer = self._recv_buffer[(self.header_len+self.data_len):]
|
|
||||||
self.header_valid = False
|
|
||||||
|
|
||||||
'''
|
|
||||||
Message handler methods
|
|
||||||
'''
|
|
||||||
def msg_contact_info(self):
|
|
||||||
if self.ctrl.is_ind():
|
|
||||||
if self.server_side and self.__process_contact_info():
|
|
||||||
self.__build_header(0x91)
|
|
||||||
self._send_buffer += b'\x01'
|
|
||||||
self.__finish_send_msg()
|
|
||||||
# don't forward this contact info here, we will build one
|
|
||||||
# when the remote connection is established
|
|
||||||
elif self.await_conn_resp_cnt > 0:
|
|
||||||
self.await_conn_resp_cnt -= 1
|
|
||||||
else:
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|
||||||
def __process_contact_info(self) -> bool:
|
|
||||||
result = struct.unpack_from('!B', self._recv_buffer, self.header_len)
|
|
||||||
name_len = result[0]
|
|
||||||
if self.data_len < name_len+2:
|
|
||||||
return False
|
|
||||||
result = struct.unpack_from(f'!{name_len+1}pB', self._recv_buffer,
|
|
||||||
self.header_len)
|
|
||||||
self.contact_name = result[0]
|
|
||||||
mail_len = result[1]
|
|
||||||
logger.info(f'name: {self.contact_name}')
|
|
||||||
|
|
||||||
result = struct.unpack_from(f'!{mail_len+1}p', self._recv_buffer,
|
|
||||||
self.header_len+name_len+1)
|
|
||||||
self.contact_mail = result[0]
|
|
||||||
logger.info(f'mail: {self.contact_mail}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
def msg_get_time(self):
|
|
||||||
tsun = Config.get('tsun')
|
|
||||||
if tsun['enabled']:
|
|
||||||
if self.ctrl.is_ind():
|
|
||||||
if self.data_len >= 8:
|
|
||||||
ts = self._timestamp()
|
|
||||||
result = struct.unpack_from('!q', self._recv_buffer,
|
|
||||||
self.header_len)
|
|
||||||
logger.debug(f'tsun-time: {result[0]:08x}'
|
|
||||||
f' proxy-time: {ts:08x}')
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
else:
|
|
||||||
if self.ctrl.is_ind():
|
|
||||||
if self.data_len == 0:
|
|
||||||
ts = self._timestamp()
|
|
||||||
logger.debug(f'time: {ts:08x}')
|
|
||||||
|
|
||||||
self.__build_header(0x91)
|
|
||||||
self._send_buffer += struct.pack('!q', ts)
|
|
||||||
self.__finish_send_msg()
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
|
|
||||||
def parse_msg_header(self):
|
|
||||||
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
|
|
||||||
|
|
||||||
data_id = result[0] # len of complete message
|
|
||||||
id_len = result[1] # len of variable id string
|
|
||||||
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
|
|
||||||
|
|
||||||
msg_hdr_len = 5+id_len+9
|
|
||||||
|
|
||||||
result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer,
|
|
||||||
self.header_len + 4)
|
|
||||||
|
|
||||||
logger.debug(f'ID: {result[0]} B: {result[1]}')
|
|
||||||
logger.debug(f'time: {result[2]:08x}')
|
|
||||||
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
|
|
||||||
# "%Y-%m-%d %H:%M:%S")}')
|
|
||||||
return msg_hdr_len
|
|
||||||
|
|
||||||
def msg_collector_data(self):
|
|
||||||
if self.ctrl.is_ind():
|
|
||||||
self.__build_header(0x99)
|
|
||||||
self._send_buffer += b'\x01'
|
|
||||||
self.__finish_send_msg()
|
|
||||||
self.__process_data()
|
|
||||||
|
|
||||||
elif self.ctrl.is_resp():
|
|
||||||
return # ignore received response
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|
||||||
def msg_inverter_data(self):
|
|
||||||
if self.ctrl.is_ind():
|
|
||||||
self.__build_header(0x99)
|
|
||||||
self._send_buffer += b'\x01'
|
|
||||||
self.__finish_send_msg()
|
|
||||||
self.__process_data()
|
|
||||||
|
|
||||||
elif self.ctrl.is_resp():
|
|
||||||
return # ignore received response
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|
||||||
def __process_data(self):
|
|
||||||
msg_hdr_len = self.parse_msg_header()
|
|
||||||
|
|
||||||
for key, update in self.db.parse(self._recv_buffer, self.header_len
|
|
||||||
+ msg_hdr_len):
|
|
||||||
if update:
|
|
||||||
self.new_data[key] = True
|
|
||||||
|
|
||||||
def msg_ota_update(self):
|
|
||||||
if self.ctrl.is_req():
|
|
||||||
self.inc_counter('OTA_Start_Msg')
|
|
||||||
elif self.ctrl.is_ind():
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown Ctrl')
|
|
||||||
self.inc_counter('Unknown_Ctrl')
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|
||||||
def msg_unknown(self):
|
|
||||||
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
|
|
||||||
self.inc_counter('Unknown_Msg')
|
|
||||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class Singleton(type):
|
|||||||
|
|
||||||
|
|
||||||
class Mqtt(metaclass=Singleton):
|
class Mqtt(metaclass=Singleton):
|
||||||
client = None
|
__client = None
|
||||||
cb_MqttIsUp = None
|
__cb_MqttIsUp = None
|
||||||
|
|
||||||
def __init__(self, cb_MqttIsUp):
|
def __init__(self, cb_MqttIsUp):
|
||||||
logger_mqtt.debug('MQTT: __init__')
|
logger_mqtt.debug('MQTT: __init__')
|
||||||
@@ -50,8 +50,8 @@ class Mqtt(metaclass=Singleton):
|
|||||||
|
|
||||||
async def publish(self, topic: str, payload: str | bytes | bytearray
|
async def publish(self, topic: str, payload: str | bytes | bytearray
|
||||||
| int | float | None = None) -> None:
|
| int | float | None = None) -> None:
|
||||||
if self.client:
|
if self.__client:
|
||||||
await self.client.publish(topic, payload)
|
await self.__client.publish(topic, payload)
|
||||||
|
|
||||||
async def __loop(self) -> None:
|
async def __loop(self) -> None:
|
||||||
mqtt = Config.get('mqtt')
|
mqtt = Config.get('mqtt')
|
||||||
@@ -59,21 +59,23 @@ class Mqtt(metaclass=Singleton):
|
|||||||
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
|
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
|
||||||
f'{mqtt["port"]} '
|
f'{mqtt["port"]} '
|
||||||
f'user:{mqtt["user"]}')
|
f'user:{mqtt["user"]}')
|
||||||
self.client = aiomqtt.Client(hostname=mqtt['host'], port=mqtt['port'],
|
self.__client = aiomqtt.Client(hostname=mqtt['host'],
|
||||||
|
port=mqtt['port'],
|
||||||
username=mqtt['user'],
|
username=mqtt['user'],
|
||||||
password=mqtt['passwd'])
|
password=mqtt['passwd'])
|
||||||
|
|
||||||
interval = 5 # Seconds
|
interval = 5 # Seconds
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with self.client:
|
async with self.__client:
|
||||||
logger_mqtt.info('MQTT broker connection established')
|
logger_mqtt.info('MQTT broker connection established')
|
||||||
|
|
||||||
if self.cb_MqttIsUp:
|
if self.cb_MqttIsUp:
|
||||||
await self.cb_MqttIsUp()
|
await self.cb_MqttIsUp()
|
||||||
|
|
||||||
async with self.client.messages() as messages:
|
async with self.__client.messages() as messages:
|
||||||
await self.client.subscribe(f"{ha['auto_conf_prefix']}"
|
await self.__client.subscribe(
|
||||||
|
f"{ha['auto_conf_prefix']}"
|
||||||
"/status")
|
"/status")
|
||||||
async for message in messages:
|
async for message in messages:
|
||||||
status = message.payload.decode("UTF-8")
|
status = message.payload.decode("UTF-8")
|
||||||
@@ -89,5 +91,5 @@ class Mqtt(metaclass=Singleton):
|
|||||||
await asyncio.sleep(interval)
|
await asyncio.sleep(interval)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger_mqtt.debug("MQTT task cancelled")
|
logger_mqtt.debug("MQTT task cancelled")
|
||||||
self.client = None
|
self.__client = None
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import signal
|
|||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
from logging import config # noqa F401
|
from logging import config # noqa F401
|
||||||
from async_stream import AsyncStream
|
from messages import Message
|
||||||
from inverter import Inverter
|
from inverter import Inverter
|
||||||
|
from gen3.inverter_g3 import InverterG3
|
||||||
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +15,14 @@ async def handle_client(reader, writer):
|
|||||||
'''Handles a new incoming connection and starts an async loop'''
|
'''Handles a new incoming connection and starts an async loop'''
|
||||||
|
|
||||||
addr = writer.get_extra_info('peername')
|
addr = writer.get_extra_info('peername')
|
||||||
await Inverter(reader, writer, addr).server_loop(addr)
|
await InverterG3(reader, writer, addr).server_loop(addr)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_client_v2(reader, writer):
|
||||||
|
'''Handles a new incoming connection and starts an async loop'''
|
||||||
|
|
||||||
|
addr = writer.get_extra_info('peername')
|
||||||
|
await InverterG3P(reader, writer, addr).server_loop(addr)
|
||||||
|
|
||||||
|
|
||||||
def handle_SIGTERM(loop):
|
def handle_SIGTERM(loop):
|
||||||
@@ -24,7 +33,7 @@ def handle_SIGTERM(loop):
|
|||||||
#
|
#
|
||||||
# first, close all open TCP connections
|
# first, close all open TCP connections
|
||||||
#
|
#
|
||||||
for stream in AsyncStream:
|
for stream in Message:
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -81,11 +90,12 @@ if __name__ == "__main__":
|
|||||||
functools.partial(handle_SIGTERM, loop))
|
functools.partial(handle_SIGTERM, loop))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Create a task for our listening server. This must be a task! If we call
|
# Create taska for our listening servera. These must be tasks! If we call
|
||||||
# start_server directly out of our main task, the eventloop will be blocked
|
# start_server directly out of our main task, the eventloop will be blocked
|
||||||
# and we can't receive and handle the UNIX signals!
|
# and we can't receive and handle the UNIX signals!
|
||||||
#
|
#
|
||||||
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
|
||||||
|
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest, json
|
import pytest, json
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Register
|
||||||
|
from app.src.gen3.infos_g3 import InfosG3
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ContrDataSeq(): # Get Time Request message
|
def ContrDataSeq(): # Get Time Request message
|
||||||
@@ -176,7 +177,7 @@ def InvDataSeq2_Zero(): # Data indication from the controller
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_control(ContrDataSeq):
|
def test_parse_control(ContrDataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
for key, result in i.parse (ContrDataSeq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ def test_parse_control(ContrDataSeq):
|
|||||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
def test_parse_control2(Contr2DataSeq):
|
def test_parse_control2(Contr2DataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
for key, result in i.parse (Contr2DataSeq):
|
for key, result in i.parse (Contr2DataSeq):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ def test_parse_control2(Contr2DataSeq):
|
|||||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
def test_parse_inverter(InvDataSeq):
|
def test_parse_inverter(InvDataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
for key, result in i.parse (InvDataSeq):
|
for key, result in i.parse (InvDataSeq):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ def test_parse_inverter(InvDataSeq):
|
|||||||
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||||
|
|
||||||
def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
for key, result in i.parse (ContrDataSeq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -214,11 +215,11 @@ def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
|||||||
|
|
||||||
|
|
||||||
def test_build_ha_conf1(ContrDataSeq):
|
def test_build_ha_conf1(ContrDataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', singleton=False):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
assert comp == 'sensor'
|
assert comp == 'sensor'
|
||||||
@@ -249,7 +250,7 @@ def test_build_ha_conf1(ContrDataSeq):
|
|||||||
assert tests==4
|
assert tests==4
|
||||||
|
|
||||||
|
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456', singleton=True):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
assert False
|
assert False
|
||||||
@@ -270,7 +271,7 @@ def test_build_ha_conf1(ContrDataSeq):
|
|||||||
assert tests==5
|
assert tests==5
|
||||||
|
|
||||||
def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
for key, result in i.parse (ContrDataSeq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass
|
pass
|
||||||
for key, result in i.parse (InvDataSeq):
|
for key, result in i.parse (InvDataSeq):
|
||||||
@@ -279,7 +280,7 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', singleton=False, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
assert comp == 'sensor'
|
assert comp == 'sensor'
|
||||||
@@ -308,21 +309,16 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
|||||||
assert tests==5
|
assert tests==5
|
||||||
|
|
||||||
def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
if key == 'total':
|
if key == 'total' or key == 'inverter' or key == 'env':
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
elif key == 'env':
|
assert tests==5
|
||||||
assert update == True
|
|
||||||
tests +=1
|
|
||||||
|
|
||||||
|
|
||||||
assert tests==4
|
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
@@ -332,11 +328,11 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
|
assert tests==3
|
||||||
assert tests==4
|
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||||
|
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2_Zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
@@ -347,13 +343,13 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==4
|
assert tests==3
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "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_Temp": 0, "Rated_Power": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2_Zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
@@ -363,10 +359,10 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==4
|
assert tests==3
|
||||||
assert json.dumps(i.db['total']) == json.dumps({})
|
assert json.dumps(i.db['total']) == json.dumps({})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "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_Temp": 0, "Rated_Power": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2_Zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
@@ -377,10 +373,10 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==4
|
assert tests==3
|
||||||
assert json.dumps(i.db['total']) == json.dumps({})
|
assert json.dumps(i.db['total']) == json.dumps({})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "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_Temp": 0, "Rated_Power": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
@@ -391,40 +387,40 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==4
|
assert tests==3
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||||
|
|
||||||
|
|
||||||
def test_statistic_counter():
|
def test_statistic_counter():
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
val = i.dev_value("Test-String")
|
val = i.dev_value("Test-String")
|
||||||
assert val == "Test-String"
|
assert val == "Test-String"
|
||||||
|
|
||||||
val = i.dev_value(0xffffffff) # invalid addr
|
val = i.dev_value(0xffffffff) # invalid addr
|
||||||
assert val == None
|
assert val == None
|
||||||
|
|
||||||
val = i.dev_value(0xffffff00) # valid addr but not initiliazed
|
val = i.dev_value(Register.INVERTER_CNT) # valid addr but not initiliazed
|
||||||
assert val == None or val == 0
|
assert val == None or val == 0
|
||||||
|
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0}})
|
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}})
|
||||||
|
|
||||||
val = i.dev_value(0xffffff00) # valid and initiliazed addr
|
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
i.inc_counter('Inverter_Cnt')
|
i.inc_counter('Inverter_Cnt')
|
||||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0}})
|
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}})
|
||||||
val = i.dev_value(0xffffff00)
|
val = i.dev_value(Register.INVERTER_CNT)
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
i.dec_counter('Inverter_Cnt')
|
i.dec_counter('Inverter_Cnt')
|
||||||
val = i.dev_value(0xffffff00)
|
val = i.dev_value(Register.INVERTER_CNT)
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
def test_dep_rules():
|
def test_dep_rules():
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
res = i.ignore_this_device({})
|
res = i.ignore_this_device({})
|
||||||
@@ -434,90 +430,90 @@ def test_dep_rules():
|
|||||||
assert res == True
|
assert res == True
|
||||||
|
|
||||||
i.inc_counter('Inverter_Cnt') # is 1
|
i.inc_counter('Inverter_Cnt') # is 1
|
||||||
val = i.dev_value(0xffffff00)
|
val = i.dev_value(Register.INVERTER_CNT)
|
||||||
assert val == 1
|
assert val == 1
|
||||||
res = i.ignore_this_device({'reg':0xffffff00})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT})
|
||||||
assert res == True
|
assert res == True
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'less_eq': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2})
|
||||||
assert res == False
|
assert res == False
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'gte': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2})
|
||||||
assert res == True
|
assert res == True
|
||||||
|
|
||||||
i.inc_counter('Inverter_Cnt') # is 2
|
i.inc_counter('Inverter_Cnt') # is 2
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'less_eq': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2})
|
||||||
assert res == False
|
assert res == False
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'gte': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2})
|
||||||
assert res == False
|
assert res == False
|
||||||
|
|
||||||
i.inc_counter('Inverter_Cnt') # is 3
|
i.inc_counter('Inverter_Cnt') # is 3
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'less_eq': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2})
|
||||||
assert res == True
|
assert res == True
|
||||||
res = i.ignore_this_device({'reg':0xffffff00, 'gte': 2})
|
res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2})
|
||||||
assert res == False
|
assert res == False
|
||||||
|
|
||||||
def test_table_definition():
|
def test_table_definition():
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
val = i.dev_value(0xffffff04) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', singleton=False, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456', singleton=True, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
val = i.dev_value(0xffffff04) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
# test missing 'fmt' value
|
# test missing 'fmt' value
|
||||||
Infos._Infos__info_defs[0xfffffffe] = {'name':['proxy', 'Internal_Test1'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test1_'}}
|
i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test1'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test1_'}}
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456', singleton=True, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
if id == 'intern_test1_456':
|
if id == 'intern_test1_456':
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests == 1
|
assert tests == 1
|
||||||
|
|
||||||
val = i.dev_value(0xffffff04) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
# test missing 'dev' value
|
# test missing 'dev' value
|
||||||
Infos._Infos__info_defs[0xfffffffe] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}}
|
i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}}
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456', singleton=True, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
if id == 'intern_test2_456':
|
if id == 'intern_test2_456':
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests == 1
|
assert tests == 1
|
||||||
|
|
||||||
val = i.dev_value(0xffffff04) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 2
|
assert val == 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# test invalid 'via' value
|
# test invalid 'via' value
|
||||||
Infos._Infos__info_devs['test_dev'] = {'via':'xyz', 'name':'Module PV1'}
|
i.info_devs['test_dev'] = {'via':'xyz', 'name':'Module PV1'}
|
||||||
|
|
||||||
Infos._Infos__info_defs[0xfffffffe] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev':'test_dev', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}}
|
i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev':'test_dev', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}}
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456', singleton=True, sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
if id == 'intern_test2_456':
|
if id == 'intern_test2_456':
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests == 1
|
assert tests == 1
|
||||||
|
|
||||||
val = i.dev_value(0xffffff04) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 3
|
assert val == 3
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_data_type(InvalidDataSeq):
|
def test_invalid_data_type(InvalidDataSeq):
|
||||||
i = Infos()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
val = i.dev_value(0xffffff03) # check invalid data type counter
|
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -525,6 +521,6 @@ def test_invalid_data_type(InvalidDataSeq):
|
|||||||
pass
|
pass
|
||||||
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
||||||
|
|
||||||
val = i.dev_value(0xffffff03) # check invalid data type counter
|
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||||
assert val == 1
|
assert val == 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest, logging
|
import pytest, logging
|
||||||
from app.src.messages import Message, Control
|
from app.src.gen3.talent import Talent, Control
|
||||||
from app.src.config import Config
|
from app.src.config import Config
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Infos
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ Infos.static_init()
|
|||||||
|
|
||||||
tracer = logging.getLogger('tracer')
|
tracer = logging.getLogger('tracer')
|
||||||
|
|
||||||
class MemoryStream(Message):
|
class MemoryStream(Talent):
|
||||||
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||||
super().__init__(server_side)
|
super().__init__(server_side)
|
||||||
self.__msg = msg
|
self.__msg = msg
|
||||||
@@ -45,8 +45,8 @@ class MemoryStream(Message):
|
|||||||
def _timestamp(self):
|
def _timestamp(self):
|
||||||
return 1700260990000
|
return 1700260990000
|
||||||
|
|
||||||
def _Message__flush_recv_msg(self) -> None:
|
def _Talent__flush_recv_msg(self) -> None:
|
||||||
super()._Message__flush_recv_msg()
|
super()._Talent__flush_recv_msg()
|
||||||
self.msg_count += 1
|
self.msg_count += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -291,7 +291,9 @@ def test_read_two_messages(ConfigTsunAllowAll, Msg2ContactInfo,MsgContactResp,Ms
|
|||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
|
||||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
m._init_new_client_conn(b'solarhub', b'solarhub@123456')
|
m.contact_name = b'solarhub'
|
||||||
|
m.contact_mail = b'solarhub@123456'
|
||||||
|
m._init_new_client_conn()
|
||||||
assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub@123456'
|
assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub@123456'
|
||||||
|
|
||||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
@@ -309,7 +311,9 @@ def test_read_two_messages(ConfigTsunAllowAll, Msg2ContactInfo,MsgContactResp,Ms
|
|||||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||||
|
|
||||||
m._send_buffer = bytearray(0) # clear send buffer for next test
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
m._init_new_client_conn(b'solarhub', b'solarhub@123456')
|
m.contact_name = b'solarhub'
|
||||||
|
m.contact_mail = b'solarhub@123456'
|
||||||
|
m._init_new_client_conn()
|
||||||
assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000002\x91\x00\x08solarhub\x0fsolarhub@123456'
|
assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000002\x91\x00\x08solarhub\x0fsolarhub@123456'
|
||||||
m.close()
|
m.close()
|
||||||
|
|
||||||
@@ -700,14 +704,14 @@ def test_ctrl_byte():
|
|||||||
|
|
||||||
|
|
||||||
def test_msg_iterator():
|
def test_msg_iterator():
|
||||||
m1 = Message(server_side=True)
|
m1 = Talent(server_side=True)
|
||||||
m2 = Message(server_side=True)
|
m2 = Talent(server_side=True)
|
||||||
m3 = Message(server_side=True)
|
m3 = Talent(server_side=True)
|
||||||
m3.close()
|
m3.close()
|
||||||
del m3
|
del m3
|
||||||
test1 = 0
|
test1 = 0
|
||||||
test2 = 0
|
test2 = 0
|
||||||
for key in Message:
|
for key in Talent:
|
||||||
if key == m1:
|
if key == m1:
|
||||||
test1+=1
|
test1+=1
|
||||||
elif key == m2:
|
elif key == m2:
|
||||||
@@ -718,19 +722,19 @@ def test_msg_iterator():
|
|||||||
assert test2 == 1
|
assert test2 == 1
|
||||||
|
|
||||||
def test_proxy_counter():
|
def test_proxy_counter():
|
||||||
m = Message(server_side=True)
|
m = Talent(server_side=True)
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
m.db.stat['proxy']['Unknown_Msg'] = 0
|
m.db.stat['proxy']['Unknown_Msg'] = 0
|
||||||
m.new_stat_data['proxy'] = False
|
Infos.new_stat_data['proxy'] = False
|
||||||
|
|
||||||
m.inc_counter('Unknown_Msg')
|
m.inc_counter('Unknown_Msg')
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
assert m.new_stat_data == {'proxy': True}
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
assert 1 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
|
|
||||||
m.new_stat_data['proxy'] = False
|
Infos.new_stat_data['proxy'] = False
|
||||||
m.dec_counter('Unknown_Msg')
|
m.dec_counter('Unknown_Msg')
|
||||||
assert m.new_data == {}
|
assert m.new_data == {}
|
||||||
assert m.new_stat_data == {'proxy': True}
|
assert Infos.new_stat_data == {'proxy': True}
|
||||||
assert 0 == m.db.stat['proxy']['Unknown_Msg']
|
assert 0 == m.db.stat['proxy']['Unknown_Msg']
|
||||||
m.close()
|
m.close()
|
||||||
|
|||||||
781
app/tests/test_solarman.py
Normal file
781
app/tests/test_solarman.py
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
import pytest, json
|
||||||
|
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||||
|
from app.src.config import Config
|
||||||
|
from app.src.infos import Infos, Register
|
||||||
|
|
||||||
|
# initialize the proxy statistics
|
||||||
|
Infos.static_init()
|
||||||
|
|
||||||
|
class MemoryStream(SolarmanV5):
|
||||||
|
def __init__(self, msg, chunks = (0,), server_side: bool = True):
|
||||||
|
super().__init__(server_side)
|
||||||
|
self.__msg = msg
|
||||||
|
self.__msg_len = len(msg)
|
||||||
|
self.__chunks = chunks
|
||||||
|
self.__offs = 0
|
||||||
|
self.__chunk_idx = 0
|
||||||
|
self.msg_count = 0
|
||||||
|
self.addr = 'Test: SrvSide'
|
||||||
|
self.db.stat['proxy']['Invalid_Msg_Format'] = 0
|
||||||
|
self.db.stat['proxy']['AT_Command'] = 0
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
len = self.__chunks[self.__chunk_idx]
|
||||||
|
self.__chunk_idx += 1
|
||||||
|
if len!=0:
|
||||||
|
self._recv_buffer += self.__msg[self.__offs:len]
|
||||||
|
copied_bytes = len - self.__offs
|
||||||
|
self.__offs = len
|
||||||
|
else:
|
||||||
|
self._recv_buffer += self.__msg[self.__offs:]
|
||||||
|
copied_bytes = self.__msg_len - self.__offs
|
||||||
|
self.__offs = self.__msg_len
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return copied_bytes
|
||||||
|
|
||||||
|
def _timestamp(self):
|
||||||
|
return 1700260990000
|
||||||
|
|
||||||
|
def _SolarmanV5__flush_recv_msg(self) -> None:
|
||||||
|
super()._SolarmanV5__flush_recv_msg()
|
||||||
|
self.msg_count += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_sn() -> bytes:
|
||||||
|
return b'\x21\x43\x65\x7b'
|
||||||
|
|
||||||
|
def get_inv_no() -> bytes:
|
||||||
|
return b'T170000000000001'
|
||||||
|
|
||||||
|
def get_invalid_sn():
|
||||||
|
return b'R170000000000002'
|
||||||
|
|
||||||
|
def correct_checksum(buf):
|
||||||
|
checksum = sum(buf[1:]) & 0xff
|
||||||
|
return checksum.to_bytes(length=1)
|
||||||
|
|
||||||
|
def incorrect_checksum(buf):
|
||||||
|
checksum = (sum(buf[1:])+1) & 0xff
|
||||||
|
return checksum.to_bytes(length=1)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def DeviceIndMsg(): # 0x4110
|
||||||
|
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def DeviceRspMsg(): # 0x1110
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x11\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
|
||||||
|
msg += b'\x66\x78\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InvalidStartByte(): # 0x4110
|
||||||
|
msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InvalidStopByte(): # 0x4110
|
||||||
|
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x14'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InvalidChecksum(): # 0x4110
|
||||||
|
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += incorrect_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterIndMsg(): # 0x4210
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x02\x58\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
|
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||||
|
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||||
|
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||||
|
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterIndMsg1600(): # 0x4210 rated Power 1600W
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x06\x40\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x06\x40\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
|
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||||
|
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||||
|
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||||
|
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterIndMsg1800(): # 0x4210 rated Power 1800W
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x07\x08\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x07\x08\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
|
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||||
|
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||||
|
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||||
|
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterIndMsg2000(): # 0x4210 rated Power 2000W
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
|
||||||
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x07\xd0\x06\x7a'
|
||||||
|
msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd'
|
||||||
|
msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04'
|
||||||
|
msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
|
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||||
|
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||||
|
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||||
|
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def InverterRspMsg(): # 0x1210
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x12\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
|
||||||
|
msg += b'\x66\x78\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def UnknownMsg(): # 0x5110
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x51\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
|
||||||
|
msg += b'\x66\x78\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def HeartbeatIndMsg(): # 0x4710
|
||||||
|
msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn()
|
||||||
|
msg += b'\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def HeartbeatRspMsg(): # 0x1710
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x17\x10\x84' +get_sn() +b'\x00\x01\x22\x71\x09'
|
||||||
|
msg += b'\x66\x78\x00\x00\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def AtCommandIndMsg(): # 0x4510
|
||||||
|
msg = b'\xa5\x01\x00\x10\x45\x10\x84' +get_sn()
|
||||||
|
msg += b'\x00'
|
||||||
|
msg += correct_checksum(msg)
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ConfigTsunAllowAll():
|
||||||
|
Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ConfigNoTsunInv1():
|
||||||
|
Config.config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889,'node_id':'inv1','suggested_area':'roof'}}}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ConfigTsunInv1():
|
||||||
|
Config.config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889,'node_id':'inv1','suggested_area':'roof'}}}
|
||||||
|
|
||||||
|
def test_read_message(DeviceIndMsg):
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,))
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == None
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_invalid_start_byte(InvalidStartByte, DeviceIndMsg):
|
||||||
|
# received a message with wrong start byte plus an valid message
|
||||||
|
# the complete receive buffer must be cleared to
|
||||||
|
# find the next valid message
|
||||||
|
m = MemoryStream(InvalidStartByte, (0,))
|
||||||
|
m.append_msg(DeviceIndMsg)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since start byte is wrong
|
||||||
|
assert m.msg_count == 0
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_invalid_stop_byte(InvalidStopByte):
|
||||||
|
# received a message with wrong stop byte
|
||||||
|
# the complete receive buffer must be cleared to
|
||||||
|
# find the next valid message
|
||||||
|
m = MemoryStream(InvalidStopByte, (0,))
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since start byte is wrong
|
||||||
|
assert m.msg_count == 1 # msg flush was called
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_invalid_stop_byte2(InvalidStopByte, DeviceIndMsg):
|
||||||
|
# received a message with wrong stop byte plus an valid message
|
||||||
|
# only the first message must be discarded
|
||||||
|
m = MemoryStream(InvalidStopByte, (0,))
|
||||||
|
m.append_msg(DeviceIndMsg)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since start byte is wrong
|
||||||
|
assert m.msg_count == 1 # msg flush was called
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==DeviceIndMsg
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == None
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_invalid_stop_start_byte(InvalidStopByte, InvalidStartByte):
|
||||||
|
# received a message with wrong stop byte plus an invalid message
|
||||||
|
# with fron start byte
|
||||||
|
# the complete receive buffer must be cleared to
|
||||||
|
# find the next valid message
|
||||||
|
m = MemoryStream(InvalidStopByte, (0,))
|
||||||
|
m.append_msg(InvalidStartByte)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since start byte is wrong
|
||||||
|
assert m.msg_count == 1 # msg flush was called
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
|
||||||
|
# received a message with wrong checksum plus an valid message
|
||||||
|
# only the first message must be discarded
|
||||||
|
m = MemoryStream(InvalidChecksum, (0,))
|
||||||
|
m.append_msg(DeviceIndMsg)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since start byte is wrong
|
||||||
|
assert m.msg_count == 1 # msg flush was called
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==DeviceIndMsg
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == None
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg):
|
||||||
|
ConfigNoTsunInv1
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,))
|
||||||
|
m.append_msg(DeviceIndMsg)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m._forward_buffer==b''
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_read_message_in_chunks(DeviceIndMsg):
|
||||||
|
m = MemoryStream(DeviceIndMsg, (4,11,0))
|
||||||
|
m.read() # read 4 bytes, header incomplere
|
||||||
|
assert not m.header_valid # must be invalid, since header not complete
|
||||||
|
assert m.msg_count == 0
|
||||||
|
m.read() # read missing bytes for complete header
|
||||||
|
assert m.header_valid # must be valid, since header is complete but not the msg
|
||||||
|
assert m.msg_count == 0
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == 0 # should be None ?
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.read() # read rest of message
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(DeviceIndMsg, (4,10,0))
|
||||||
|
m.read() # read 4 bytes, header incomplere
|
||||||
|
assert not m.header_valid
|
||||||
|
assert m.msg_count == 0
|
||||||
|
m.read() # read 6 more bytes, header incomplere
|
||||||
|
assert not m.header_valid
|
||||||
|
assert m.msg_count == 0
|
||||||
|
m.read() # read rest of message
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
while m.read(): # read rest of message
|
||||||
|
pass
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, InverterIndMsg):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,))
|
||||||
|
m.append_msg(InverterIndMsg)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4110
|
||||||
|
assert m.serial == 0x0100
|
||||||
|
assert m.data_len == 0xd4
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m._forward_buffer==DeviceIndMsg
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
# assert m._send_buffer==MsgContactResp
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._recv_buffer==InverterIndMsg
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._forward_buffer = bytearray(0) # clear forward buffer for next test
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 2
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4210
|
||||||
|
assert m.serial == 0x9ee6
|
||||||
|
assert m.data_len == 0x199
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m._forward_buffer==InverterIndMsg
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_unkown_message(ConfigTsunInv1, UnknownMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(UnknownMsg, (0,))
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x5110
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x0a
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==UnknownMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_device_rsp(ConfigTsunInv1, DeviceRspMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(DeviceRspMsg, (0,), False)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x1110
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x0a
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==DeviceRspMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(InverterRspMsg, (0,), False)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x1210
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x0a
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==InverterRspMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_heartbeat_ind(ConfigTsunInv1, HeartbeatIndMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(HeartbeatIndMsg, (0,))
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
# assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4710
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x01
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==HeartbeatIndMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(HeartbeatRspMsg, (0,), False)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x1710
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x0a
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==HeartbeatRspMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_at_command_ind(ConfigTsunInv1, AtCommandIndMsg):
|
||||||
|
ConfigTsunInv1
|
||||||
|
m = MemoryStream(AtCommandIndMsg, (0,))
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||||
|
assert m.msg_count == 1
|
||||||
|
assert m.header_len==11
|
||||||
|
assert m.snr == 2070233889
|
||||||
|
# assert m.unique_id == '2070233889'
|
||||||
|
assert m.control == 0x4510
|
||||||
|
assert m.serial == 0x8410
|
||||||
|
assert m.data_len == 0x01
|
||||||
|
assert m._recv_buffer==b''
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
assert m._forward_buffer==AtCommandIndMsg
|
||||||
|
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||||
|
assert m.db.stat['proxy']['AT_Command'] == 1
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_600(ConfigTsunAllowAll, InverterIndMsg):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||||
|
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 600 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MS2000(600)' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
|
||||||
|
m._send_buffer = bytearray(0) # clear send buffer for next test
|
||||||
|
m._init_new_client_conn()
|
||||||
|
assert m._send_buffer==b''
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_1600(ConfigTsunAllowAll, InverterIndMsg1600):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg1600, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||||
|
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 1600 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 1600 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MS1600' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_1800(ConfigTsunAllowAll, InverterIndMsg1800):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg1800, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||||
|
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 1800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 1800 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MS1800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_build_modell_2000(ConfigTsunAllowAll, InverterIndMsg2000):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(InverterIndMsg2000, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert None == m.db.get_db_value(Register.RATED_POWER, None)
|
||||||
|
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
|
assert 2000 == m.db.get_db_value(Register.RATED_POWER, 0)
|
||||||
|
assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
|
||||||
|
ConfigTsunAllowAll
|
||||||
|
m = MemoryStream(DeviceIndMsg, (0,))
|
||||||
|
assert 0 == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0)
|
||||||
|
assert 'IGEN TECH' == m.db.get_db_value(Register.CHIP_TYPE, None)
|
||||||
|
assert None == m.db.get_db_value(Register.CHIP_MODEL, None)
|
||||||
|
m.read() # read complete msg, and dispatch msg
|
||||||
|
assert 'LSW5BLE_17_02B0_1.05' == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0).rstrip('\00')
|
||||||
|
assert 'LSW5BLE' == m.db.get_db_value(Register.CHIP_MODEL, 0)
|
||||||
|
m.close()
|
||||||
@@ -80,6 +80,7 @@ services:
|
|||||||
- $(DNS2:-4.4.4.4}
|
- $(DNS2:-4.4.4.4}
|
||||||
ports:
|
ports:
|
||||||
- 5005:5005
|
- 5005:5005
|
||||||
|
- 10000:10000
|
||||||
volumes:
|
volumes:
|
||||||
- ${PROJECT_DIR}./tsun-proxy/log:/home/tsun-proxy/log
|
- ${PROJECT_DIR}./tsun-proxy/log:/home/tsun-proxy/log
|
||||||
- ${PROJECT_DIR}./tsun-proxy/config:/home/tsun-proxy/config
|
- ${PROJECT_DIR}./tsun-proxy/config:/home/tsun-proxy/config
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ def ClientConnection():
|
|||||||
s.connect((host, port))
|
s.connect((host, port))
|
||||||
s.settimeout(1)
|
s.settimeout(1)
|
||||||
yield s
|
yield s
|
||||||
|
time.sleep(2.5)
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
def tempClientConnection():
|
def tempClientConnection():
|
||||||
|
|||||||
150
system_tests/test_tcp_socket_v2.py
Normal file
150
system_tests/test_tcp_socket_v2.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# test_with_pytest.py and scapy
|
||||||
|
#
|
||||||
|
import pytest, socket, time, os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
#from scapy.all import *
|
||||||
|
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
||||||
|
|
||||||
|
def get_sn() -> bytes:
|
||||||
|
return bytes.fromhex(SOLARMAN_SNR)
|
||||||
|
|
||||||
|
def get_inv_no() -> bytes:
|
||||||
|
return b'T170000000000001'
|
||||||
|
|
||||||
|
def get_invalid_sn():
|
||||||
|
return b'R170000000000002'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgContactInfo(): # Contact Info message
|
||||||
|
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
|
||||||
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f'
|
||||||
|
msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c'
|
||||||
|
msg += b'\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgContactResp(): # Contact Response message
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x11\x01\x01' +get_sn() +b'\x02\x01\x6a\xfd\x8f'
|
||||||
|
msg += b'\x65\x3c\x00\x00\x00\x75\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgDataInd():
|
||||||
|
msg = b'\xa5\x99\x01\x10\x42\x59\x84' +get_sn() +b'\x01\xb0\x02\x2c\x87'
|
||||||
|
msg += b'\x22\x32\xb7\x29\x00\x00\xd6\xcf\xe1\x33\x01\x00\x0c\x05\x00\x00'
|
||||||
|
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x01\x12\x02\x12\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x40\x10\x08\xd8\x00\x09\x13\x84\x00\x35\x00\x00\x02\x58\x00\xd8'
|
||||||
|
msg += b'\x01\x3f\x00\x17\x00\x4d\x01\x44\x00\x14\x00\x43\x01\x45\x00\x18'
|
||||||
|
msg += b'\x00\x52\x00\x12\x00\x01\x00\x00\x00\x7c\x00\x00\x24\xed\x00\x2c'
|
||||||
|
msg += b'\x00\x00\x0b\x10\x00\x26\x00\x00\x0a\x0f\x00\x30\x00\x00\x0b\x76'
|
||||||
|
msg += b'\x00\x00\x00\x00\x06\x16\x00\x00\x00\x00\x55\xaa\x00\x01\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00'
|
||||||
|
msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00'
|
||||||
|
msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41'
|
||||||
|
msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c'
|
||||||
|
msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05'
|
||||||
|
msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00'
|
||||||
|
msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00'
|
||||||
|
msg += b'\x00\x00\x00\x00\x24\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def MsgDataResp(): # Contact Response message
|
||||||
|
msg = b'\xa5\x0a\x00\x10\x12\x80\x84' +get_sn() +b'\x01\x01\xd1\x96\x04'
|
||||||
|
msg += b'\x66\x3c\x00\x00\x00\xed\x15'
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = 'iot.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
|
port = 10000
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.connect((host, port))
|
||||||
|
s.settimeout(1)
|
||||||
|
yield s
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
def checkResponse(data, Msg):
|
||||||
|
check = bytearray(data)
|
||||||
|
check[5]= Msg[5] # ignore seq
|
||||||
|
check[13:18]= Msg[13:18] # ignore timestamp + first byte of repeat time
|
||||||
|
check[21]= Msg[21] # ignore crc
|
||||||
|
assert check == Msg
|
||||||
|
|
||||||
|
|
||||||
|
def tempClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = 'iot.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
|
port = 10000
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.connect((host, port))
|
||||||
|
s.settimeout(1)
|
||||||
|
yield s
|
||||||
|
time.sleep(2.5)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
def test_open_close():
|
||||||
|
try:
|
||||||
|
for s in tempClientConnection():
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
assert False
|
||||||
|
assert True
|
||||||
|
|
||||||
|
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
|
||||||
|
s = ClientConnection
|
||||||
|
try:
|
||||||
|
s.sendall(MsgContactInfo)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
checkResponse(data, MsgContactResp)
|
||||||
|
|
||||||
|
def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
|
||||||
|
s = ClientConnection
|
||||||
|
try:
|
||||||
|
s.sendall(MsgDataInd)
|
||||||
|
# time.sleep(2.5)
|
||||||
|
data = s.recv(1024)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
# time.sleep(2.5)
|
||||||
|
checkResponse(data, MsgDataResp)
|
||||||
Reference in New Issue
Block a user