Compare commits

..

70 Commits

Author SHA1 Message Date
renovate[bot]
271d6709da Update dependency aiohttp to v3.11.16 2025-04-02 05:38:09 +00:00
renovate[bot]
1b5af7fa97 Update dependency pytest-cov to v6.1.0 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:17:42 +02:00
renovate[bot]
2731c68675 Update dependency aiohttp to v3.11.15 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:12:17 +02:00
renovate[bot]
a8f8eca06c Update dependency aiomqtt to v2.3.1 (#337)
* Update dependency aiomqtt to v2.3.1

* update aiomqtt badge

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:08:42 +02:00
renovate[bot]
f9eb4ad8d7 Update dependency coverage to v7.8.0 (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 22:54:40 +02:00
Stefan Allius
0e65e90c25 add DDZY422-D2 as not supported (#333)
* add DDZY422-D2 as not supported

* describe not supported devices clearer
2025-03-30 16:40:02 +02:00
renovate[bot]
18b2a2bfb2 Update dependency python-dotenv to v1.1.0 (#332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 01:13:17 +01:00
renovate[bot]
d1da8a85d3 Update dependency pytest-asyncio to v0.26.0 (#331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 01:05:22 +01:00
Stefan Allius
433faecbb5 update uml diagrams (#329)
* update uml diagrams

* pin versions to make test runs reproducible

* add install target for easier dev env setup
2025-03-30 00:44:27 +01:00
Stefan Allius
632498c384 S allius/issue327 (#328)
* fix typo

* add DCU-1000 storage systems/batteries

* fix compatiblity table

* concern ms3000 support
2025-03-26 23:24:56 +01:00
Stefan Allius
d9384a6118 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy 2025-03-26 19:43:34 +01:00
Stefan Allius
9ec111759a Merge pull request #326 from s-allius/dev-0.13
Dev 0.13
2025-03-26 19:40:42 +01:00
Stefan Allius
8d2dcb7212 S allius/issue320 (#324)
* at unit test for 0x4510 msg with frametype 5
2025-03-26 18:56:01 +01:00
Stefan Allius
32d7711ab7 S allius/issue321 (#325)
* support frame type no 8 for AT+ responses
2025-03-26 18:47:09 +01:00
Stefan Allius
dff8934b82 Dcu1000 (#312)
* set equipment model dor DCU1000 devices

* DCU1000: add temp sensor and mppt states

* DCU1000: add total generation

* add more DCU1000 registers for MODBUS polling

* improve names of batterie measurements

* add more diagnostic registers

* adapt unit tests

* move uml files into subfolder

* add sensors for batterie cell voltages

* swap On and Off for MPPT status
2025-03-25 20:10:10 +01:00
Stefan Allius
3eb6a24dcb Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-03-23 23:53:22 +01:00
Stefan Allius
da383c7794 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy 2025-03-23 23:50:33 +01:00
renovate[bot]
f9be171865 Update ghcr.io/hassio-addons/base Docker tag to v17.2.2 (#315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-23 23:43:25 +01:00
Stefan Allius
45abc69ffb fix Add-on build errors
- bump python to version 3.12.9-r0
- fix workspace path for VSCode
2025-03-23 23:38:12 +01:00
Stefan Allius
96c35ed263 bump python to version 3.12.9-r0 2025-03-23 23:31:46 +01:00
Stefan Allius
795a52e172 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-03-17 22:34:31 +01:00
renovate[bot]
5d1ee60baf Update dependency aiohttp to v3.11.14 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 22:29:14 +01:00
Stefan Allius
7cf9e98c7f Merge branch 'renovate/python-3.x' of https://github.com/s-allius/tsun-gen3-proxy into renovate/python-3.x 2025-03-16 19:34:34 +01:00
Stefan Allius
e0777dca8e Add support for MS-3000 inverter (#299)
* split register map into multiple maps

* add base support reg mapping 0x01900000

* fix shadowed builtin

* detect reg mapping for sensor automatically

* add device and test regs for MS-3000

* add more register mappings

* fix unit tests

* add more MS-3000 registers

* build modell string for TSUN MS-3000

* add MS3000 unit test

* remove obsolete method __set_config_parms

* fix start addr of modbus scans

- in server mode the start addr must be reduced
  by mb_step

* add tests for sensor_list of ms-3000 inverters

* MS-3000: add integer test register

* DCU-1000: add Out Status register

* add integer test and batterie out register

* fix Sonar Qube finding

* DCU-1000: add temp sensors
2025-03-16 18:49:01 +01:00
Stefan Allius
955657fd87 add first costumer apparmor definition (#296)
* add first costumer apparmor definition

* add initial apparmor support
2025-03-16 13:11:03 +01:00
Stefan Allius
ecd21e46fb add modbus scanner config for HA Add-ons 2025-03-15 17:16:54 +01:00
Stefan Allius
3489e8997d fix MQTT paket transmitting (#309) 2025-03-15 13:52:49 +01:00
Stefan Allius
88cb01f613 add Modbus polling mode for DCU1000 (#305)
* add Modbus scanning mode

* fix modbus polling for DCU 1000

* add modbus register for DCU 1000

* calculate meta values from modbus regs

* update changelog

* reduce code duplication

* refactor modbus_scan

* add additional unit tests
2025-03-11 19:47:37 +01:00
Stefan Allius
be60f9ea1e calculate power values for DCU (#303)
* calculate power values for DCU

* refactor code
2025-03-02 21:09:03 +01:00
Stefan Allius
10b4a84701 allow R47serial numbers for GEN3 inverters (#302) 2025-03-02 19:05:36 +01:00
Stefan Allius
06ceb02f0d ignore apparmor.txt 2025-02-27 22:50:24 +01:00
Stefan Allius
8a2ca3ab9a fix the build target 2025-02-27 22:43:07 +01:00
Stefan Allius
3f3ed1b14f add watchdog for Add-ons (#291) 2025-02-27 16:11:32 +01:00
Stefan Allius
036dd6d1dc S allius/issue281 (#282)
* accept DCU serial number starting with '410'

* determine sensor-list by serial number

* adapt unit test for DCU support

* send first batterie measurements to home assistant

* add test case for sensor-list==3036

* add more registers for batteries

* improve error logging (Monitoring SN)

* update the add-on repro only for one stage

* add configuration for energie storages

* add License and Readme file to the add-on

* addon: add date and time to dev and debug docker container tag

* disable duplicate code check for config.py

* cleanup unit test, remove trailing whitespaces

* update changelog

* fix example config for batteries

* cleanup config.jinja template

* fix comments

* improve help texts
2025-02-24 22:39:34 +01:00
Stefan Allius
1f0ac97368 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-24 21:38:18 +01:00
renovate[bot]
5faf242d6c Update dependency aiohttp to v3.11.13 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 21:36:42 +01:00
Stefan Allius
ec3af69e62 S allius/issue288 (#289)
* remove apostrophes from fmt strings

- thanks to @onkelenno for the suggestion

* improve the logger initializing

- don't overwrite the logging.ini settings if the
env variable LOG_LVL isn't well defined
- Thanks to @onkelenno for the idea to improve

* set default argument for LOG_LVL to INFO in docker files

* adapt unit test
2025-02-23 14:17:57 +01:00
Stefan Allius
113a41ebfe Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-23 11:39:10 +01:00
renovate[bot]
13e6adc5c0 Update ghcr.io/hassio-addons/base Docker tag to v17.2.1 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-23 11:22:36 +01:00
Stefan Allius
f9256099c7 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-19 23:33:11 +01:00
renovate[bot]
204bc76153 Update SonarSource/sonarqube-scan-action action to v5 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 23:12:48 +01:00
Stefan Allius
58c7f51266 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-15 00:22:33 +01:00
renovate[bot]
1eaabb97a2 Update dependency aiocron to v2 (#284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 00:21:43 +01:00
Stefan Allius
7a6e6f73a5 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-14 21:55:57 +01:00
renovate[bot]
39495d3e9e Update ghcr.io/hassio-addons/base Docker tag to v17.1.5 (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 21:48:04 +01:00
Stefan Allius
a257f09d4c add ghcr logout for the clean target 2025-02-11 20:45:54 +01:00
Stefan Allius
5f0a35d55b Update AddOn base docker image to version 17.1.3 and python3 to 3.12.9-r0 2025-02-11 20:45:01 +01:00
Stefan Allius
4df36e2672 revert AddOn base docker image to version 17.1.0 2025-02-11 20:20:48 +01:00
Stefan Allius
48a9696df2 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-11 20:15:45 +01:00
renovate[bot]
24567eaf5f Update ghcr.io/hassio-addons/base Docker tag to v17.1.3 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 20:12:49 +01:00
Stefan Allius
42fe33bacf add initial DCU support 2025-02-11 00:08:57 +01:00
Stefan Allius
cfdd65606d Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-02-10 20:30:42 +01:00
renovate[bot]
2e3ed8f162 Update python Docker tag to v3.13.2 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 20:28:03 +01:00
renovate[bot]
66a875c291 Update dependency aiohttp to v3.11.12 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 20:25:07 +01:00
renovate[bot]
46043e7754 Update ghcr.io/hassio-addons/base Docker tag to v17.1.2 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 20:21:37 +01:00
Stefan Allius
01ad8eff6b Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2025-01-16 19:37:22 +01:00
Stefan Allius
53c76e72a2 Dev 0.12 (#275)
* bump version to 0.12.1

* add initial version for release candidates

* add rc version

* version 0.12.1

* addon: bump base image version to v17.1.0

* 270 ha addon add syntax check to config parameters (#274)

* fixed requirement status of client mode host

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

---------

Co-authored-by: metzi <147942647+mime24@users.noreply.github.com>
Co-authored-by: Michael Metz <michael.metz@siemens.com>
2025-01-16 19:31:30 +01:00
renovate[bot]
24b092b69e Update ghcr.io/hassio-addons/base Docker tag to v17.1.0 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 17:46:05 +01:00
metzi
cf1563dd55 270 ha addon add syntax check to config parameters (#271)
* quotation marks removed from monitor_sn

* validation for serial, ports and client_mode_host

* removed TODO:

* allow only serial with 16 digit and starting with R17, Y17, Y47

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>
2025-01-13 19:49:12 +01:00
renovate[bot]
962f6ee5fb Update ghcr.io/hassio-addons/base Docker tag to v17.0.2 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-04 21:12:39 +01:00
Stefan Allius
9e60ad4bcd Dev 0.12 (#266)
* add ha_addons repository to cscode workspace

* Issue220 ha addon dokumentation update (#232)

* initial DOCS.md for Addon

* links to Mosquitto and Adguard

* replaced _ by . for PV-Strings

* mentioned add-on installation method in README.md

* fix most of the markdown linter warnings

* add missing alt texts

* added nice add repository to my Home Assistant badges

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>

* S allius/issue216 (#235)


* improve docker run

- establish multistage Dockerfile
- build a python wheel for all needed packages
- remove unneeded tools like apk for runtime

* pin versions, fix hadolint warnings

* merge from dev-0.12

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* Issue220 ha addon dokumentation update (#245)

* revised config disclaimer

* add newline at end of file to fix linter warning

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* 238 ha addon repository check (#244)

* move Makefile and bake file into parent folder

* build config.yaml from template

* use Makefile instead of build shell script

* ignore temporary or created files

* add rules for building the add-on repository

* add rel version of add-on

* add  jinja2-cli

* ignore inverter replays which a older than 1 day (#246)

* S allius/issue7 (#248)

* report alarm and fault bitfield to ha

* define the alarm and fault names

* configure log path and max number of daily log files (#243)

* configure log path and max number of daily log files

* don't use a subfolder for configs

* use make instead of a build script

* mount /homeassistant/tsun-proxy

* Add venv to base image

* give write access to mounted folder

* intial checkin, ignore SC1091

* set advanced and stage value in config.yaml

* fix typo

* added watchdog and removed Port 8127 from mapping

* fixed typo and use new add-on repro

- change the install button to install from
 https://github.com/s-allius/ha-addons

* add addon-rel target

* disable watchdog due to exceptions in the ha supervisor

* update changelog

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* Update README.md (#251)

install `https://github.com/s-allius/ha-addons` as repro for our add-on

* add german language file (#253)

* fix return type get_extra_info in FakeWriter

* move global startup code into main methdod

* pin version of base image

* avoid forwarding to a private (lokal) IP addr (#256)

* avoid forwarding to a private (lokal) IP addr

* test DNS resolver issues

* increase test coverage

* update changelog

* fix client_mode configuration block (#252)

* fix client_mode block

* add client mode

* fix tests with client_mode values

* log client_mode configuration

* add forward flag for client_mode

* improve startup logging

* added client_mode example

* adjusted translation files

* AT commands added

* typo

* missing "PLUS"

* link to config details

* improve log msg for config problems

* improve log msg on config errors

* improve log msg for config problems

* copy CHANGELOG.md into add-on repro

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* rename "ConfigErr" to match naming convention

* disable test coverage for __main__

* update changelog version 0.12

* Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy

* copy the run.sh scripts into the add-on repros

* set image path using jinja template

* fix wiki pathss

---------

Co-authored-by: metzi <147942647+mime24@users.noreply.github.com>
Co-authored-by: Michael Metz <michael.metz@siemens.com>
2024-12-24 14:20:12 +01:00
Stefan Allius
f5d760e2f0 Change wiki paths 2024-12-24 14:14:56 +01:00
Stefan Allius
3234e87b55 S allius/issue180 (#265)
* move default_config.toml into src/cnf/.

* improve file handling

* remove obsolete rules
2024-12-24 00:13:32 +01:00
Stefan Allius
412013f626 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2024-12-23 19:17:40 +01:00
renovate[bot]
1781dba065 Update dependency aiohttp to v3.11.11 (#264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 19:17:03 +01:00
Stefan Allius
1b3833989e Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.13 2024-12-23 14:03:30 +01:00
Stefan Allius
26ca006853 Dev 0.12 (#262) 2024-12-23 14:01:27 +01:00
Stefan Allius
1e160f3b0f set verion 0.13 2024-12-23 00:10:57 +01:00
Stefan Allius
338b86964d Dev 0.12 (#260)
- fix build add-on version for releases
2024-12-23 00:02:40 +01:00
Stefan Allius
35952654db Dev 0.12 (#259)
* add ha_addons repository to cscode workspace

* Issue220 ha addon dokumentation update (#232)

* initial DOCS.md for Addon

* links to Mosquitto and Adguard

* replaced _ by . for PV-Strings

* mentioned add-on installation method in README.md

* fix most of the markdown linter warnings

* add missing alt texts

* added nice add repository to my Home Assistant badges

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>

* S allius/issue216 (#235)


* improve docker run

- establish multistage Dockerfile
- build a python wheel for all needed packages
- remove unneeded tools like apk for runtime

* pin versions, fix hadolint warnings

* merge from dev-0.12

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* Issue220 ha addon dokumentation update (#245)

* revised config disclaimer

* add newline at end of file to fix linter warning

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* 238 ha addon repository check (#244)

* move Makefile and bake file into parent folder

* build config.yaml from template

* use Makefile instead of build shell script

* ignore temporary or created files

* add rules for building the add-on repository

* add rel version of add-on

* add  jinja2-cli

* ignore inverter replays which a older than 1 day (#246)

* S allius/issue7 (#248)

* report alarm and fault bitfield to ha

* define the alarm and fault names

* configure log path and max number of daily log files (#243)

* configure log path and max number of daily log files

* don't use a subfolder for configs

* use make instead of a build script

* mount /homeassistant/tsun-proxy

* Add venv to base image

* give write access to mounted folder

* intial checkin, ignore SC1091

* set advanced and stage value in config.yaml

* fix typo

* added watchdog and removed Port 8127 from mapping

* fixed typo and use new add-on repro

- change the install button to install from
 https://github.com/s-allius/ha-addons

* add addon-rel target

* disable watchdog due to exceptions in the ha supervisor

* update changelog

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* Update README.md (#251)

install `https://github.com/s-allius/ha-addons` as repro for our add-on

* add german language file (#253)

* fix return type get_extra_info in FakeWriter

* move global startup code into main methdod

* pin version of base image

* avoid forwarding to a private (lokal) IP addr (#256)

* avoid forwarding to a private (lokal) IP addr

* test DNS resolver issues

* increase test coverage

* update changelog

* fix client_mode configuration block (#252)

* fix client_mode block

* add client mode

* fix tests with client_mode values

* log client_mode configuration

* add forward flag for client_mode

* improve startup logging

* added client_mode example

* adjusted translation files

* AT commands added

* typo

* missing "PLUS"

* link to config details

* improve log msg for config problems

* improve log msg on config errors

* improve log msg for config problems

* copy CHANGELOG.md into add-on repro

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>

* rename "ConfigErr" to match naming convention

* disable test coverage for __main__

* update changelog version 0.12

---------

Co-authored-by: metzi <147942647+mime24@users.noreply.github.com>
Co-authored-by: Michael Metz <michael.metz@siemens.com>
2024-12-22 22:46:37 +01:00
58 changed files with 3152 additions and 819 deletions

View File

@@ -37,10 +37,10 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up Python 3.12
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@@ -58,7 +58,7 @@ jobs:
coverage report
- name: Analyze with SonarCloud
if: ${{ env.SONAR_TOKEN != 0 }}
uses: SonarSource/sonarqube-scan-action@v4
uses: SonarSource/sonarqube-scan-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -1 +1 @@
3.13.1
3.13.2

View File

@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]
- update dependency python to 3.13
- add initial support for TSUN MS-3000
- add initial apparmor support [#293](https://github.com/s-allius/tsun-gen3-proxy/issues/293)
- add Modbus polling mode for DCU1000 [#292](https://github.com/s-allius/tsun-gen3-proxy/issues/292)
- add Modbus scanning mode
- allow `R47`serial numbers for GEN3 inverters
- add watchdog for Add-ons
- add first costumer apparmor definition
- Respect logging.ini file, if LOG_ENV isn't set well [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288)
- Remove trailing apostrophe in the log output [#288](https://github.com/s-allius/tsun-gen3-proxy/issues/288)
- update AddOn base docker image to version 17.2.1
- addon: add date and time to dev container version
- Update AddOn python3 to 3.12.9-r0
- add initial DCU support
- update aiohttp to version 3.11.12
- fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180)
## [0.12.1] - 2025-01-13
- addon: bump base image version to v17.1.0
- addon: add syntax check to config parameters
- addon: bump base image version to v17.0.2
## [0.12.0] - 2024-12-22
- add hadolint configuration

View File

@@ -1,4 +1,4 @@
.PHONY: build clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel
.PHONY: build clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install
debug dev preview rc rel:
$(MAKE) -C app $@
@@ -12,3 +12,7 @@ addon-dev addon-debug addon-rc addon-rel:
check-docker-compose:
docker-compose config -q
install:
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -r requirements-test.txt

123
README.md
View File

@@ -1,13 +1,14 @@
<h1 align="center">TSUN-Gen3-Proxy</h1>
<p align="center">A proxy for</p>
<h3 align="center">TSUN Gen 3 Micro-Inverters</h3>
<h3 align="center">and Batteries</h3>
<p align="center">for easy</p>
<h3 align="center">MQTT/Home-Assistant</h3>
<p align="center">integration</p>
<p align="center">
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
<a href="https://www.python.org/downloads/release/python-3120/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.12-blue.svg"></a>
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.0-lightblue.svg"></a>
<a href="https://www.python.org/downloads/release/python-3130/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.13-blue.svg"></a>
<a href="https://aiomqtt.bo3hm.com/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.1-lightblue.svg"></a>
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
<br>
@@ -20,11 +21,11 @@
# Overview
This proxy enables a reliable connection between TSUN third generation inverters and an MQTT broker. With the proxy, you can easily retrieve real-time values such as power, current and daily energy and integrate the inverter into typical home automations. This works even without an internet connection. The optional connection to the TSUN Cloud can be disabled!
This proxy enables a reliable connection between TSUN third generation devices and an MQTT broker. With the proxy, you can easily retrieve real-time values such as power, current and daily energy from inverters and energy storage systems and integrate them into typical home automations. This works even without an internet connection. The optional connection to the TSUN Cloud can be disabled!
In detail, the inverter establishes a TCP connection to the TSUN cloud to transmit current measured values every 300 seconds. To be able to forward the measurement data to an MQTT broker, the proxy must be looped into this TCP connection.
In detail, the device establishes a TCP connection to the TSUN cloud to transmit current measured values every 300 seconds. To be able to forward the measurement data to an MQTT broker, the proxy must be looped into this TCP connection.
Through this, the inverter then establishes a connection to the proxy and the proxy establishes another connection to the TSUN Cloud. The transmitted data is interpreted by the proxy and then passed on to both the TSUN Cloud and the MQTT broker. The connection to the TSUN Cloud is optional and can be switched off in the configuration (default is on). Then no more data is sent to the Internet, but no more remote updates of firmware and operating parameters (e.g. rated power, grid parameters) are possible.
Through this, the device then establishes a connection to the proxy and the proxy establishes another connection to the TSUN Cloud. The transmitted data is interpreted by the proxy and then passed on to both the TSUN Cloud and the MQTT broker. The connection to the TSUN Cloud is optional and can be switched off in the configuration (default is on). Then no more data is sent to the Internet, but no more remote updates of firmware and operating parameters (e.g. rated power, grid parameters) are possible.
By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily.
@@ -32,12 +33,12 @@ Alternatively you can run the TSUN-Proxy as a Home Assistant Add-on. The install
Follow the Instructions mentioned in the add-on subdirectory `ha_addons`.
<br>
This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN.
This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters and storage systems with an MQTT broker. There is no support and no warranty from TSUN.
<br><br>
```txt
❗An essential requirement is that the proxy can be looped into the connection
between the inverter and TSUN Cloud.
between the device and TSUN Cloud.
There are various ways to do this, for example via an DNS host entry or via firewall
rules (iptables) in your router. However, depending on the circumstances, not all
@@ -49,7 +50,8 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole.
## Features
- Supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600
- Supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300
- Supports TSUN GEN3 PLUS batteries: TSOL-DC1000 (from version 0.13)
- Supports TSUN GEN3 inverters: TSOL-MS3000, MS800, MS700, MS600, MS400, MS350 and MS300
- `MQTT` support
- `Home-Assistant` auto-discovery support
- `MODBUS` support via MQTT topics
@@ -68,15 +70,15 @@ Here are some screenshots of how the inverter is displayed in the Home Assistant
## Requirements
### for Docker Installation
### Requirements for Docker Installation
- A running Docker engine to host the container
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
- Ability to loop the proxy into the connection between the device and the TSUN cloud
### for Home Assistant Add-on Installation
### Requirements for Home Assistant Add-on Installation
- Running Home Assistant on Home Assistant OS or Supervised. Container and Core installations doesn't support add-ons.
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
- Ability to loop the proxy into the connection between the device and the TSUN cloud
# Getting Started
@@ -117,19 +119,20 @@ docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v
# Configuration
```txt
❗The following description applies to the Docker installation. When installing the Home Assistant add-on, the
configuration is carried out via the Home Assistant UI. Some of the options described below are not required for
this. Additionally, creating a config.toml file is not necessary. However, for a general understanding of the
configuration and functionality, it is helpful to read the following description.
❗The following description applies to the Docker installation. When installing the Home
Assistant add-on, the configuration is carried out via the Home Assistant UI. Some of the
options described below are not required for this. Additionally, creating a config.toml
file is not necessary. However, for a general understanding of the configuration and
functionality, it is helpful to read the following description.
```
The configuration consists of several parts. First, the container and the proxy itself must be configured, and then the connection of the inverter to the proxy must be set up, which is done differently depending on the inverter generation
The configuration consists of several parts. First, the container and the proxy itself must be configured, and then the connection of the device to the proxy must be set up, which is done differently depending on the device generation
For GEN3PLUS inverters, this can be done easily via the web interface of the inverter. The GEN3 inverters do not have a web interface, so the proxy is integrated via a modified DNS resolution.
For GEN3PLUS devices, this can be done easily via the web interface of the devices. The GEN3 inverters do not have a web interface, so the proxy is integrated via a modified DNS resolution.
1. [Container Setup](#container-setup)
2. [Proxy Configuration](#proxy-configuration)
3. [Inverter Configuration](#inverter-configuration) (only GEN3PLUS)
3. [Inverter and Batterie Configuration](#inverter-and-batterie-configuration) (only GEN3PLUS)
4. [DNS Settings](#dns-settings) (Mandatory for GEN3)
## Container Setup
@@ -138,7 +141,7 @@ No special configuration is required for the Docker container if it is built and
On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files.
A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#docker-compose-environment-variables).
A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#docker-compose-environment-variables).
## Proxy Configuration
@@ -168,7 +171,7 @@ You find more details here: <https://toml.io/en/v1.0.0>
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
###
### Here you will find a description of all configuration options:
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml
###
### The configration uses the TOML format, which aims to be easy to read due to
### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
@@ -184,7 +187,7 @@ You find more details here: <https://toml.io/en/v1.0.0>
## required credentials. As the proxy does not currently support an encrypted connection
## to the MQTT broker, it is strongly recommended that you do not use a public broker.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account
##
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
@@ -201,7 +204,7 @@ mqtt.passwd = ''
## values match the HA default configuration. If you need to change these or want to use
## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant
##
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
@@ -219,7 +222,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox
## inverters. This connection is only required if you want send data to the TSUN cloud
## to use the TSUN APPs or receive firmware updates.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only
##
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
@@ -235,7 +238,7 @@ tsun.port = 5005
## inverters. This connection is only required if you want send data to the TSUN cloud
## to use the TSUN APPs or receive firmware updates.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#solarman-cloud-for-gen3plus-inverter-only
##
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
solarman.host = 'iot.talent-monitoring.com'
@@ -309,6 +312,33 @@ pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
##
## For each GEN3PLUS energy storage system, the serial number must be mapped to an MQTT
## definition. To do this, the corresponding configuration block is started with
## `[batteries.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## to this energy storage system. Further device-specific parameters (e.g. polling mode,
## client mode) can be set in the configuration block
##
## The serial numbers of all GEN3PLUS energy storage systems/batteries start with `410`!
## Each GEN3PLUS device is supplied with a “Monitoring SN:”. This can be found on a
## sticker enclosed with the inverter.
##
[batteries."4100000000000001"]
monitor_sn = 3000000000 # The GEN3PLUS "Monitoring SN:"
node_id = 'bat_1' # MQTT replacement for devices serial number
suggested_area = ''garage' # suggested installation place for home-assistant
modbus_polling = true # Enable optional MODBUS polling
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
# the next line and configure the fixed IP of your inverter
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
###
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via
@@ -330,19 +360,19 @@ mqtt.block = []
</details>
## Inverter Configuration
## Inverter and Batterie Configuration
GEN3PLUS inverters offer a web interface that can be used to configure the inverter. This is very practical for sending the data directly to the proxy. On the one hand, the inverter broadcasts its own SSID on 2.4GHz. This can be recognized because it is broadcast with `AP_<Montoring SN>`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the inverter.
GEN3PLUS devices (inverter, batteries, ...) offer a web interface that can be used to configure it. This is very practical for sending the data directly to the proxy. On the one hand, the device broadcasts its own SSID on 2.4GHz. This can be recognized because it is broadcast with `AP_<Montoring SN>`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the device.
If you have already connected the inverter to the cloud via the TSUN app, you can also address the inverter directly via WiFi. In the first case, the inverter uses the fixed IP address `10.10.100.254`, in the second case you have to look up the IP address in your router.
If you have already connected the device to the cloud via the TSUN app, you can also address the device directly via WiFi. In the first case, the device uses the fixed IP address `10.10.100.254`, in the second case you have to look up the IP address in your router.
The standard web interface of the inverter can be accessed at `http://<ip-adress>/index_cn.html`. Here you can set up the WLAN connection or change the password. The default user and password is `admin`/`admin`.
The standard web interface of the device can be accessed at `http://<ip-adress>/index_cn.html`. Here you can set up the WLAN connection or change the password. The default user and password is `admin`/`admin`.
For our purpose, the hidden URL `http://<ip-adress>/config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port `10000` for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems.
```txt
❗If the IP port is set to 10443 in the inverter configuration, you probably have a firmware with SSL support.
In this case, you MUST NOT change the port or the host address, as this may cause the inverter to hang and
❗If the IP port is set to 10443 in the device configuration, you probably have a firmware with SSL support.
In this case, you MUST NOT change the port or the host address, as this may cause the device to hang and
require a complete reset. Use the configuration in client mode instead.
```
@@ -350,23 +380,23 @@ If access to the web interface does not work, it can also be redirected via DNS
## Client Mode (GEN3PLUS only)
Newer GEN3PLUS inverters support SSL encrypted connections over port 10443 to the TSUN cloud. In this case you can't loop the proxy into this connection, since the certicate verification of the inverter don't allow this. You can configure the proxy in client-mode to establish an unencrypted connection to the inverter. For this porpuse the inverter listen on port `8899`.
Newer GEN3PLUS inverters, batteries and smart meter support SSL encrypted connections over port 10443 to the TSUN cloud. In this case you can't loop the proxy into this connection, since the certicate verification of the device don't allow this. You can configure the proxy in client-mode to establish an unencrypted connection to the inverter. For this porpuse the device listen on port `8899`.
There are some requirements to be met:
- the inverter should have a fixed IP
- the proxy must be able to reach the inverter. You must configure a corresponding route in your router if the inverter and the proxy are in different IP networks
- add a 'client_mode' line to your config.toml file, to specify the inverter's ip address
- the device should have a fixed IP
- the proxy must be able to reach the device. You must configure a corresponding route in your router if the device and the proxy are in different IP networks
- add a 'client_mode' line to your config.toml file, to specify the device's ip address
## DNS Settings
### 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 device and the TSUN Cloud, you must adapt the DNS record of *logger.talent-monitoring.com* within the network that your deivce 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.
The new GEN3 PLUS devices 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. User of the Home Assistant Add-on should use the AdGuard Add-on for this.
### DNS Rebind Protection
@@ -382,7 +412,7 @@ As described above, set a DNS sever in the Docker command or Docker compose file
### Over The Air (OTA) firmware update
Even if the proxy is connected between the inverter and the TSUN Cloud, an OTA update is supported. To do this, the inverter must be able to reach the website <http://www.talent-monitoring.com:9002/> in order to download images from there.
Even if the proxy is connected between the device and the TSUN Cloud, an OTA update is supported. To do this, the device must be able to reach the website <http://www.talent-monitoring.com:9002/> in order to download images from there.
It must be ensured that this address is not mapped to the proxy!
@@ -395,21 +425,24 @@ A combination with a red question mark should work, but I have not checked it in
<table align="center">
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 4.0.10</th><th align="center">Fw. 4.0.20</th></tr>
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr>
<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250, MS3000</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 micro inverters (quad MPPT):<br>MS3000</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D, MS800</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr>
<tr><td>GEN3 PLUS storage systems:<br>DC1000</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr>
<tr><td>GEN3 PLUS smart meter:<br>TSOL-MG3-MS, DDZY422-D2</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">❓</td><td align="center">❓</td></tr>
</<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td></tr>
</table>
```txt
Legend
: Firmware not available for this devices
✔️: proxy support testet
❓: proxy support possible but not testet
✔️: Proxy support testet
❓: Proxy support unknown. There is an open port, but all known protocols do not work.
🚧: Proxy support in preparation
```
The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` or `Y47E` instead of `R17E`
❗GEN3 Plus generation devices (e.g. MS-2000, DC-1000) can be recognized by their serial number. This starts with 'Y17' or 'Y47' for inverters and '410' for the DC-1000 battery storage system. In contrast, the serial number of GEN3 inverters begins with 'R17' or 'R47'.
If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check)

View File

@@ -1 +1 @@
0.12.0
0.13.0

View File

@@ -30,7 +30,7 @@ ARG SERVICE_NAME
ARG VERSION
ARG UID
ARG GID
ARG LOG_LVL
ARG LOG_LVL=INFO
ARG environment
ENV SERVICE_NAME=$SERVICE_NAME
@@ -59,7 +59,6 @@ RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* &&
# copy the content of the local src and config directory to the working directory
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY config .
COPY src .
RUN echo ${VERSION} > /proxy-version.txt \
&& date > /build-date.txt

View File

@@ -7,23 +7,6 @@ IMAGE = tsun-gen3-proxy
# Folders
SRC=.
SRC_PROXY=$(SRC)/src
CNF_PROXY=$(SRC)/config
DST=rootfs
DST_PROXY=$(DST)/home/proxy
# collect source files
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
$(wildcard $(SRC_PROXY)/*.ini)\
$(wildcard $(SRC_PROXY)/cnf/*.py)\
$(wildcard $(SRC_PROXY)/gen3/*.py)\
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
# determine destination files
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
export BUILD_DATE := ${shell date -Iminutes}
VERSION := $(shell cat $(SRC)/.version)
@@ -48,20 +31,4 @@ preview rc rel:
docker buildx bake -f docker-bake.hcl $@
.PHONY: debug dev preview rc rel
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
@echo Copy $< to $@
@mkdir -p $(@D)
@cp $< $@
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
@echo Copy $< to $@
@mkdir -p $(@D)
@cp $< $@
$(DST)/requirements.txt : $(SRC)/requirements.txt
@echo Copy $< to $@
@cp $< $@

263
app/docu/proxy.svg Normal file
View File

@@ -0,0 +1,263 @@
<?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="634pt" height="966pt"
viewBox="0.00 0.00 634.00 966.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 962)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-962 630,-962 630,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="200.1964,-934 91.8036,-934 91.8036,-898 206.1964,-898 206.1964,-928 200.1964,-934"/>
<polyline fill="none" stroke="#000000" points="200.1964,-934 200.1964,-928 "/>
<polyline fill="none" stroke="#000000" points="206.1964,-928 200.1964,-928 "/>
<text text-anchor="middle" x="149" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
<text text-anchor="middle" x="149" y="-907" 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="224,-926 224,-958 340,-958 340,-926 224,-926"/>
<text text-anchor="start" x="233.649" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AbstractIterMeta&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="224,-906 224,-926 340,-926 340,-906 224,-906"/>
<polygon fill="none" stroke="#000000" points="224,-874 224,-906 340,-906 340,-874 224,-874"/>
<text text-anchor="start" x="260.61" y="-887" 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="187,-726 187,-758 378,-758 378,-726 187,-726"/>
<text text-anchor="start" x="248.5965" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;InverterIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="187,-706 187,-726 378,-726 378,-706 187,-706"/>
<polygon fill="none" stroke="#000000" points="187,-650 187,-706 378,-706 378,-650 187,-650"/>
<text text-anchor="start" x="249.022" y="-687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()&#45;&gt;bool</text>
<text text-anchor="start" x="196.7835" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;disc(shutdown_started=False)</text>
<text text-anchor="start" x="228.044" y="-663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;create_remote()</text>
</g>
<!-- A1&#45;&gt;A4 -->
<g id="edge1" class="edge">
<title>A1&#45;&gt;A4</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M282,-863.7744C282,-831.6663 282,-790.6041 282,-758.1476"/>
<polygon fill="none" stroke="#000000" points="278.5001,-863.8621 282,-873.8622 285.5001,-863.8622 278.5001,-863.8621"/>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="450,-454 450,-498 572,-498 572,-454 450,-454"/>
<text text-anchor="start" x="501.277" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
<text text-anchor="start" x="478.4815" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;Singleton&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="450,-398 450,-454 572,-454 572,-398 450,-398"/>
<text text-anchor="start" x="468.4875" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;ha_restarts</text>
<text text-anchor="start" x="476.2665" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__client</text>
<text text-anchor="start" x="459.8735" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__cb_MqttIsUp</text>
<polygon fill="none" stroke="#000000" points="450,-354 450,-398 572,-398 572,-354 450,-354"/>
<text text-anchor="start" x="472.936" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish()</text>
<text text-anchor="start" x="477.1045" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;close()</text>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="396,-792 396,-824 626,-824 626,-792 396,-792"/>
<text text-anchor="start" x="498.2215" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Proxy</text>
<polygon fill="none" stroke="#000000" points="396,-676 396,-792 626,-792 626,-676 396,-676"/>
<text text-anchor="start" x="482.6545" y="-773" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;db_stat</text>
<text text-anchor="start" x="475.991" y="-761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;entity_prfx</text>
<text text-anchor="start" x="466.826" y="-749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;discovery_prfx</text>
<text text-anchor="start" x="466.262" y="-737" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;proxy_node_id</text>
<text text-anchor="start" x="462.373" y="-725" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;proxy_unique_id</text>
<text text-anchor="start" x="478.216" y="-713" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;mqtt:Mqtt</text>
<text text-anchor="start" x="480.4355" y="-689" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<polygon fill="none" stroke="#000000" points="396,-584 396,-676 626,-676 626,-584 396,-584"/>
<text text-anchor="start" x="487.1145" y="-657" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_init()</text>
<text text-anchor="start" x="481.834" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_close()</text>
<text text-anchor="start" x="453.484" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_cb_mqtt_is_up()</text>
<text text-anchor="start" x="405.697" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_register_proxy_stat_home_assistant()</text>
<text text-anchor="start" x="414.584" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_publ_mqtt_proxy_stat(key)</text>
</g>
<!-- A3&#45;&gt;A2 -->
<g id="edge9" class="edge">
<title>A3&#45;&gt;A2</title>
<path fill="none" stroke="#000000" d="M511,-571.373C511,-549.9571 511,-528.339 511,-508.5579"/>
<polygon fill="#000000" stroke="#000000" points="511.0001,-571.682 515,-577.6821 511,-583.682 507,-577.682 511.0001,-571.682"/>
<polygon fill="#000000" stroke="#000000" points="511,-498.392 515.5001,-508.3919 511,-503.392 511.0001,-508.392 511.0001,-508.392 511.0001,-508.392 511,-503.392 506.5001,-508.392 511,-498.392 511,-498.392"/>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="214,-502 214,-534 405,-534 405,-502 214,-502"/>
<text text-anchor="start" x="281.16" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterBase</text>
<polygon fill="none" stroke="#000000" points="214,-386 214,-502 405,-502 405,-386 214,-386"/>
<text text-anchor="start" x="290.3335" y="-483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
<text text-anchor="start" x="278.9355" y="-471" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<text text-anchor="start" x="299.497" y="-447" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="282.5505" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">config_id:str</text>
<text text-anchor="start" x="255.8785" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:MessageProt</text>
<text text-anchor="start" x="270.053" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="275.332" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="214,-318 214,-386 405,-386 405,-318 214,-318"/>
<text text-anchor="start" x="276.022" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()&#45;&gt;bool</text>
<text text-anchor="start" x="223.7835" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;disc(shutdown_started=False)</text>
<text text-anchor="start" x="255.044" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;create_remote()</text>
<text text-anchor="start" x="249.484" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;async_publ_mqtt()</text>
</g>
<!-- A3&#45;&gt;A5 -->
<g id="edge7" class="edge">
<title>A3&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M417.6791,-575.5683C407.6409,-561.7533 397.5008,-547.7982 387.6588,-534.2532"/>
<polygon fill="none" stroke="#000000" points="414.8649,-577.6495 423.5747,-583.682 420.5279,-573.5347 414.8649,-577.6495"/>
</g>
<!-- A4&#45;&gt;A5 -->
<g id="edge2" class="edge">
<title>A4&#45;&gt;A5</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M288.2719,-639.4228C291.3086,-608.1559 295.0373,-569.7639 298.491,-534.2034"/>
<polygon fill="none" stroke="#000000" points="284.7531,-639.4473 287.27,-649.7389 291.7203,-640.1241 284.7531,-639.4473"/>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="365,-236 365,-268 465,-268 465,-236 365,-236"/>
<text text-anchor="start" x="392.4995" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">StreamPtr</text>
<polygon fill="none" stroke="#000000" points="365,-216 365,-236 465,-236 465,-216 365,-216"/>
<polygon fill="none" stroke="#000000" points="365,-172 365,-216 465,-216 465,-172 365,-172"/>
<text text-anchor="start" x="374.7175" y="-197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:ProtocolIfc</text>
<text text-anchor="start" x="389.7185" y="-185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
</g>
<!-- A5&#45;&gt;A6 -->
<g id="edge8" class="edge">
<title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M364.6387,-317.872C371.8786,-303.802 379.0526,-289.86 385.6187,-277.0995"/>
<polygon fill="#000000" stroke="#000000" points="390.2846,-268.0318 389.7105,-278.9826 387.9969,-272.4777 385.7091,-276.9237 385.7091,-276.9237 385.7091,-276.9237 387.9969,-272.4777 381.7078,-274.8647 390.2846,-268.0318 390.2846,-268.0318"/>
<text text-anchor="middle" x="389.5069" y="-285.0166" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">2</text>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="346.7314,-238 271.2686,-238 271.2686,-202 346.7314,-202 346.7314,-238"/>
<text text-anchor="middle" x="309" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
</g>
<!-- A5&#45;&gt;A7 -->
<g id="edge5" class="edge">
<title>A5&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M309,-307.7729C309,-280.5002 309,-254.684 309,-238.2013"/>
<polygon fill="none" stroke="#000000" points="305.5001,-307.872 309,-317.872 312.5001,-307.872 305.5001,-307.872"/>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="102.9001,-238 21.0999,-238 21.0999,-202 102.9001,-202 102.9001,-238"/>
<text text-anchor="middle" x="62" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
</g>
<!-- A5&#45;&gt;A9 -->
<g id="edge6" class="edge">
<title>A5&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M205.2667,-346.4637C174.3973,-321.9347 140.8582,-294.4156 111,-268 100.2971,-258.5312 88.8616,-247.3925 79.732,-238.23"/>
<polygon fill="none" stroke="#000000" points="203.462,-349.4991 213.4739,-352.965 207.8086,-344.0121 203.462,-349.4991"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="458.6421,-36 369.3579,-36 369.3579,0 458.6421,0 458.6421,-36"/>
<text text-anchor="middle" x="414" y="-15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AsyncIfc&gt;&gt;</text>
</g>
<!-- A6&#45;&gt;A11 -->
<g id="edge11" class="edge">
<title>A6&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M401.1633,-171.974C395.4982,-146.4565 391.0868,-114.547 395,-86 396.8468,-72.5276 400.661,-57.9618 404.3907,-45.7804"/>
<polygon fill="#000000" stroke="#000000" points="407.4587,-36.1851 408.6994,-47.0805 405.9359,-40.9476 404.4131,-45.71 404.4131,-45.71 404.4131,-45.71 405.9359,-40.9476 400.1269,-44.3395 407.4587,-36.1851 407.4587,-36.1851"/>
<text text-anchor="middle" x="409.9892" y="-53.0243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="502.0879,-122 403.9121,-122 403.9121,-86 502.0879,-86 502.0879,-122"/>
<text text-anchor="middle" x="453" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;ProtocolIfc&gt;&gt;</text>
</g>
<!-- A6&#45;&gt;A12 -->
<g id="edge10" class="edge">
<title>A6&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M430.7853,-171.8133C435.2329,-158.2365 439.9225,-143.9208 443.8408,-131.9595"/>
<polygon fill="#000000" stroke="#000000" points="447.0602,-122.132 448.2235,-133.036 445.5036,-126.8835 443.9471,-131.6351 443.9471,-131.6351 443.9471,-131.6351 445.5036,-126.8835 439.6707,-130.2341 447.0602,-122.132 447.0602,-122.132"/>
<text text-anchor="middle" x="449.4498" y="-138.9887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="#fff8dc" stroke="#000000" points="583.406,-248 482.594,-248 482.594,-192 589.406,-192 589.406,-242 583.406,-248"/>
<polyline fill="none" stroke="#000000" points="583.406,-248 583.406,-242 "/>
<polyline fill="none" stroke="#000000" points="589.406,-242 583.406,-242 "/>
<text text-anchor="middle" x="536" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3</text>
<text text-anchor="middle" x="536" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
<text text-anchor="middle" x="536" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
<text text-anchor="middle" x="536" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:Talent</text>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge3" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M317.0491,-238.3283C325.9345,-256.0056 342.0793,-281.6949 365,-293 404.8598,-312.6598 424.0578,-310.2929 465,-293 486.6607,-283.8511 504.9784,-264.5049 517.5802,-248.0264"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="#fff8dc" stroke="#000000" points="247.522,-248 120.478,-248 120.478,-192 253.522,-192 253.522,-242 247.522,-248"/>
<polyline fill="none" stroke="#000000" points="247.522,-248 247.522,-242 "/>
<polyline fill="none" stroke="#000000" points="253.522,-242 247.522,-242 "/>
<text text-anchor="middle" x="187" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3PLUS</text>
<text text-anchor="middle" x="187" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
<text text-anchor="middle" x="187" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
<text text-anchor="middle" x="187" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:SolarmanV5</text>
</g>
<!-- A9&#45;&gt;A10 -->
<g id="edge4" class="edge">
<title>A9&#45;&gt;A10</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M103.0156,-220C108.8114,-220 114.6072,-220 120.403,-220"/>
</g>
<!-- A12&#45;&gt;A11 -->
<g id="edge12" class="edge">
<title>A12&#45;&gt;A11</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M444.7291,-85.7616C439.4033,-74.0176 432.3824,-58.5355 426.396,-45.3349"/>
<polygon fill="#000000" stroke="#000000" points="422.259,-36.2121 430.4874,-43.4608 424.324,-40.7657 426.3891,-45.3194 426.3891,-45.3194 426.3891,-45.3194 424.324,-40.7657 422.2908,-47.1779 422.259,-36.2121 422.259,-36.2121"/>
<text text-anchor="middle" x="429.5451" y="-69.7445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
</g>
<!-- A13 -->
<g id="node14" class="node">
<title>A13</title>
<polygon fill="none" stroke="#000000" points="9,-454 9,-486 116,-486 116,-454 9,-454"/>
<text text-anchor="start" x="32.7695" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusConn</text>
<polygon fill="none" stroke="#000000" points="9,-386 9,-454 116,-454 116,-386 9,-386"/>
<text text-anchor="start" x="53.0515" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">host</text>
<text text-anchor="start" x="53.887" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">port</text>
<text text-anchor="start" x="52.497" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="18.883" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:InverterG3P</text>
<polygon fill="none" stroke="#000000" points="9,-366 9,-386 116,-386 116,-366 9,-366"/>
</g>
<!-- A13&#45;&gt;A9 -->
<g id="edge13" class="edge">
<title>A13&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M62,-365.8625C62,-327.1513 62,-278.6088 62,-248.4442"/>
<polygon fill="#000000" stroke="#000000" points="62,-238.2147 66.5001,-248.2147 62,-243.2147 62.0001,-248.2147 62.0001,-248.2147 62.0001,-248.2147 62,-243.2147 57.5001,-248.2148 62,-238.2147 62,-238.2147"/>
<text text-anchor="middle" x="70.4524" y="-253.3409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
<text text-anchor="middle" x="53.5476" y="-344.7363" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A14 -->
<g id="node15" class="node">
<title>A14</title>
<polygon fill="none" stroke="#000000" points="0,-714 0,-746 124,-746 124,-714 0,-714"/>
<text text-anchor="start" x="35.8835" y="-727" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusTcp</text>
<polygon fill="none" stroke="#000000" points="0,-694 0,-714 124,-714 124,-694 0,-694"/>
<polygon fill="none" stroke="#000000" points="0,-662 0,-694 124,-694 124,-662 0,-662"/>
<text text-anchor="start" x="9.763" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;modbus_loop()</text>
</g>
<!-- A14&#45;&gt;A13 -->
<g id="edge14" class="edge">
<title>A14&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M62,-661.7778C62,-617.9184 62,-548.5387 62,-496.3736"/>
<polygon fill="#000000" stroke="#000000" points="62,-486.1827 66.5001,-496.1827 62,-491.1827 62.0001,-496.1827 62.0001,-496.1827 62.0001,-496.1827 62,-491.1827 57.5001,-496.1828 62,-486.1827 62,-486.1827"/>
<text text-anchor="middle" x="70.4524" y="-501.3089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">*</text>
<text text-anchor="middle" x="53.5476" y="-640.6516" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">creates</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -32,5 +32,5 @@
[ModbusConn|host;port;addr;stream:InverterG3P;|]has-1>[InverterG3P]
[ModbusTcp]creates-*>[ModbusConn]
[ModbusTcp||<async>modbus_loop()]creates-*>[ModbusConn]

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -4,11 +4,11 @@
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: G Pages: 1 -->
<svg width="539pt" height="1940pt"
viewBox="0.00 0.00 538.62 1940.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg width="541pt" height="1940pt"
viewBox="0.00 0.00 540.62 1940.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1936)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1936 534.6165,-1936 534.6165,4 -4,4"/>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1936 536.6165,-1936 536.6165,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
@@ -47,78 +47,78 @@
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="202.1165,-632 202.1165,-664 300.1165,-664 300.1165,-632 202.1165,-632"/>
<text text-anchor="start" x="224.1665" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
<polygon fill="none" stroke="#000000" points="202.1165,-576 202.1165,-632 300.1165,-632 300.1165,-576 202.1165,-576"/>
<text text-anchor="start" x="241.1135" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="211.6695" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="216.9485" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="202.1165,-520 202.1165,-576 300.1165,-576 300.1165,-520 202.1165,-520"/>
<text text-anchor="start" x="215.5585" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
<text text-anchor="start" x="236.119" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<polygon fill="none" stroke="#000000" points="199.1165,-632 199.1165,-664 297.1165,-664 297.1165,-632 199.1165,-632"/>
<text text-anchor="start" x="221.1665" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
<polygon fill="none" stroke="#000000" points="199.1165,-576 199.1165,-632 297.1165,-632 297.1165,-576 199.1165,-576"/>
<text text-anchor="start" x="238.1135" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="208.6695" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="213.9485" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="199.1165,-520 199.1165,-576 297.1165,-576 297.1165,-520 199.1165,-520"/>
<text text-anchor="start" x="212.5585" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
<text text-anchor="start" x="233.119" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="419.4531,-320 322.7799,-320 322.7799,-284 419.4531,-284 419.4531,-320"/>
<text text-anchor="middle" x="371.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="415.4531,-320 318.7799,-320 318.7799,-284 415.4531,-284 415.4531,-320"/>
<text text-anchor="middle" x="367.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A3 -->
<g id="edge1" class="edge">
<title>A2&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M285.5402,-508.8093C310.5478,-448.3743 342.848,-370.3156 359.7149,-329.5539"/>
<polygon fill="#000000" stroke="#000000" points="285.5219,-508.8538 286.9238,-515.9273 280.9336,-519.942 279.5317,-512.8685 285.5219,-508.8538"/>
<polygon fill="#000000" stroke="#000000" points="363.5595,-320.2627 363.894,-331.2235 361.6477,-324.8828 359.7359,-329.5029 359.7359,-329.5029 359.7359,-329.5029 361.6477,-324.8828 355.5779,-327.7823 363.5595,-320.2627 363.5595,-320.2627"/>
<path fill="none" stroke="#000000" d="M282.2534,-508.8093C307.0525,-448.3743 339.0836,-370.3156 355.8099,-329.5539"/>
<polygon fill="#000000" stroke="#000000" points="282.2407,-508.8403 283.6635,-515.9097 277.6851,-519.942 276.2624,-512.8726 282.2407,-508.8403"/>
<polygon fill="#000000" stroke="#000000" points="359.6225,-320.2627 359.9893,-331.2224 357.7243,-324.8884 355.8262,-329.5141 355.8262,-329.5141 355.8262,-329.5141 357.7243,-324.8884 351.663,-327.8058 359.6225,-320.2627 359.6225,-320.2627"/>
</g>
<!-- A4 -->
<g id="node5" class="node">
<title>A4</title>
<polygon fill="none" stroke="#000000" points="304.5106,-320 197.7224,-320 197.7224,-284 304.5106,-284 304.5106,-320"/>
<text text-anchor="middle" x="251.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="300.5106,-320 193.7224,-320 193.7224,-284 300.5106,-284 300.5106,-320"/>
<text text-anchor="middle" x="247.1165" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A4 -->
<g id="edge2" class="edge">
<title>A2&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M251.1165,-507.5905C251.1165,-447.68 251.1165,-370.9429 251.1165,-330.266"/>
<polygon fill="#000000" stroke="#000000" points="251.1166,-507.942 255.1165,-513.942 251.1165,-519.942 247.1165,-513.942 251.1166,-507.942"/>
<polygon fill="#000000" stroke="#000000" points="251.1165,-320.2627 255.6166,-330.2626 251.1165,-325.2627 251.1166,-330.2627 251.1166,-330.2627 251.1166,-330.2627 251.1165,-325.2627 246.6166,-330.2627 251.1165,-320.2627 251.1165,-320.2627"/>
<path fill="none" stroke="#000000" d="M247.8254,-507.5905C247.6188,-447.68 247.3542,-370.9429 247.214,-330.266"/>
<polygon fill="#000000" stroke="#000000" points="247.8265,-507.9421 251.8473,-513.9282 247.868,-519.942 243.8473,-513.9559 247.8265,-507.9421"/>
<polygon fill="#000000" stroke="#000000" points="247.1795,-320.2627 251.714,-330.2471 247.1968,-325.2627 247.2141,-330.2626 247.2141,-330.2626 247.2141,-330.2626 247.1968,-325.2627 242.7141,-330.2782 247.1795,-320.2627 247.1795,-320.2627"/>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="none" stroke="#000000" points="265.1165,-100 265.1165,-132 443.1165,-132 443.1165,-100 265.1165,-100"/>
<text text-anchor="start" x="309.668" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
<polygon fill="none" stroke="#000000" points="265.1165,-68 265.1165,-100 443.1165,-100 443.1165,-68 265.1165,-68"/>
<text text-anchor="start" x="321.8875" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
<polygon fill="none" stroke="#000000" points="265.1165,0 265.1165,-68 443.1165,-68 443.1165,0 265.1165,0"/>
<text text-anchor="start" x="305.774" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="296.605" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward()</text>
<text text-anchor="start" x="274.9255" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish_outstanding_mqtt()</text>
<text text-anchor="start" x="339.119" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<polygon fill="none" stroke="#000000" points="261.1165,-100 261.1165,-132 439.1165,-132 439.1165,-100 261.1165,-100"/>
<text text-anchor="start" x="305.668" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
<polygon fill="none" stroke="#000000" points="261.1165,-68 261.1165,-100 439.1165,-100 439.1165,-68 261.1165,-68"/>
<text text-anchor="start" x="317.8875" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
<polygon fill="none" stroke="#000000" points="261.1165,0 261.1165,-68 439.1165,-68 439.1165,0 261.1165,0"/>
<text text-anchor="start" x="301.774" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="292.605" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward()</text>
<text text-anchor="start" x="270.9255" y="-25" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish_outstanding_mqtt()</text>
<text text-anchor="start" x="335.119" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A3&#45;&gt;A8 -->
<g id="edge8" class="edge">
<title>A3&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M368.9314,-271.6651C366.5869,-239.1181 362.7725,-186.1658 359.6014,-142.1431"/>
<polygon fill="#000000" stroke="#000000" points="368.9485,-271.9044 373.3693,-277.6014 369.8108,-283.8733 365.39,-278.1763 368.9485,-271.9044"/>
<polygon fill="#000000" stroke="#000000" points="358.8731,-132.0321 364.08,-141.6829 359.2323,-137.0192 359.5916,-142.0063 359.5916,-142.0063 359.5916,-142.0063 359.2323,-137.0192 355.1032,-142.3296 358.8731,-132.0321 358.8731,-132.0321"/>
<path fill="none" stroke="#000000" d="M364.9314,-271.6651C362.5869,-239.1181 358.7725,-186.1658 355.6014,-142.1431"/>
<polygon fill="#000000" stroke="#000000" points="364.9485,-271.9044 369.3693,-277.6014 365.8108,-283.8733 361.39,-278.1763 364.9485,-271.9044"/>
<polygon fill="#000000" stroke="#000000" points="354.8731,-132.0321 360.08,-141.6829 355.2323,-137.0192 355.5916,-142.0063 355.5916,-142.0063 355.5916,-142.0063 355.2323,-137.0192 351.1032,-142.3296 354.8731,-132.0321 354.8731,-132.0321"/>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="93.1165,-82 93.1165,-114 231.1165,-114 231.1165,-82 93.1165,-82"/>
<text text-anchor="start" x="119.6135" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
<polygon fill="none" stroke="#000000" points="93.1165,-62 93.1165,-82 231.1165,-82 231.1165,-62 93.1165,-62"/>
<polygon fill="none" stroke="#000000" points="93.1165,-18 93.1165,-62 231.1165,-62 231.1165,-18 93.1165,-18"/>
<text text-anchor="start" x="115.9945" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="102.9405" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward())</text>
<polygon fill="none" stroke="#000000" points="89.1165,-82 89.1165,-114 227.1165,-114 227.1165,-82 89.1165,-82"/>
<text text-anchor="start" x="115.6135" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
<polygon fill="none" stroke="#000000" points="89.1165,-62 89.1165,-82 227.1165,-82 227.1165,-62 89.1165,-62"/>
<polygon fill="none" stroke="#000000" points="89.1165,-18 89.1165,-62 227.1165,-62 227.1165,-18 89.1165,-18"/>
<text text-anchor="start" x="111.9945" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="98.9405" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward())</text>
</g>
<!-- A4&#45;&gt;A9 -->
<g id="edge10" class="edge">
<title>A4&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M244.2806,-283.8733C231.5204,-250.0372 203.5834,-175.9573 183.8383,-123.5994"/>
<polygon fill="#000000" stroke="#000000" points="180.2523,-114.0904 187.9915,-121.8593 182.0166,-118.7688 183.781,-123.4472 183.781,-123.4472 183.781,-123.4472 182.0166,-118.7688 179.5704,-125.0351 180.2523,-114.0904 180.2523,-114.0904"/>
<text text-anchor="middle" x="229.9759" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
<path fill="none" stroke="#000000" d="M240.2806,-283.8733C227.5204,-250.0372 199.5834,-175.9573 179.8383,-123.5994"/>
<polygon fill="#000000" stroke="#000000" points="176.2523,-114.0904 183.9915,-121.8593 178.0166,-118.7688 179.781,-123.4472 179.781,-123.4472 179.781,-123.4472 178.0166,-118.7688 175.5704,-125.0351 176.2523,-114.0904 176.2523,-114.0904"/>
<text text-anchor="middle" x="225.9759" y="-266.8956" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A5 -->
<g id="node6" class="node">
@@ -149,135 +149,138 @@
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="87.1165,-622 87.1165,-654 180.1165,-654 180.1165,-622 87.1165,-622"/>
<text text-anchor="start" x="105.2805" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
<polygon fill="none" stroke="#000000" points="87.1165,-530 87.1165,-622 180.1165,-622 180.1165,-530 87.1165,-530"/>
<text text-anchor="start" x="96.6645" y="-603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
<text text-anchor="start" x="100.5535" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
<text text-anchor="start" x="100.2785" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
<text text-anchor="start" x="99.7125" y="-567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
<text text-anchor="start" x="115.83" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="109.166" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
<polygon fill="none" stroke="#000000" points="84.1165,-622 84.1165,-654 177.1165,-654 177.1165,-622 84.1165,-622"/>
<text text-anchor="start" x="102.2805" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
<polygon fill="none" stroke="#000000" points="84.1165,-530 84.1165,-622 177.1165,-622 177.1165,-530 84.1165,-530"/>
<text text-anchor="start" x="93.6645" y="-603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
<text text-anchor="start" x="97.5535" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
<text text-anchor="start" x="97.2785" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
<text text-anchor="start" x="96.7125" y="-567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
<text text-anchor="start" x="112.83" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="106.166" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
</g>
<!-- A5&#45;&gt;A6 -->
<g id="edge3" class="edge">
<title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M166.8518,-752.0017C159.4629,-716.9571 152.1492,-682.2694 146.2303,-654.1971"/>
<polygon fill="none" stroke="#000000" points="163.4489,-752.8275 168.9367,-761.8903 170.2983,-751.3833 163.4489,-752.8275"/>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M165.2976,-752.0017C157.592,-716.9571 149.9649,-682.2694 143.7924,-654.1971"/>
<polygon fill="none" stroke="#000000" points="161.906,-752.8753 167.4719,-761.8903 168.7427,-751.372 161.906,-752.8753"/>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="78.1165,-390 78.1165,-422 180.1165,-422 180.1165,-390 78.1165,-390"/>
<text text-anchor="start" x="99.3905" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
<polygon fill="none" stroke="#000000" points="78.1165,-310 78.1165,-390 180.1165,-390 180.1165,-310 78.1165,-310"/>
<text text-anchor="start" x="114.6695" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
<text text-anchor="start" x="116.8995" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
<text text-anchor="start" x="119.1135" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="114.6695" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
<text text-anchor="start" x="115.2245" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
<polygon fill="none" stroke="#000000" points="78.1165,-182 78.1165,-310 180.1165,-310 180.1165,-182 78.1165,-182"/>
<text text-anchor="start" x="100.7705" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="116.8985" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
<text text-anchor="start" x="114.119" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="109.6705" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="94.387" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
<text text-anchor="start" x="93.8375" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
<text text-anchor="start" x="87.7235" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
<polygon fill="none" stroke="#000000" points="74.1165,-390 74.1165,-422 176.1165,-422 176.1165,-390 74.1165,-390"/>
<text text-anchor="start" x="95.3905" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
<polygon fill="none" stroke="#000000" points="74.1165,-310 74.1165,-390 176.1165,-390 176.1165,-310 74.1165,-310"/>
<text text-anchor="start" x="110.6695" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
<text text-anchor="start" x="112.8995" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
<text text-anchor="start" x="115.1135" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="110.6695" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
<text text-anchor="start" x="111.2245" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
<polygon fill="none" stroke="#000000" points="74.1165,-182 74.1165,-310 176.1165,-310 176.1165,-182 74.1165,-182"/>
<text text-anchor="start" x="96.7705" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="112.8985" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
<text text-anchor="start" x="110.119" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="105.6705" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="90.387" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
<text text-anchor="start" x="89.8375" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
<text text-anchor="start" x="83.7235" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
</g>
<!-- A6&#45;&gt;A7 -->
<g id="edge4" class="edge">
<title>A6&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M132.1177,-519.5861C131.7106,-490.0737 131.229,-455.1552 130.7721,-422.0295"/>
<polygon fill="none" stroke="#000000" points="128.6207,-519.837 132.2584,-529.7877 135.6201,-519.7404 128.6207,-519.837"/>
<path fill="none" stroke="#000000" d="M128.868,-519.5861C128.3592,-490.0737 127.7571,-455.1552 127.186,-422.0295"/>
<polygon fill="none" stroke="#000000" points="125.3719,-519.8496 129.0439,-529.7877 132.3709,-519.7288 125.3719,-519.8496"/>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge5" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M186.5777,-185.0204C187.4154,-184.0001 188.2616,-182.9929 189.1165,-182 210.4788,-157.1889 238.2469,-135.0276 264.8921,-116.8901"/>
<polygon fill="none" stroke="#000000" points="183.6875,-183.0361 180.3256,-193.0834 189.2193,-187.3255 183.6875,-183.0361"/>
<path fill="none" stroke="#000000" d="M182.5777,-185.0204C183.4154,-184.0001 184.2616,-182.9929 185.1165,-182 206.4788,-157.1889 234.2469,-135.0276 260.8921,-116.8901"/>
<polygon fill="none" stroke="#000000" points="179.6875,-183.0361 176.3256,-193.0834 185.2193,-187.3255 179.6875,-183.0361"/>
</g>
<!-- A7&#45;&gt;A9 -->
<g id="edge6" class="edge">
<title>A7&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M147.3214,-171.8077C150.1952,-151.2556 152.9992,-131.2022 155.3799,-114.1772"/>
<polygon fill="none" stroke="#000000" points="143.8252,-171.5375 145.9065,-181.9259 150.7577,-172.5069 143.8252,-171.5375"/>
<path fill="none" stroke="#000000" d="M143.3214,-171.8077C146.1952,-151.2556 148.9992,-131.2022 151.3799,-114.1772"/>
<polygon fill="none" stroke="#000000" points="139.8252,-171.5375 141.9065,-181.9259 146.7577,-172.5069 139.8252,-171.5375"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="none" stroke="#000000" points="319.1165,-668 319.1165,-700 410.1165,-700 410.1165,-668 319.1165,-668"/>
<text text-anchor="start" x="337.1115" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
<polygon fill="none" stroke="#000000" points="319.1165,-552 319.1165,-668 410.1165,-668 410.1165,-552 319.1165,-552"/>
<text text-anchor="start" x="345.4395" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
<text text-anchor="start" x="354.6135" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="349.6145" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
<text text-anchor="start" x="352.674" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
<text text-anchor="start" x="357.6725" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
<text text-anchor="start" x="336.8265" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
<text text-anchor="start" x="350.7285" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="319.1165,-484 319.1165,-552 410.1165,-552 410.1165,-484 319.1165,-484"/>
<text text-anchor="start" x="329.057" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="345.1705" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="349.619" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<polygon fill="none" stroke="#000000" points="318.1165,-668 318.1165,-700 409.1165,-700 409.1165,-668 318.1165,-668"/>
<text text-anchor="start" x="336.1115" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
<polygon fill="none" stroke="#000000" points="318.1165,-552 318.1165,-668 409.1165,-668 409.1165,-552 318.1165,-552"/>
<text text-anchor="start" x="344.4395" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
<text text-anchor="start" x="353.6135" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="348.6145" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
<text text-anchor="start" x="351.674" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
<text text-anchor="start" x="356.6725" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
<text text-anchor="start" x="335.8265" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
<text text-anchor="start" x="349.7285" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="318.1165,-484 318.1165,-552 409.1165,-552 409.1165,-484 318.1165,-484"/>
<text text-anchor="start" x="328.057" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="344.1705" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="348.619" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A10&#45;&gt;A3 -->
<g id="edge7" class="edge">
<title>A10&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M366.9763,-473.5237C368.222,-421.9136 369.5798,-365.6622 370.389,-332.138"/>
<polygon fill="#000000" stroke="#000000" points="366.733,-483.6023 362.4757,-473.4966 366.8537,-478.6038 366.9744,-473.6052 366.9744,-473.6052 366.9744,-473.6052 366.8537,-478.6038 371.4731,-473.7139 366.733,-483.6023 366.733,-483.6023"/>
<polygon fill="#000000" stroke="#000000" points="370.3911,-332.0495 366.5371,-325.9547 370.6807,-320.053 374.5347,-326.1478 370.3911,-332.0495"/>
<path fill="none" stroke="#000000" d="M364.7507,-473.5237C365.4625,-421.9136 366.2384,-365.6622 366.7008,-332.138"/>
<polygon fill="#000000" stroke="#000000" points="364.6116,-483.6023 360.2501,-473.5412 364.6806,-478.6028 364.7497,-473.6033 364.7497,-473.6033 364.7497,-473.6033 364.6806,-478.6028 369.2492,-473.6654 364.6116,-483.6023 364.6116,-483.6023"/>
<polygon fill="#000000" stroke="#000000" points="366.7019,-332.0519 362.7851,-325.9973 366.8675,-320.053 370.7843,-326.1077 366.7019,-332.0519"/>
</g>
<!-- A10&#45;&gt;A4 -->
<g id="edge9" class="edge">
<title>A10&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M318.2339,-474.2481C295.3796,-415.5956 270.1211,-350.7729 258.151,-320.053"/>
<polygon fill="#000000" stroke="#000000" points="321.8788,-483.6023 314.0551,-475.9185 320.0634,-478.9435 318.2481,-474.2847 318.2481,-474.2847 318.2481,-474.2847 320.0634,-478.9435 322.441,-472.6508 321.8788,-483.6023 321.8788,-483.6023"/>
<text text-anchor="middle" x="272.6076" y="-330.8736" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
<path fill="none" stroke="#000000" d="M316.0157,-474.2481C292.5547,-415.5956 266.6257,-350.7729 254.3377,-320.053"/>
<polygon fill="#000000" stroke="#000000" points="319.7574,-483.6023 311.8653,-475.9888 317.9004,-478.9599 316.0435,-474.3175 316.0435,-474.3175 316.0435,-474.3175 317.9004,-478.9599 320.2216,-472.6462 319.7574,-483.6023 319.7574,-483.6023"/>
<text text-anchor="middle" x="268.9174" y="-330.7436" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="442.1165,-318 442.1165,-350 509.1165,-350 509.1165,-318 442.1165,-318"/>
<text text-anchor="start" x="454.775" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
<polygon fill="none" stroke="#000000" points="442.1165,-298 442.1165,-318 509.1165,-318 509.1165,-298 442.1165,-298"/>
<polygon fill="none" stroke="#000000" points="442.1165,-254 442.1165,-298 509.1165,-298 509.1165,-254 442.1165,-254"/>
<text text-anchor="start" x="452.0005" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="459.7845" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
<polygon fill="none" stroke="#000000" points="434.1165,-336 434.1165,-368 529.1165,-368 529.1165,-336 434.1165,-336"/>
<text text-anchor="start" x="460.775" y="-349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
<polygon fill="none" stroke="#000000" points="434.1165,-304 434.1165,-336 529.1165,-336 529.1165,-304 434.1165,-304"/>
<text text-anchor="start" x="443.829" y="-317" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">client_mode:bool</text>
<polygon fill="none" stroke="#000000" points="434.1165,-236 434.1165,-304 529.1165,-304 529.1165,-236 434.1165,-236"/>
<text text-anchor="start" x="458.0005" y="-285" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="465.7845" y="-273" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
<text text-anchor="start" x="469.3985" y="-261" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">calc()</text>
<text text-anchor="start" x="467.73" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build()</text>
</g>
<!-- A10&#45;&gt;A12 -->
<g id="edge12" class="edge">
<title>A10&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M405.6067,-483.6023C421.7045,-441.5449 439.4849,-395.0916 453.0329,-359.6958"/>
<polygon fill="#000000" stroke="#000000" points="456.737,-350.0185 457.3649,-360.9664 454.9496,-354.6881 453.1623,-359.3577 453.1623,-359.3577 453.1623,-359.3577 454.9496,-354.6881 448.9596,-357.7491 456.737,-350.0185 456.737,-350.0185"/>
<path fill="none" stroke="#000000" d="M407.2231,-483.6023C421.5819,-448.3138 437.2,-409.9305 450.3629,-377.5809"/>
<polygon fill="#000000" stroke="#000000" points="454.1856,-368.1861 454.5848,-379.1447 452.3011,-372.8174 450.4166,-377.4487 450.4166,-377.4487 450.4166,-377.4487 452.3011,-372.8174 446.2485,-375.7526 454.1856,-368.1861 454.1856,-368.1861"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="428.1165,-680 428.1165,-712 531.1165,-712 531.1165,-680 428.1165,-680"/>
<text text-anchor="start" x="468.7785" y="-693" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
<polygon fill="none" stroke="#000000" points="428.1165,-624 428.1165,-680 531.1165,-680 531.1165,-624 428.1165,-624"/>
<text text-anchor="start" x="471.558" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="447.1025" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="460.72" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="428.1165,-472 428.1165,-624 531.1165,-624 531.1165,-472 428.1165,-472"/>
<text text-anchor="start" x="455.452" y="-605" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="453.501" y="-593" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="450.447" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="448.777" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="446.8265" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
<text text-anchor="start" x="461.8295" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
<text text-anchor="start" x="454.6105" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
<text text-anchor="start" x="455.991" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
<text text-anchor="start" x="440.1535" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
<text text-anchor="start" x="449.602" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
<text text-anchor="start" x="437.939" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
<polygon fill="none" stroke="#000000" points="430.1165,-680 430.1165,-712 533.1165,-712 533.1165,-680 430.1165,-680"/>
<text text-anchor="start" x="470.7785" y="-693" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
<polygon fill="none" stroke="#000000" points="430.1165,-624 430.1165,-680 533.1165,-680 533.1165,-624 430.1165,-624"/>
<text text-anchor="start" x="473.558" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="449.1025" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="462.72" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="430.1165,-472 430.1165,-624 533.1165,-624 533.1165,-472 430.1165,-472"/>
<text text-anchor="start" x="457.452" y="-605" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="455.501" y="-593" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="452.447" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="450.777" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="448.8265" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
<text text-anchor="start" x="463.8295" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
<text text-anchor="start" x="456.6105" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
<text text-anchor="start" x="457.991" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
<text text-anchor="start" x="442.1535" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
<text text-anchor="start" x="451.602" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
<text text-anchor="start" x="439.939" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
</g>
<!-- A11&#45;&gt;A12 -->
<g id="edge11" class="edge">
<title>A11&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M477.322,-461.8987C476.7744,-422.1971 476.206,-380.9898 475.7834,-350.352"/>
<polygon fill="none" stroke="#000000" points="473.823,-462.0018 477.4607,-471.9525 480.8223,-461.9052 473.823,-462.0018"/>
<path fill="none" stroke="#000000" d="M481.1165,-461.9134C481.1165,-429.4373 481.1165,-395.9527 481.1165,-368.0691"/>
<polygon fill="none" stroke="#000000" points="477.6166,-461.9525 481.1165,-471.9525 484.6166,-461.9526 477.6166,-461.9525"/>
</g>
<!-- A13 -->
<g id="node14" class="node">
@@ -321,8 +324,8 @@
<!-- A13&#45;&gt;A10 -->
<g id="edge16" class="edge">
<title>A13&#45;&gt;A10</title>
<path fill="none" stroke="#000000" d="M277.1595,-1125.5329C299.2708,-989.8666 328.1962,-812.3923 346.4719,-700.2604"/>
<polygon fill="none" stroke="#000000" points="273.6668,-1125.205 275.5125,-1135.6378 280.5757,-1126.3311 273.6668,-1125.205"/>
<path fill="none" stroke="#000000" d="M276.8964,-1125.5329C298.8204,-989.8666 327.5006,-812.3923 345.6214,-700.2604"/>
<polygon fill="none" stroke="#000000" points="273.4037,-1125.2075 275.2634,-1135.6378 280.314,-1126.3243 273.4037,-1125.2075"/>
</g>
<!-- A14&#45;&gt;A13 -->
<g id="edge15" class="edge">

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -26,7 +26,7 @@
[remote:StreamPtr]0..1->[AsyncStreamClient]
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
[Infos]^[InfosG3P||ha_confs();parse()]
[Infos]^[InfosG3P|client_mode:bool|ha_confs();parse();calc();build()]
[SolarmanV5]->[InfosG3P]

View File

@@ -1,260 +0,0 @@
<?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="626pt" height="966pt"
viewBox="0.00 0.00 625.50 966.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 962)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-962 621.5,-962 621.5,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="191.6964,-934 83.3036,-934 83.3036,-898 197.6964,-898 197.6964,-928 191.6964,-934"/>
<polyline fill="none" stroke="#000000" points="191.6964,-934 191.6964,-928 "/>
<polyline fill="none" stroke="#000000" points="197.6964,-928 191.6964,-928 "/>
<text text-anchor="middle" x="140.5" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
<text text-anchor="middle" x="140.5" y="-907" 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="215.5,-926 215.5,-958 331.5,-958 331.5,-926 215.5,-926"/>
<text text-anchor="start" x="225.149" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AbstractIterMeta&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="215.5,-906 215.5,-926 331.5,-926 331.5,-906 215.5,-906"/>
<polygon fill="none" stroke="#000000" points="215.5,-874 215.5,-906 331.5,-906 331.5,-874 215.5,-874"/>
<text text-anchor="start" x="252.11" y="-887" 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="178.5,-726 178.5,-758 369.5,-758 369.5,-726 178.5,-726"/>
<text text-anchor="start" x="240.0965" y="-739" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;InverterIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="178.5,-706 178.5,-726 369.5,-726 369.5,-706 178.5,-706"/>
<polygon fill="none" stroke="#000000" points="178.5,-650 178.5,-706 369.5,-706 369.5,-650 178.5,-650"/>
<text text-anchor="start" x="240.522" y="-687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()&#45;&gt;bool</text>
<text text-anchor="start" x="188.2835" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;disc(shutdown_started=False)</text>
<text text-anchor="start" x="219.544" y="-663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;create_remote()</text>
</g>
<!-- A1&#45;&gt;A4 -->
<g id="edge1" class="edge">
<title>A1&#45;&gt;A4</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M273.5,-863.7744C273.5,-831.6663 273.5,-790.6041 273.5,-758.1476"/>
<polygon fill="none" stroke="#000000" points="270.0001,-863.8621 273.5,-873.8622 277.0001,-863.8622 270.0001,-863.8621"/>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="441.5,-454 441.5,-498 563.5,-498 563.5,-454 441.5,-454"/>
<text text-anchor="start" x="492.777" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
<text text-anchor="start" x="469.9815" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;Singleton&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="441.5,-398 441.5,-454 563.5,-454 563.5,-398 441.5,-398"/>
<text text-anchor="start" x="459.9875" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;ha_restarts</text>
<text text-anchor="start" x="467.7665" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__client</text>
<text text-anchor="start" x="451.3735" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__cb_MqttIsUp</text>
<polygon fill="none" stroke="#000000" points="441.5,-354 441.5,-398 563.5,-398 563.5,-354 441.5,-354"/>
<text text-anchor="start" x="464.436" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish()</text>
<text text-anchor="start" x="468.6045" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;close()</text>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="387.5,-792 387.5,-824 617.5,-824 617.5,-792 387.5,-792"/>
<text text-anchor="start" x="489.7215" y="-805" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Proxy</text>
<polygon fill="none" stroke="#000000" points="387.5,-676 387.5,-792 617.5,-792 617.5,-676 387.5,-676"/>
<text text-anchor="start" x="474.1545" y="-773" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;db_stat</text>
<text text-anchor="start" x="467.491" y="-761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;entity_prfx</text>
<text text-anchor="start" x="458.326" y="-749" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;discovery_prfx</text>
<text text-anchor="start" x="457.762" y="-737" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;proxy_node_id</text>
<text text-anchor="start" x="453.873" y="-725" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;proxy_unique_id</text>
<text text-anchor="start" x="469.716" y="-713" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;cls&gt;mqtt:Mqtt</text>
<text text-anchor="start" x="471.9355" y="-689" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<polygon fill="none" stroke="#000000" points="387.5,-584 387.5,-676 617.5,-676 617.5,-584 387.5,-584"/>
<text text-anchor="start" x="478.6145" y="-657" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_init()</text>
<text text-anchor="start" x="473.334" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">class_close()</text>
<text text-anchor="start" x="444.984" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_cb_mqtt_is_up()</text>
<text text-anchor="start" x="397.197" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_register_proxy_stat_home_assistant()</text>
<text text-anchor="start" x="406.084" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_publ_mqtt_proxy_stat(key)</text>
</g>
<!-- A3&#45;&gt;A2 -->
<g id="edge9" class="edge">
<title>A3&#45;&gt;A2</title>
<path fill="none" stroke="#000000" d="M502.5,-571.373C502.5,-549.9571 502.5,-528.339 502.5,-508.5579"/>
<polygon fill="#000000" stroke="#000000" points="502.5001,-571.682 506.5,-577.6821 502.5,-583.682 498.5,-577.682 502.5001,-571.682"/>
<polygon fill="#000000" stroke="#000000" points="502.5,-498.392 507.0001,-508.3919 502.5,-503.392 502.5001,-508.392 502.5001,-508.392 502.5001,-508.392 502.5,-503.392 498.0001,-508.392 502.5,-498.392 502.5,-498.392"/>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="205.5,-502 205.5,-534 396.5,-534 396.5,-502 205.5,-502"/>
<text text-anchor="start" x="272.66" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterBase</text>
<polygon fill="none" stroke="#000000" points="205.5,-386 205.5,-502 396.5,-502 396.5,-386 205.5,-386"/>
<text text-anchor="start" x="281.8335" y="-483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
<text text-anchor="start" x="270.4355" y="-471" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<text text-anchor="start" x="290.997" y="-447" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="274.0505" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">config_id:str</text>
<text text-anchor="start" x="247.3785" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:MessageProt</text>
<text text-anchor="start" x="261.553" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="266.832" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="205.5,-318 205.5,-386 396.5,-386 396.5,-318 205.5,-318"/>
<text text-anchor="start" x="267.522" y="-367" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()&#45;&gt;bool</text>
<text text-anchor="start" x="215.2835" y="-355" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;disc(shutdown_started=False)</text>
<text text-anchor="start" x="246.544" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;create_remote()</text>
<text text-anchor="start" x="240.984" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;async_publ_mqtt()</text>
</g>
<!-- A3&#45;&gt;A5 -->
<g id="edge7" class="edge">
<title>A3&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M409.1791,-575.5683C399.1409,-561.7533 389.0008,-547.7982 379.1588,-534.2532"/>
<polygon fill="none" stroke="#000000" points="406.3649,-577.6495 415.0747,-583.682 412.0279,-573.5347 406.3649,-577.6495"/>
</g>
<!-- A4&#45;&gt;A5 -->
<g id="edge2" class="edge">
<title>A4&#45;&gt;A5</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M279.7719,-639.4228C282.8086,-608.1559 286.5373,-569.7639 289.991,-534.2034"/>
<polygon fill="none" stroke="#000000" points="276.2531,-639.4473 278.77,-649.7389 283.2203,-640.1241 276.2531,-639.4473"/>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="356.5,-236 356.5,-268 456.5,-268 456.5,-236 356.5,-236"/>
<text text-anchor="start" x="383.9995" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">StreamPtr</text>
<polygon fill="none" stroke="#000000" points="356.5,-216 356.5,-236 456.5,-236 456.5,-216 356.5,-216"/>
<polygon fill="none" stroke="#000000" points="356.5,-172 356.5,-216 456.5,-216 456.5,-172 356.5,-172"/>
<text text-anchor="start" x="366.2175" y="-197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:ProtocolIfc</text>
<text text-anchor="start" x="381.2185" y="-185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
</g>
<!-- A5&#45;&gt;A6 -->
<g id="edge8" class="edge">
<title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M356.1387,-317.872C363.3786,-303.802 370.5526,-289.86 377.1187,-277.0995"/>
<polygon fill="#000000" stroke="#000000" points="381.7846,-268.0318 381.2105,-278.9826 379.4969,-272.4777 377.2091,-276.9237 377.2091,-276.9237 377.2091,-276.9237 379.4969,-272.4777 373.2078,-274.8647 381.7846,-268.0318 381.7846,-268.0318"/>
<text text-anchor="middle" x="381.0069" y="-285.0166" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">2</text>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="338.2314,-238 262.7686,-238 262.7686,-202 338.2314,-202 338.2314,-238"/>
<text text-anchor="middle" x="300.5" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
</g>
<!-- A5&#45;&gt;A7 -->
<g id="edge5" class="edge">
<title>A5&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M300.5,-307.7729C300.5,-280.5002 300.5,-254.684 300.5,-238.2013"/>
<polygon fill="none" stroke="#000000" points="297.0001,-307.872 300.5,-317.872 304.0001,-307.872 297.0001,-307.872"/>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="94.4001,-238 12.5999,-238 12.5999,-202 94.4001,-202 94.4001,-238"/>
<text text-anchor="middle" x="53.5" y="-217" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
</g>
<!-- A5&#45;&gt;A9 -->
<g id="edge6" class="edge">
<title>A5&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M196.7667,-346.4637C165.8973,-321.9347 132.3582,-294.4156 102.5,-268 91.7971,-258.5312 80.3616,-247.3925 71.232,-238.23"/>
<polygon fill="none" stroke="#000000" points="194.962,-349.4991 204.9739,-352.965 199.3086,-344.0121 194.962,-349.4991"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="450.1421,-36 360.8579,-36 360.8579,0 450.1421,0 450.1421,-36"/>
<text text-anchor="middle" x="405.5" y="-15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AsyncIfc&gt;&gt;</text>
</g>
<!-- A6&#45;&gt;A11 -->
<g id="edge11" class="edge">
<title>A6&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M392.6633,-171.974C386.9982,-146.4565 382.5868,-114.547 386.5,-86 388.3468,-72.5276 392.161,-57.9618 395.8907,-45.7804"/>
<polygon fill="#000000" stroke="#000000" points="398.9587,-36.1851 400.1994,-47.0805 397.4359,-40.9476 395.9131,-45.71 395.9131,-45.71 395.9131,-45.71 397.4359,-40.9476 391.6269,-44.3395 398.9587,-36.1851 398.9587,-36.1851"/>
<text text-anchor="middle" x="401.4892" y="-53.0243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="493.5879,-122 395.4121,-122 395.4121,-86 493.5879,-86 493.5879,-122"/>
<text text-anchor="middle" x="444.5" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;ProtocolIfc&gt;&gt;</text>
</g>
<!-- A6&#45;&gt;A12 -->
<g id="edge10" class="edge">
<title>A6&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M422.2853,-171.8133C426.7329,-158.2365 431.4225,-143.9208 435.3408,-131.9595"/>
<polygon fill="#000000" stroke="#000000" points="438.5602,-122.132 439.7235,-133.036 437.0036,-126.8835 435.4471,-131.6351 435.4471,-131.6351 435.4471,-131.6351 437.0036,-126.8835 431.1707,-130.2341 438.5602,-122.132 438.5602,-122.132"/>
<text text-anchor="middle" x="440.9498" y="-138.9887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="#fff8dc" stroke="#000000" points="574.906,-248 474.094,-248 474.094,-192 580.906,-192 580.906,-242 574.906,-248"/>
<polyline fill="none" stroke="#000000" points="574.906,-248 574.906,-242 "/>
<polyline fill="none" stroke="#000000" points="580.906,-242 574.906,-242 "/>
<text text-anchor="middle" x="527.5" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3</text>
<text text-anchor="middle" x="527.5" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
<text text-anchor="middle" x="527.5" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
<text text-anchor="middle" x="527.5" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:Talent</text>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge3" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M308.5491,-238.3283C317.4345,-256.0056 333.5793,-281.6949 356.5,-293 396.3598,-312.6598 415.5578,-310.2929 456.5,-293 478.1607,-283.8511 496.4784,-264.5049 509.0802,-248.0264"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="#fff8dc" stroke="#000000" points="239.022,-248 111.978,-248 111.978,-192 245.022,-192 245.022,-242 239.022,-248"/>
<polyline fill="none" stroke="#000000" points="239.022,-248 239.022,-242 "/>
<polyline fill="none" stroke="#000000" points="245.022,-242 239.022,-242 "/>
<text text-anchor="middle" x="178.5" y="-235" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Creates an GEN3PLUS</text>
<text text-anchor="middle" x="178.5" y="-223" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter instance</text>
<text text-anchor="middle" x="178.5" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">with</text>
<text text-anchor="middle" x="178.5" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_class:SolarmanV5</text>
</g>
<!-- A9&#45;&gt;A10 -->
<g id="edge4" class="edge">
<title>A9&#45;&gt;A10</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M94.5156,-220C100.3114,-220 106.1072,-220 111.903,-220"/>
</g>
<!-- A12&#45;&gt;A11 -->
<g id="edge12" class="edge">
<title>A12&#45;&gt;A11</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M436.2291,-85.7616C430.9033,-74.0176 423.8824,-58.5355 417.896,-45.3349"/>
<polygon fill="#000000" stroke="#000000" points="413.759,-36.2121 421.9874,-43.4608 415.824,-40.7657 417.8891,-45.3194 417.8891,-45.3194 417.8891,-45.3194 415.824,-40.7657 413.7908,-47.1779 413.759,-36.2121 413.759,-36.2121"/>
<text text-anchor="middle" x="421.0451" y="-69.7445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
</g>
<!-- A13 -->
<g id="node14" class="node">
<title>A13</title>
<polygon fill="none" stroke="#000000" points=".5,-454 .5,-486 107.5,-486 107.5,-454 .5,-454"/>
<text text-anchor="start" x="24.2695" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusConn</text>
<polygon fill="none" stroke="#000000" points=".5,-386 .5,-454 107.5,-454 107.5,-386 .5,-386"/>
<text text-anchor="start" x="44.5515" y="-435" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">host</text>
<text text-anchor="start" x="45.387" y="-423" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">port</text>
<text text-anchor="start" x="43.997" y="-411" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="10.383" y="-399" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stream:InverterG3P</text>
<polygon fill="none" stroke="#000000" points=".5,-366 .5,-386 107.5,-386 107.5,-366 .5,-366"/>
</g>
<!-- A13&#45;&gt;A9 -->
<g id="edge13" class="edge">
<title>A13&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M53.5,-365.8625C53.5,-327.1513 53.5,-278.6088 53.5,-248.4442"/>
<polygon fill="#000000" stroke="#000000" points="53.5,-238.2147 58.0001,-248.2147 53.5,-243.2147 53.5001,-248.2147 53.5001,-248.2147 53.5001,-248.2147 53.5,-243.2147 49.0001,-248.2148 53.5,-238.2147 53.5,-238.2147"/>
<text text-anchor="middle" x="61.9524" y="-253.3409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
<text text-anchor="middle" x="45.0476" y="-344.7363" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A14 -->
<g id="node15" class="node">
<title>A14</title>
<polygon fill="none" stroke="#000000" points="93.7333,-722 13.2667,-722 13.2667,-686 93.7333,-686 93.7333,-722"/>
<text text-anchor="middle" x="53.5" y="-701" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ModbusTcp</text>
</g>
<!-- A14&#45;&gt;A13 -->
<g id="edge14" class="edge">
<title>A14&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M53.5,-685.7596C53.5,-647.9991 53.5,-559.5189 53.5,-496.3277"/>
<polygon fill="#000000" stroke="#000000" points="53.5,-486.0223 58.0001,-496.0223 53.5,-491.0223 53.5001,-496.0223 53.5001,-496.0223 53.5001,-496.0223 53.5,-491.0223 49.0001,-496.0224 53.5,-486.0223 53.5,-486.0223"/>
<text text-anchor="middle" x="61.9524" y="-501.1485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">*</text>
<text text-anchor="middle" x="45.0476" y="-664.6335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">creates</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,8 +1,8 @@
flake8
pytest
pytest-asyncio
pytest-cov
python-dotenv
mock
coverage
jinja2-cli
flake8==7.1.2
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.1.0
python-dotenv==1.1.0
mock==5.2.0
coverage==7.8.0
jinja2-cli==0.8.2

View File

@@ -1,4 +1,4 @@
aiomqtt==2.3.0
aiomqtt==2.3.1
schema==0.7.7
aiocron==1.8
aiohttp==3.11.10
aiocron==2.1
aiohttp==3.11.16

View File

@@ -78,7 +78,8 @@ class Config():
}
},
'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),
Use(lambda s: s + '/'
@@ -92,8 +93,13 @@ class Config():
Optional('forward', default=False): Use(bool),
},
Optional('modbus_polling', default=True): Use(bool),
Optional('modbus_scanning'): {
'start': Use(int),
Optional('step', default=0x400): Use(int),
Optional('bytes', default=0x10): Use(int),
},
Optional('suggested_area', default=""): Use(str),
Optional('sensor_list', default=0x2b0): Use(int),
Optional('sensor_list', default=0): Use(int),
Optional('pv1'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
@@ -119,6 +125,38 @@ class Config():
Optional('manufacturer'): Use(str),
}
}
},
'batteries': {
And(Use(str), lambda s: len(s) == 16): {
Optional('monitor_sn', default=0): Use(int),
Optional('node_id', default=""): And(Use(str),
Use(lambda s: s + '/'
if len(s) > 0
and s[-1] != '/'
else s)),
Optional('client_mode'): {
'host': Use(str),
Optional('port', default=8899):
And(Use(int), lambda n: 1024 <= n <= 65535),
Optional('forward', default=False): Use(bool),
},
Optional('modbus_polling', default=True): Use(bool),
Optional('modbus_scanning'): {
'start': Use(int),
Optional('step', default=0x400): Use(int),
Optional('bytes', default=0x10): Use(int),
},
Optional('suggested_area', default=""): Use(str),
Optional('sensor_list', default=0): Use(int),
Optional('pv1'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv2'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
}
}
}
}, ignore_extra_keys=True
)
@@ -178,7 +216,7 @@ here. The default config reader is handled in the Config.init method'''
rd_config = reader.get_config()
config = cls.act_config.copy()
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
'gen3plus']:
'gen3plus', 'batteries']:
if key in rd_config:
config[key] = config[key] | rd_config[key]

View File

@@ -31,7 +31,8 @@ class ConfigReadJson(ConfigIfc):
def convert_to_obj(self, data):
conf = {}
for key, val in data.items():
if key == 'inverters' and isinstance(val, list):
if (key == 'inverters' or key == 'batteries') and \
isinstance(val, list):
self.convert_inv_arr(conf, key, val)
else:
self._extend_key(conf, key, val)

View File

@@ -15,7 +15,7 @@
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Operation-Modes-Overview
###
### Here you will find a description of all configuration options:
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
### https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml
###
### The configration uses the TOML format, which aims to be easy to read due to
### obvious semantics. You find more details here: https://toml.io/en/v1.0.0
@@ -31,7 +31,7 @@
## required credentials. As the proxy does not currently support an encrypted connection
## to the MQTT broker, it is strongly recommended that you do not use a public broker.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#mqtt-broker-account
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#mqtt-broker-account
##
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
@@ -48,7 +48,7 @@ mqtt.passwd = ''
## values match the HA default configuration. If you need to change these or want to use
## a different MQTT client, you can adjust the prefixes of the MQTT topics below.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#home-assistant
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#home-assistant
##
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
@@ -66,7 +66,7 @@ ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a prox
## inverters. This connection is only required if you want send data to the TSUN cloud
## to use the TSUN APPs or receive firmware updates.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#tsun-cloud-for-gen3-inverter-only
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#tsun-cloud-for-gen3-inverter-only
##
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
@@ -82,7 +82,7 @@ tsun.port = 5005
## inverters. This connection is only required if you want send data to the TSUN cloud
## to use the TSUN APPs or receive firmware updates.
##
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#solarman-cloud-for-gen3plus-inverter-only
## https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml#solarman-cloud-for-gen3plus-inverter-only
##
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
@@ -113,7 +113,7 @@ inverters.allow_all = false # only allow known inverters
##
## For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
## definition. To do this, the corresponding configuration block is started with
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
## in the configuration block
##
@@ -132,7 +132,7 @@ pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module de
##
## For each GEN3PLUS inverter, the serial number of the inverter must be mapped to an MQTT
## definition. To do this, the corresponding configuration block is started with
## `[Inverter.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## `[inverters.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
## can be set in the configuration block
##
@@ -157,6 +157,33 @@ pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
##
## For each GEN3PLUS energy storage system, the serial number must be mapped to an MQTT
## definition. To do this, the corresponding configuration block is started with
## `[batteries.“<16-digit serial number>”]` so that all subsequent parameters are assigned
## to this energy storage system. Further device-specific parameters (e.g. polling mode,
## client mode) can be set in the configuration block
##
## The serial numbers of all GEN3PLUS energy storage systems/batteries start with `410`!
## Each GEN3PLUS device is supplied with a “Monitoring SN:”. This can be found on a
## sticker enclosed with the inverter.
##
[batteries."4100000000000001"]
monitor_sn = 3000000000 # The GEN3PLUS "Monitoring SN:"
node_id = '' # MQTT replacement for devices serial number
suggested_area = '' # suggested installation place for home-assistant
modbus_polling = true # Enable optional MODBUS polling
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
# the next line and configure the fixed IP of your inverter
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
###
### If the proxy mode is configured, commands from TSUN can be sent to the inverter via

View File

@@ -2,26 +2,14 @@
import struct
import logging
from typing import Generator
from itertools import chain
from infos import Infos, Register
class RegisterMap:
__slots__ = ()
map = {
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
0x000927c0: {'reg': Register.CHIP_TYPE},
0x00092f90: {'reg': Register.CHIP_MODEL},
0x00094ae8: {'reg': Register.MAC_ADDR},
0x00095a88: {'reg': Register.TRACE_URL},
0x00095aec: {'reg': Register.LOGGER_URL},
0x0000000a: {'reg': Register.PRODUCT_NAME},
0x00000014: {'reg': Register.MANUFACTURER},
0x0000001e: {'reg': Register.VERSION},
0x00000028: {'reg': Register.SERIAL_NUMBER},
0x00000032: {'reg': Register.EQUIPMENT_MODEL},
0x00013880: {'reg': Register.NO_INPUTS},
0xffffff00: {'reg': Register.INVERTER_CNT},
0xffffff01: {'reg': Register.UNKNOWN_SNR},
0xffffff02: {'reg': Register.UNKNOWN_MSG},
@@ -33,6 +21,100 @@ class RegisterMap:
0xffffff08: {'reg': Register.POLLING_INTERVAL},
0xfffffffe: {'reg': Register.TEST_REG1},
0xffffffff: {'reg': Register.TEST_REG2},
}
map_0e100000 = {
0x00092ba8: {'reg': Register.COLLECTOR_FW_VERSION},
0x000927c0: {'reg': Register.CHIP_TYPE},
0x00092f90: {'reg': Register.CHIP_MODEL},
0x00094ae8: {'reg': Register.MAC_ADDR},
0x00095a88: {'reg': Register.TRACE_URL},
0x00095aec: {'reg': Register.LOGGER_URL},
0x000cfc38: {'reg': Register.CONNECT_COUNT},
0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
0x000c96a8: {'reg': Register.POWER_ON_TIME},
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
}
map_01900000 = {
0x0000000a: {'reg': Register.PRODUCT_NAME},
0x00000014: {'reg': Register.MANUFACTURER},
0x0000001e: {'reg': Register.VERSION},
0x00000046: {'reg': Register.SERIAL_NUMBER},
0x0000005A: {'reg': Register.EQUIPMENT_MODEL},
0x00000064: {'reg': Register.INVERTER_STATUS},
0x00000190: {'reg': Register.EVENT_ALARM},
0x000001f4: {'reg': Register.EVENT_FAULT},
0x00000258: {'reg': Register.EVENT_BF1},
0x000002bc: {'reg': Register.EVENT_BF2},
0x00000320: {'reg': Register.TEST_IVAL_1},
0x000003e8: {'reg': Register.TEST_VAL_0},
0x0000044c: {'reg': Register.TEST_VAL_1}, # DC 1 Inpput Voltage *10
0x000004b0: {'reg': Register.TEST_VAL_2},
0x00000514: {'reg': Register.GRID_VOLTAGE}, # Grid Voltage
0x00000578: {'reg': Register.GRID_CURRENT}, # Grid Current
0x000005dc: {'reg': Register.TEST_VAL_3},
0x00000640: {'reg': Register.GRID_FREQUENCY},
0x000006a4: {'reg': Register.TEST_IVAL_2},
0x00000708: {'reg': Register.TEST_IVAL_3},
0x0000076c: {'reg': Register.TEST_IVAL_4},
0x000007d0: {'reg': Register.TEST_VAL_4}, # DC 2 Input Voltage *10
0x00000834: {'reg': Register.MAX_DESIGNED_POWER},
0x00000898: {'reg': Register.OUTPUT_POWER}, # Grid Power
0x000008fc: {'reg': Register.DAILY_GENERATION}, # Daily Generation
0x00000960: {'reg': Register.TOTAL_GENERATION}, # Total Genration
0x000009c4: {'reg': Register.TEST_IVAL_5},
0x00000a28: {'reg': Register.TEST_VAL_10}, # Isolationsimpedanz Rx
0x00000a8c: {'reg': Register.TEST_VAL_11}, # Isolationsimpedanz Ry
0x00000af0: {'reg': Register.TEST_IVAL_6},
0x000001324: {'reg': Register.PV1_VOLTAGE}, # PV1 Voltage
0x000001388: {'reg': Register.PV1_CURRENT}, # PV1 Current
0x0000013ec: {'reg': Register.PV1_POWER}, # PV1 Power
0x000001450: {'reg': Register.TEST_VAL_5},
0x0000015e0: {'reg': Register.PV2_VOLTAGE}, # PV2 Voltage
0x000001644: {'reg': Register.PV2_CURRENT}, # PV2 Current
0x0000016a8: {'reg': Register.PV2_POWER}, # PV2 Power
0x00000170c: {'reg': Register.TEST_VAL_6},
0x00000189c: {'reg': Register.PV3_VOLTAGE},
0x000001900: {'reg': Register.PV3_CURRENT},
0x000001964: {'reg': Register.PV3_POWER},
0x0000019c8: {'reg': Register.TEST_VAL_7},
0x000001c20: {'reg': Register.TEST_VAL_14},
0x000001c84: {'reg': Register.TEST_VAL_15},
0x000001ce8: {'reg': Register.TEST_VAL_16}, # DC 1 Voltage
0x000001d4c: {'reg': Register.TEST_VAL_17},
0x000001db0: {'reg': Register.TEST_VAL_18},
0x000001e14: {'reg': Register.TEST_IVAL_8},
0x000001e78: {'reg': Register.PV4_VOLTAGE},
0x000001edc: {'reg': Register.PV4_CURRENT},
0x000001f40: {'reg': Register.PV4_POWER},
0x000001fa4: {'reg': Register.TEST_VAL_8},
0x0000020c9: {'reg': Register.TEST_IVAL_9},
0x0000020db: {'reg': Register.TEST_IVAL_10},
0x000002134: {'reg': Register.PV5_VOLTAGE},
0x000002198: {'reg': Register.PV5_CURRENT},
0x0000021fc: {'reg': Register.PV5_POWER},
# 0x000002260: {'reg': Register.TEST_VAL_13},
0x0000023f0: {'reg': Register.PV6_VOLTAGE},
0x000002454: {'reg': Register.PV6_CURRENT},
0x0000024b8: {'reg': Register.PV6_POWER},
# 0x00000251c: {'reg': Register.TEST_VAL_14},
0x000002774: {'reg': Register.TEST_VAL_24},
0x0000027d8: {'reg': Register.TEST_VAL_25},
0x00000283c: {'reg': Register.TEST_VAL_26}, # DC 2 Voltage
0x0000028a0: {'reg': Register.TEST_VAL_27},
0x000002904: {'reg': Register.TEST_VAL_28},
0x000002968: {'reg': Register.TEST_IVAL_11},
0x0000029cc: {'reg': Register.TEST_IVAL_12},
}
map_01900001 = {
0x0000000a: {'reg': Register.PRODUCT_NAME},
0x00000014: {'reg': Register.MANUFACTURER},
0x0000001e: {'reg': Register.VERSION},
0x00000028: {'reg': Register.SERIAL_NUMBER},
0x00000032: {'reg': Register.EQUIPMENT_MODEL},
0x00013880: {'reg': Register.NO_INPUTS},
0x00000640: {'reg': Register.OUTPUT_POWER},
0x000005dc: {'reg': Register.RATED_POWER},
0x00000514: {'reg': Register.INVERTER_TEMP},
@@ -61,12 +143,7 @@ class RegisterMap:
0x000003e8: {'reg': Register.GRID_VOLTAGE},
0x0000044c: {'reg': Register.GRID_CURRENT},
0x000004b0: {'reg': Register.GRID_FREQUENCY},
0x000cfc38: {'reg': Register.CONNECT_COUNT},
0x000c3500: {'reg': Register.SIGNAL_STRENGTH},
0x000c96a8: {'reg': Register.POWER_ON_TIME},
0x000d0020: {'reg': Register.COLLECT_INTERVAL},
0x000cf850: {'reg': Register.DATA_UP_INTERVAL},
0x000c7f38: {'reg': Register.COMMUNICATION_TYPE},
0x00000190: {'reg': Register.EVENT_ALARM},
0x000001f4: {'reg': Register.EVENT_FAULT},
0x00000258: {'reg': Register.EVENT_BF1},
@@ -86,6 +163,18 @@ class RegisterMap:
}
class RegisterSel:
__sensor_map = {
0x0e100000: RegisterMap.map_0e100000,
0x01900000: RegisterMap.map_01900000,
0x01900001: RegisterMap.map_01900001,
}
@classmethod
def get(cls, sensor: int):
return cls.__sensor_map.get(sensor, RegisterMap.map)
class InfosG3(Infos):
__slots__ = ()
@@ -101,18 +190,27 @@ class InfosG3(Infos):
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():
sensor = self.get_db_value(Register.SENSOR_LIST)
if "01900000" == sensor:
items = RegisterMap.map_01900000.items()
elif "01900001" == sensor:
items = RegisterMap.map_01900001.items()
else:
items = {}
for _, row in chain(RegisterMap.map_0e100000.items(), items):
reg = row['reg']
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, node_id: str = '') -> \
def parse(self, buf, ind=0, sensor: int = 0, node_id: str = '') -> \
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'''
reg_map = RegisterSel.get(sensor)
result = struct.unpack_from('!l', buf, ind)
elms = result[0]
i = 0
@@ -120,11 +218,11 @@ class InfosG3(Infos):
while i < elms:
result = struct.unpack_from('!lB', buf, ind)
addr = result[0]
if addr not in RegisterMap.map:
if addr not in reg_map:
row = None
info_id = -1
else:
row = RegisterMap.map[addr]
row = reg_map[addr]
info_id = row['reg']
data_type = result[1]
ind += 5
@@ -192,3 +290,6 @@ class InfosG3(Infos):
if update:
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
f' {result}{unit}')
logging.log(level, f'[{node_id}] GEN3: {name} :'
f' {result}{unit}')

View File

@@ -75,6 +75,7 @@ class Talent(Message):
0x87: self.get_modbus_log_lvl,
0x04: logging.INFO,
}
self.sensor_list = 0
'''
Our puplic methods
@@ -98,13 +99,9 @@ class Talent(Message):
if serial_no in inverters:
inv = inverters[serial_no]
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
self._set_config_parms(inv)
self.db.set_pv_module_details(inv)
if self.mb:
self.mb.set_node_id(self.node_id)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
else:
self.node_id = ''
self.sug_area = ''
@@ -175,12 +172,17 @@ class Talent(Message):
def mb_timout_cb(self, exp_cnt):
self.mb_timer.start(self.mb_timeout)
if self.mb_scan:
self._send_modbus_scan()
return
if 2 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x2000,
96, logging.DEBUG)
else:
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000,
48, logging.DEBUG)
def _init_new_client_conn(self) -> bool:
contact_name = self.contact_name
@@ -442,7 +444,7 @@ class Talent(Message):
logger.debug(f'time: {timestamp:08x}')
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
# "%Y-%m-%d %H:%M:%S")}')
return msg_hdr_len, timestamp
return msg_hdr_len, data_id, timestamp
def msg_collector_data(self):
if self.ctrl.is_ind():
@@ -479,21 +481,51 @@ class Talent(Message):
self.forward()
def __process_data(self, ignore_replay: bool):
msg_hdr_len, ts = self.parse_msg_header()
if ignore_replay:
def __build_model_name(self):
db = self.db
model = db.get_db_value(Register.EQUIPMENT_MODEL, None)
if model:
return
max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
if max_pow == 3000:
model = f'TSOL-MS{max_pow}'
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
self.db.set_db_def_value(Register.MANUFACTURER, 'TSUN')
self.db.set_db_def_value(Register.NO_INPUTS, 4)
def __process_data(self, inv_data: bool):
msg_hdr_len, data_id, ts = self.parse_msg_header()
if inv_data:
# handle register mapping
if 0 == self.sensor_list:
self.sensor_list = data_id
self.db.set_db_def_value(Register.SENSOR_LIST,
f"{self.sensor_list:08x}")
logging.debug(f"Use sensor-list: {self.sensor_list:#08x}"
f" for '{self.unique_id}'")
if data_id != self.sensor_list:
logging.warning(f'Unexpected Sensor-List:{data_id:08x}'
f' (!={self.sensor_list:08x})')
# ignore replays for inverter data
age = self._utc() - self._utcfromts(ts)
age = age/(3600*24)
logger.debug(f"Age: {age} days")
if age > 1:
if age > 1: # is a replay?
return
inv_update = False
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
+ msg_hdr_len, self.node_id):
+ msg_hdr_len, data_id, self.node_id):
if update:
if key == 'inverter':
inv_update = True
self._set_mqtt_timestamp(key, self._utcfromts(ts))
self.new_data[key] = True
if inv_update:
self.__build_model_name()
def msg_ota_update(self):
if self.ctrl.is_req():
self.inc_counter('OTA_Start_Msg')
@@ -554,6 +586,9 @@ class Talent(Message):
logger.warning('Unknown Message')
self.inc_counter('Unknown_Msg')
return
if (self.mb_scan):
modbus_msg_len = self.data_len - hdr_len
self._dump_modbus_scan(data, hdr_len, modbus_msg_len)
for key, update, _ in self.mb.recv_resp(self.db, data[
hdr_len:]):

View File

@@ -1,9 +1,25 @@
from typing import Generator
from itertools import chain
from infos import Infos, Register, ProxyMode, Fmt
class RegisterFunc:
@staticmethod
def prod_sum(info: Infos, arr: dict) -> None | int:
result = 0
for sum in arr:
prod = 1
for factor in sum:
val = info.get_db_value(factor)
if val is None:
return None
prod = prod * val
result += prod
return result
class RegisterMap:
# make the class read/only by using __slots__
__slots__ = ()
@@ -32,7 +48,8 @@ class RegisterMap:
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
}
map_02b0 = {
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
@@ -110,6 +127,69 @@ class RegisterMap:
0xffffff02: {'reg': Register.POLLING_INTERVAL},
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
}
map_3026 = {
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
0x42010030: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage
0x42010032: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current
0x42010034: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage
0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current
0x42010038: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501
0x4201003a: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x4201003c: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501 MPTT-1 Status?
0x4201003e: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501 MPTT-2 Status?
0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x42010044: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent
0x42010046: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010048: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201004a: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201004c: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201004e: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010050: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010052: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010054: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010056: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010058: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201005a: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201005c: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x4201005e: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010060: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010062: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010064: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x42010066: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501
0x42010068: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501
0x4201006a: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501
0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x42010070: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501, state of output value 0 or 1
0x42010072: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501 controller temp
0x42010074: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501, control input 0..2048
0x42010076: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501
0x42010078: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501
'calc': {
1: {'reg': Register.BATT_PV_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
'params': [[Register.BATT_PV1_VOLT, Register.BATT_PV1_CUR],
[Register.BATT_PV2_VOLT, Register.BATT_PV2_CUR]]},
2: {'reg': Register.BATT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
'params': [[Register.BATT_VOLT, Register.BATT_CUR]]},
3: {'reg': Register.BATT_OUT_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501
'params': [[Register.BATT_OUT_VOLT, Register.BATT_OUT_CUR]]},
}
}
class RegisterSel:
__sensor_map = {
0x02b0: RegisterMap.map_02b0,
0x3026: RegisterMap.map_3026,
}
@classmethod
def get(cls, sensor: int):
return cls.__sensor_map.get(sensor, RegisterMap.map)
class InfosG3P(Infos):
@@ -144,7 +224,22 @@ class InfosG3P(Infos):
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():
sensor = self.get_db_value(Register.SENSOR_LIST)
if "3026" == sensor:
reg_map = RegisterMap.map_3026
elif "02b0" == sensor:
reg_map = RegisterMap.map_02b0
else:
reg_map = {}
items = reg_map.items()
if 'calc' in reg_map:
virt = reg_map['calc'].items()
else:
virt = {}
for idx, row in chain(RegisterMap.map.items(), items, virt):
if 'calc' == idx:
continue
info_id = row['reg']
if self.__hide_topic(row):
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
@@ -153,13 +248,17 @@ class InfosG3P(Infos):
if res:
yield res
def parse(self, buf, msg_type: int, rcv_ftype: int, node_id: str = '') \
def parse(self, buf, msg_type: int, rcv_ftype: int,
sensor: int = 0, node_id: str = '') \
-> 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():
reg_map = RegisterSel.get(sensor)
for idx, row in reg_map.items():
if 'calc' == idx:
continue
addr = idx & 0xffff
ftype = (idx >> 16) & 0xff
mtype = (idx >> 24) & 0xff
@@ -169,23 +268,36 @@ class InfosG3P(Infos):
continue
info_id = row['reg']
result = Fmt.get_value(buf, addr, row)
yield from self.__update_val(node_id, "GEN3PLUS", info_id, result)
yield from self.calc(sensor, node_id)
keys, level, unit, must_incr = self._key_obj(info_id)
def calc(self, sensor: int = 0, node_id: str = '') \
-> Generator[tuple[str, bool], None, None]:
'''calculate meta values from the
stored values in Infos.db
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
sensor: sensor_list number
node_id: id-string for the node'''
reg_map = RegisterSel.get(sensor)
if 'calc' in reg_map:
for row in reg_map['calc'].values():
info_id = row['reg']
result = row['func'](self, row['params'])
yield from self.__update_val(node_id, "CALC", info_id, result)
def __update_val(self, node_id, source: str, info_id, result):
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
if update:
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
self.tracer.log(level, f'[{node_id}] {source}: {name}'
f' : {result}{unit}')
def build(self, len, msg_type: int, rcv_ftype: int):
def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0):
buf = bytearray(len)
for idx, row in RegisterMap.map.items():
for idx, row in RegisterSel.get(sensor).items():
addr = idx & 0xffff
ftype = (idx >> 16) & 0xff
mtype = (idx >> 24) & 0xff

View File

@@ -103,7 +103,7 @@ class SolarmanEmu(SolarmanBase):
self.data_timer.start(self.data_up_inv)
_len = 420
ftype = 1
build_msg = self.db.build(_len, 0x42, ftype)
build_msg = self.db.build(_len, 0x42, ftype, 0x02b0)
self._build_header(0x4210)
self.ifc.tx_add(

View File

@@ -2,8 +2,10 @@ import struct
import logging
import time
import asyncio
from itertools import chain
from datetime import datetime
from proxy import Proxy
from async_ifc import AsyncIfc
from messages import hex_dump_memory, Message, State
from cnf.config import Config
@@ -245,6 +247,7 @@ class SolarmanBase(Message):
class SolarmanV5(SolarmanBase):
AT_CMD = 1
MB_RTU_CMD = 2
AT_CMD_RSP = 8
MB_CLIENT_DATA_UP = 30
'''Data up time in client mode'''
HDR_FMT = '<BLLL'
@@ -290,7 +293,7 @@ class SolarmanV5(SolarmanBase):
# MODbus or AT cmd
0x4510: self.msg_command_req, # from server
0x1510: self.msg_command_rsp, # from inverter
# 0x0510: self.msg_command_rsp, # from inverter
0x0510: self.msg_command_rsp, # from inverter
}
self.log_lvl = {
@@ -321,6 +324,8 @@ class SolarmanV5(SolarmanBase):
self.at_acl = g3p_cnf['at_acl']
self.sensor_list = 0
self.mb_regs = [{'addr': 0x3000, 'len': 48},
{'addr': 0x2000, 'len': 96}]
'''
Our puplic methods
@@ -356,7 +361,16 @@ class SolarmanV5(SolarmanBase):
self.new_data['controller'] = True
self.state = State.up
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
if self.mb_scan:
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
self.mb_start_reg, self.mb_bytes,
logging.INFO)
else:
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[0]['addr'],
self.mb_regs[0]['len'], logging.DEBUG)
self.mb_timer.start(self.mb_timeout)
def new_state_up(self):
@@ -376,14 +390,25 @@ class SolarmanV5(SolarmanBase):
self.ifc.fwd_add(build_msg)
self.ifc.fwd_add(struct.pack('<BB', 0, 0x15)) # crc & stop
def __set_config_parms(self, inv: dict):
def _set_config_parms(self, inv: dict, serial_no: str = ""):
'''init connection with params from the configuration'''
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
super()._set_config_parms(inv)
snr = serial_no[:3]
if '410' == snr:
self.db.set_db_def_value(Register.EQUIPMENT_MODEL,
'TSOL-DC1000')
self.sensor_list = inv['sensor_list']
if self.mb:
self.mb.set_node_id(self.node_id)
if 0 == self.sensor_list:
if '410' == snr:
self.sensor_list = 0x3026
self.mb_regs = [{'addr': 0x0000, 'len': 45}]
else:
self.sensor_list = 0x02b0
self.db.set_db_def_value(Register.SENSOR_LIST,
f"{self.sensor_list:04x}")
logging.debug(f"Use sensor-list: {self.sensor_list:#04x}"
f" for '{serial_no}'")
def _set_serial_no(self, snr: int):
'''check the serial number and configure the inverter connection'''
@@ -392,13 +417,14 @@ class SolarmanV5(SolarmanBase):
logger.debug(f'SerialNo: {serial_no}')
else:
inverters = Config.get('inverters')
batteries = Config.get('batteries')
# logger.debug(f'Inverters: {inverters}')
for key, inv in inverters.items():
for key, inv in chain(inverters.items(), batteries.items()):
# logger.debug(f'key: {key} -> {inv}')
if (type(inv) is dict and 'monitor_sn' in inv
and inv['monitor_sn'] == snr):
self.__set_config_parms(inv)
self._set_config_parms(inv, key)
self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
@@ -411,9 +437,11 @@ class SolarmanV5(SolarmanBase):
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
logging.error(f"Ignore message from unknow inverter with Monitoring-SN: {serial_no})!\n" # noqa: E501
" !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501
return
logger.warning(f'SerialNo {serial_no} not known but accepted!')
logging.warning(f"Monitoring-SN: {serial_no} not configured but accepted!" # noqa: E501
" !!Check the 'monitor_sn' setting in your configuration!!") # noqa: E501
self.unique_id = serial_no
@@ -459,12 +487,18 @@ class SolarmanV5(SolarmanBase):
def mb_timout_cb(self, exp_cnt):
self.mb_timer.start(self.mb_timeout)
if self.mb_scan:
self._send_modbus_scan()
else:
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[0]['addr'],
self.mb_regs[0]['len'], logging.INFO)
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
if 1 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
if 1 == (exp_cnt % 30) and len(self.mb_regs) > 1:
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[1]['addr'],
self.mb_regs[1]['len'], logging.INFO)
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
@@ -482,7 +516,7 @@ class SolarmanV5(SolarmanBase):
node_id = self.node_id
key = 'at_resp'
logger.info(f'{key}: {data_json}')
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
await Proxy.mqtt.publish(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
return
self.forward_at_cmd_resp = False
@@ -516,11 +550,11 @@ class SolarmanV5(SolarmanBase):
logger.info(f'Model: {model}')
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
def __process_data(self, ftype, ts):
def __process_data(self, ftype, ts, sensor=0):
inv_update = False
msg_type = self.control >> 8
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype,
self.node_id):
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type,
ftype, sensor, self.node_id):
if update:
if key == 'inverter':
inv_update = True
@@ -581,7 +615,7 @@ class SolarmanV5(SolarmanBase):
else:
ts = None
self.__process_data(ftype, ts)
self.__process_data(ftype, ts, sensor)
self.__forward_msg()
self.__send_ack_rsp(0x1210, ftype)
self.new_state_up()
@@ -626,7 +660,7 @@ class SolarmanV5(SolarmanBase):
def publish_mqtt(self, key, data): # pragma: no cover
asyncio.ensure_future(
self.mqtt.publish(key, data))
Proxy.mqtt.publish(key, data))
def get_cmd_rsp_log_lvl(self) -> int:
ftype = self.ifc.rx_peek()[self.header_len]
@@ -644,29 +678,39 @@ class SolarmanV5(SolarmanBase):
data = self.ifc.rx_peek()[self.header_len:
self.header_len+self.data_len]
ftype = data[0]
if ftype == self.AT_CMD:
if ftype == self.AT_CMD or \
ftype == self.AT_CMD_RSP:
if not self.forward_at_cmd_resp:
data_json = data[14:].decode("utf-8")
node_id = self.node_id
key = 'at_resp'
logger.info(f'{key}: {data_json}')
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
return
elif ftype == self.MB_RTU_CMD:
self.__modbus_command_rsp(data)
return
self.__forward_msg()
def __parse_modbus_rsp(self, data):
def __parse_modbus_rsp(self, data, modbus_msg_len):
inv_update = False
self.modbus_elms = 0
if (self.mb_scan):
self._dump_modbus_scan(data, 14, modbus_msg_len)
ts = self._timestamp()
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
self.modbus_elms += 1
if update:
if key == 'inverter':
inv_update = True
self._set_mqtt_timestamp(key, self._timestamp())
self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
for key, update in self.db.calc(self.sensor_list, self.node_id):
if update:
self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
return inv_update
def __modbus_command_rsp(self, data):
@@ -676,7 +720,7 @@ class SolarmanV5(SolarmanBase):
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
if valid == 1 and modbus_msg_len > 4:
# logger.info(f'first byte modbus:{data[14]}')
inv_update = self.__parse_modbus_rsp(data)
inv_update = self.__parse_modbus_rsp(data, modbus_msg_len)
if inv_update:
self.__build_model_name()

View File

@@ -121,6 +121,94 @@ class Register(Enum):
TS_INPUT = 600
TS_GRID = 601
TS_TOTAL = 602
BATT_PV1_VOLT = 1000
BATT_PV1_CUR = 1001
BATT_PV2_VOLT = 1002
BATT_PV2_CUR = 1003
BATT_38 = 1004
BATT_TOTAL_GEN = 1005
BATT_STATUS_1 = 1006
BATT_STATUS_2 = 1007
BATT_VOLT = 1010
BATT_CUR = 1011
BATT_SOC = 1012
BATT_CELL1_VOLT = 1013
BATT_CELL2_VOLT = 1014
BATT_CELL3_VOLT = 1015
BATT_CELL4_VOLT = 1016
BATT_CELL5_VOLT = 1017
BATT_CELL6_VOLT = 1018
BATT_CELL7_VOLT = 1019
BATT_CELL8_VOLT = 1020
BATT_CELL9_VOLT = 1021
BATT_CELL10_VOLT = 1022
BATT_CELL11_VOLT = 1023
BATT_CELL12_VOLT = 1024
BATT_CELL13_VOLT = 1025
BATT_CELL14_VOLT = 1026
BATT_CELL15_VOLT = 1027
BATT_CELL16_VOLT = 1028
BATT_TEMP_1 = 1029
BATT_TEMP_2 = 1030
BATT_TEMP_3 = 1031
BATT_OUT_VOLT = 1032
BATT_OUT_CUR = 1033
BATT_OUT_STATUS = 1034
BATT_TEMP_4 = 1035
BATT_74 = 1036
BATT_76 = 1037
BATT_78 = 1038
BATT_PV_PWR = 1040
BATT_PWR = 1041
BATT_OUT_PWR = 1042
TEST_VAL_0 = 2000
TEST_VAL_1 = 2001
TEST_VAL_2 = 2002
TEST_VAL_3 = 2003
TEST_VAL_4 = 2004
TEST_VAL_5 = 2005
TEST_VAL_6 = 2006
TEST_VAL_7 = 2007
TEST_VAL_8 = 2008
TEST_VAL_9 = 2009
TEST_VAL_10 = 2010
TEST_VAL_11 = 2011
TEST_VAL_12 = 2012
TEST_VAL_13 = 2013
TEST_VAL_14 = 2014
TEST_VAL_15 = 2015
TEST_VAL_16 = 2016
TEST_VAL_17 = 2017
TEST_VAL_18 = 2018
TEST_VAL_19 = 2019
TEST_VAL_20 = 2020
TEST_VAL_21 = 2021
TEST_VAL_22 = 2022
TEST_VAL_23 = 2023
TEST_VAL_24 = 2024
TEST_VAL_25 = 2025
TEST_VAL_26 = 2026
TEST_VAL_27 = 2027
TEST_VAL_28 = 2028
TEST_VAL_29 = 2029
TEST_VAL_30 = 2030
TEST_VAL_31 = 2031
TEST_VAL_32 = 2032
TEST_IVAL_1 = 2041
TEST_IVAL_2 = 2042
TEST_IVAL_3 = 2043
TEST_IVAL_4 = 2044
TEST_IVAL_5 = 2045
TEST_IVAL_6 = 2046
TEST_IVAL_7 = 2047
TEST_IVAL_8 = 2048
TEST_IVAL_9 = 2049
TEST_IVAL_10 = 2050
TEST_IVAL_11 = 2051
TEST_IVAL_12 = 2052
VALUE_1 = 9000
TEST_REG1 = 10000
TEST_REG2 = 10001
@@ -131,7 +219,10 @@ class Fmt:
def get_value(buf: bytes, idx: int, row: dict):
'''Get a value from buf and interpret as in row defined'''
fmt = row['fmt']
res = struct.unpack_from(fmt, buf, idx)
try:
res = struct.unpack_from(fmt, buf, idx)
except Exception:
return None
result = res[0]
if isinstance(result, (bytearray, bytes)):
result = result.decode().split('\x00')[0]
@@ -230,6 +321,7 @@ class Infos:
LIGHTNING = 'mdi:lightning-bolt'
COUNTER = 'mdi:counter'
GAUGE = 'mdi:gauge'
POWER = 'mdi:power'
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
SOLAR_POWER = 'mdi:solar-power'
WIFI = 'mdi:wifi'
@@ -266,6 +358,7 @@ class Infos:
__info_devs = {
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION, 'mac': Register.MAC_ADDR, 'sn': Register.COLLECTOR_SNR}, # noqa: E501
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
@@ -273,11 +366,18 @@ class Infos:
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501
'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501
'batterie': {'via': 'controller', 'name': 'Batterie', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION, 'sn': Register.SERIAL_NUMBER}, # noqa: E501
'bat_inp_pv1': {'via': 'batterie', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
'bat_inp_pv2': {'via': 'batterie', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER}, # 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
__work_mode_val_tpl = "{%set mode = ['Normal-Mode', 'Aging-Mode', 'ATE-Mode', 'Shielding GFDI', 'DTU-Mode'] %}{{mode[value_json['Work_Mode']|int(0)]|default(value_json['Work_Mode'])}}" # noqa: E501
__status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
__mppt1_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_1']|int(0)]|default(value_json['Status_1'])}}" # noqa: E501
__mppt2_status_type_val_tpl = "{%set mppt_status = ['Locked', 'On', 'Off'] %}{{mppt_status[value_json['Status_2']|int(0)]|default(value_json['Status_2'])}}" # noqa: E501
__out_status_type_val_tpl = "{%set out_status = ['Off', 'On'] %}{{out_status[value_json['out']['Out_Status']|int(0)]|default(value_json['out']['Out_Status'])}}" # noqa: E501
__rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
__designed_power_val_tpl = '''
{% if 'Max_Designed_Power' in value_json and
@@ -428,7 +528,7 @@ class Infos:
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': 'mdi:power', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.WORK_MODE: {'name': ['inverter', 'Work_Mode'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'work_mode_', 'name': 'Work Mode', 'val_tpl': __work_mode_val_tpl, 'icon': POWER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.INPUT_COEFFICIENT: {'name': ['inverter', 'Input_Coefficient'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'input_coef_', 'val_tpl': __input_coef_val_tpl, 'name': 'Input Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -476,7 +576,7 @@ class Infos:
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': POWER}}, # noqa: E501
Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -518,15 +618,15 @@ class Infos:
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': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
# controller:
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': 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': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.INFO, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.INFO, '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': WIFI}}, # noqa: E501
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.INFO, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.SSID: {'name': ['controller', 'WiFi_SSID'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -536,6 +636,93 @@ class Infos:
Register.PROD_COMPL_TYPE: {'name': ['other', 'Prod_Compliance_Type'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.BATT_PV1_VOLT: {'name': ['batterie', 'pv1', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_PV1_CUR: {'name': ['batterie', 'pv1', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_PV2_VOLT: {'name': ['batterie', 'pv2', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_PV2_CUR: {'name': ['batterie', 'pv2', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'bat_inp_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_38: {'name': ['batterie', 'Reg_38'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_38_', 'fmt': FMT_FLOAT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_TOTAL_GEN: {'name': ['batterie', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'batterie', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.BATT_STATUS_1: {'name': ['batterie', 'Status_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status1_', 'name': 'MPPT-1 Status', 'val_tpl': __mppt1_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
Register.BATT_STATUS_2: {'name': ['batterie', 'Status_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'status2_', 'name': 'MPPT-2 Status', 'val_tpl': __mppt2_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
Register.BATT_VOLT: {'name': ['batterie', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CUR: {'name': ['batterie', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_bat_', 'fmt': FMT_FLOAT, 'name': 'Batterie Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_SOC: {'name': ['batterie', 'SOC'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'batterie', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'soc_', 'fmt': FMT_FLOAT, 'name': 'State of Charge (SOC)', 'icon': 'mdi:battery-90'}}, # noqa: E501
Register.BATT_CELL1_VOLT: {'name': ['batterie', 'Cell', 'Volt1'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell1_', 'val_tpl': "{{ (value_json['Cell']['Volt1'] | float)}}", 'name': 'Cell-01 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL3_VOLT: {'name': ['batterie', 'Cell', 'Volt3'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell3_', 'val_tpl': "{{ (value_json['Cell']['Volt2'] | float)}}", 'name': 'Cell-03 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL4_VOLT: {'name': ['batterie', 'Cell', 'Volt4'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell4_', 'val_tpl': "{{ (value_json['Cell']['Volt3'] | float)}}", 'name': 'Cell-04 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL2_VOLT: {'name': ['batterie', 'Cell', 'Volt2'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell2_', 'val_tpl': "{{ (value_json['Cell']['Volt4'] | float)}}", 'name': 'Cell-02 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL5_VOLT: {'name': ['batterie', 'Cell', 'Volt5'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell5_', 'val_tpl': "{{ (value_json['Cell']['Volt5'] | float)}}", 'name': 'Cell-05 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL6_VOLT: {'name': ['batterie', 'Cell', 'Volt6'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell6_', 'val_tpl': "{{ (value_json['Cell']['Volt6'] | float)}}", 'name': 'Cell-06 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL7_VOLT: {'name': ['batterie', 'Cell', 'Volt7'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell7_', 'val_tpl': "{{ (value_json['Cell']['Volt7'] | float)}}", 'name': 'Cell-07 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL8_VOLT: {'name': ['batterie', 'Cell', 'Volt8'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell8_', 'val_tpl': "{{ (value_json['Cell']['Volt8'] | float)}}", 'name': 'Cell-08 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL9_VOLT: {'name': ['batterie', 'Cell', 'Volt9'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell9_', 'val_tpl': "{{ (value_json['Cell']['Volt9'] | float)}}", 'name': 'Cell-09 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL10_VOLT: {'name': ['batterie', 'Cell', 'Volt10'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell10_', 'val_tpl': "{{ (value_json['Cell']['Volt10'] | float)}}", 'name': 'Cell-10 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL11_VOLT: {'name': ['batterie', 'Cell', 'Volt11'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell11_', 'val_tpl': "{{ (value_json['Cell']['Volt11'] | float)}}", 'name': 'Cell-11 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL12_VOLT: {'name': ['batterie', 'Cell', 'Volt12'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell12_', 'val_tpl': "{{ (value_json['Cell']['Volt12'] | float)}}", 'name': 'Cell-12 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL13_VOLT: {'name': ['batterie', 'Cell', 'Volt13'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell13_', 'val_tpl': "{{ (value_json['Cell']['Volt13'] | float)}}", 'name': 'Cell-13 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL14_VOLT: {'name': ['batterie', 'Cell', 'Volt14'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell14_', 'val_tpl': "{{ (value_json['Cell']['Volt14'] | float)}}", 'name': 'Cell-14 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL15_VOLT: {'name': ['batterie', 'Cell', 'Volt15'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell15_', 'val_tpl': "{{ (value_json['Cell']['Volt15'] | float)}}", 'name': 'Cell-15 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_CELL16_VOLT: {'name': ['batterie', 'Cell', 'Volt16'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_cell16_', 'val_tpl': "{{ (value_json['Cell']['Volt16'] | float)}}", 'name': 'Cell-16 Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_TEMP_1: {'name': ['batterie', 'Temp_1'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_1_', 'fmt': FMT_INT, 'name': 'Batterie Temp-1', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_TEMP_2: {'name': ['batterie', 'Temp_2'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_2_', 'fmt': FMT_INT, 'name': 'Batterie Temp-2', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_TEMP_3: {'name': ['batterie', 'Temp_3'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_3_', 'fmt': FMT_INT, 'name': 'Batterie Temp-3', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_OUT_VOLT: {'name': ['batterie', 'out', 'Voltage'], 'level': logging.INFO, 'unit': 'V', 'ha': {'dev': 'batterie', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'val_tpl': "{{ (value_json['out']['Voltage'] | float)}}", 'name': 'Output Voltage', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_OUT_CUR: {'name': ['batterie', 'out', 'Current'], 'level': logging.INFO, 'unit': 'A', 'ha': {'dev': 'batterie', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'val_tpl': "{{ (value_json['out']['Current'] | float)}}", 'name': 'Output Current', 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_OUT_STATUS: {'name': ['batterie', 'out', 'Out_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'out_status_', 'name': 'Output Status', 'val_tpl': __out_status_type_val_tpl, 'icon': POWER}}, # noqa: E501
Register.BATT_TEMP_4: {'name': ['batterie', 'Controller_Temp'], 'level': logging.INFO, 'unit': '°C', 'ha': {'dev': 'batterie', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_4_', 'fmt': FMT_INT, 'name': 'Ctrl Temperature'}}, # noqa: E501
Register.BATT_74: {'name': ['batterie', 'Reg_74'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_74_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_76: {'name': ['batterie', 'Reg_76'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_76_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_78: {'name': ['batterie', 'Reg_78'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'batt_78_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.BATT_PV_PWR: {'name': ['batterie', 'PV_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'pv_power_', 'fmt': FMT_INT, 'name': 'PV Power'}}, # noqa: E501
Register.BATT_PWR: {'name': ['batterie', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_', 'fmt': FMT_INT, 'name': 'Batterie Power'}}, # noqa: E501
Register.BATT_OUT_PWR: {'name': ['batterie', 'out', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'batterie', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'val_tpl': "{{ (value_json['out']['Power'] | int)}}", 'name': 'Output Power'}}, # noqa: E501
Register.TEST_VAL_0: {'name': ['input', 'Val_0'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_0_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_1: {'name': ['input', 'Val_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_2: {'name': ['input', 'Val_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_3: {'name': ['input', 'Val_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_4: {'name': ['input', 'Val_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_5: {'name': ['input', 'Val_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_6: {'name': ['input', 'Val_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_7: {'name': ['input', 'Val_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_8: {'name': ['input', 'Val_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_9: {'name': ['input', 'Val_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_10: {'name': ['input', 'Val_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_11: {'name': ['input', 'Val_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_12: {'name': ['input', 'Val_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_13: {'name': ['input', 'Val_13'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_13_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_14: {'name': ['input', 'Val_14'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_14_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_15: {'name': ['input', 'Val_15'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_15_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_16: {'name': ['input', 'Val_16'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_16_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_17: {'name': ['input', 'Val_17'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_17_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_18: {'name': ['input', 'Val_18'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_18_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_19: {'name': ['input', 'Val_19'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_19_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_20: {'name': ['input', 'Val_20'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_20_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_21: {'name': ['input', 'Val_21'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_21_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_22: {'name': ['input', 'Val_22'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_22_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_23: {'name': ['input', 'Val_23'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_23_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_24: {'name': ['input', 'Val_24'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_24_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_25: {'name': ['input', 'Val_25'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_25_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_26: {'name': ['input', 'Val_26'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_26_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_27: {'name': ['input', 'Val_27'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_27_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_28: {'name': ['input', 'Val_28'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_28_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_29: {'name': ['input', 'Val_29'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_29_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_30: {'name': ['input', 'Val_30'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_30_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_31: {'name': ['input', 'Val_31'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_31_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_VAL_32: {'name': ['input', 'Val_32'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'val_32_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_1: {'name': ['input', 'iVal_1'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_1_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_2: {'name': ['input', 'iVal_2'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_2_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_3: {'name': ['input', 'iVal_3'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_3_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_4: {'name': ['input', 'iVal_4'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_4_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_5: {'name': ['input', 'iVal_5'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_5_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_6: {'name': ['input', 'iVal_6'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_6_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_7: {'name': ['input', 'iVal_7'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_7_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_8: {'name': ['input', 'iVal_8'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_8_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_9: {'name': ['input', 'iVal_9'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_9_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_10: {'name': ['input', 'iVal_10'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_10_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_11: {'name': ['input', 'iVal_11'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_11_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.TEST_IVAL_12: {'name': ['input', 'iVal_12'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'ival_12_', 'fmt': FMT_INT, 'ent_cat': 'diagnostic'}}, # noqa: E501
}
@property

View File

@@ -151,6 +151,8 @@ class InverterBase(InverterIfc, Proxy):
# home assistant has changed the status back to online
try:
if (('inverter' in stream.new_data and stream.new_data['inverter'])
or ('batterie' in stream.new_data and
stream.new_data['batterie'])
or ('collector' in stream.new_data and
stream.new_data['collector'])
or self.mqtt.ha_restarts != self.__ha_restarts):

View File

@@ -67,10 +67,10 @@ formatter=file_formatter
args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups)
[formatter_console_formatter]
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
datefmt='%Y-%m-%d %H:%M:%S
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s
datefmt=%Y-%m-%d %H:%M:%S
[formatter_file_formatter]
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
datefmt='%Y-%m-%d %H:%M:%S
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s
datefmt=%Y-%m-%d %H:%M:%S

View File

@@ -117,6 +117,11 @@ class Message(ProtocolIfc):
self.mb_first_timeout = self.MB_START_TIMEOUT
'''timer value for next Modbus polling request'''
self.modbus_polling = False
self.mb_start_reg = 0
self.mb_step = 0
self.mb_bytes = 0
self.mb_inv_no = 1
self.mb_scan = False
@property
def node_id(self):
@@ -135,6 +140,25 @@ class Message(ProtocolIfc):
# to our _recv_buffer
return # pragma: no cover
def _set_config_parms(self, inv: dict):
'''init connection with params from the configuration'''
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
if 'modbus_scanning' in inv:
scan = inv['modbus_scanning']
self.mb_scan = True
self.mb_start_reg = scan['start']
self.mb_step = scan['step']
self.mb_bytes = scan['bytes']
if 'client_mode' in inv:
self.mb_start_reg = scan['start']
else:
self.mb_start_reg = scan['start'] - scan['step']
self.mb_start_reg &= 0xffff
if self.mb:
self.mb.set_node_id(self.node_id)
def _set_mqtt_timestamp(self, key, ts: float | None):
if key not in self.new_data or \
not self.new_data[key]:
@@ -160,15 +184,39 @@ class Message(ProtocolIfc):
to = self.MAX_DEF_IDLE_TIME
return to
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
def _send_modbus_cmd(self, dev_id, func, addr, val, log_lvl) -> None:
if self.state != State.up:
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
' as the state is not UP')
return
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
self.mb.build_msg(dev_id, func, addr, val, log_lvl)
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
self._send_modbus_cmd(func, addr, val, log_lvl)
self._send_modbus_cmd(Modbus.INV_ADDR, func, addr, val, log_lvl)
def _send_modbus_scan(self):
self.mb_start_reg += self.mb_step
if self.mb_start_reg > 0xffff:
self.mb_start_reg = self.mb_start_reg & 0xffff
self.mb_inv_no += 1
logging.info(f"Next Round: inv:{self.mb_inv_no}"
f" reg:{self.mb_start_reg:04x}")
if (self.mb_start_reg & 0xfffc) % 0x80 == 0:
logging.info(f"[{self.node_id}] Scan info: "
f"inv:{self.mb_inv_no}"
f" reg:{self.mb_start_reg:04x}")
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
self.mb_start_reg, self.mb_bytes,
logging.INFO)
def _dump_modbus_scan(self, data, hdr_len, modbus_msg_len):
if (data[hdr_len] == self.mb_inv_no and
data[hdr_len+1] == Modbus.READ_REGS):
logging.info(f'[{self.node_id}] Valid MODBUS data '
f'(reg: 0x{self.mb.last_reg:04x}):')
hex_dump_memory(logging.INFO, 'Valid MODBUS data '
f'(reg: 0x{self.mb.last_reg:04x}):',
data[hdr_len:], modbus_msg_len)
'''
Our puplic methods

View File

@@ -37,6 +37,45 @@ class Modbus():
__crc_tab = []
mb_reg_mapping = {
0x0000: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
0x0008: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage
0x0009: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current
0x000a: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage
0x000b: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current
0x000c: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501
0x000d: {'reg': Register.BATT_TOTAL_GEN, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x000e: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501
0x000f: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501
0x0010: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0011: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0012: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent
0x0013: {'reg': Register.BATT_CELL1_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0014: {'reg': Register.BATT_CELL2_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0015: {'reg': Register.BATT_CELL3_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0016: {'reg': Register.BATT_CELL4_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0017: {'reg': Register.BATT_CELL5_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0018: {'reg': Register.BATT_CELL6_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0019: {'reg': Register.BATT_CELL7_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001a: {'reg': Register.BATT_CELL8_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001b: {'reg': Register.BATT_CELL9_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001c: {'reg': Register.BATT_CELL10_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001d: {'reg': Register.BATT_CELL11_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001e: {'reg': Register.BATT_CELL12_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x001f: {'reg': Register.BATT_CELL13_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0020: {'reg': Register.BATT_CELL14_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0021: {'reg': Register.BATT_CELL15_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0022: {'reg': Register.BATT_CELL16_VOLT, 'fmt': '!h', 'ratio': 0.001}, # noqa: E501
0x0023: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501
0x0024: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501
0x0025: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501
0x0026: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0027: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0028: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!h'}, # noqa: E501
0x0029: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E501
0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501
0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501
0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},

View File

@@ -1,6 +1,7 @@
import logging
import traceback
import asyncio
from itertools import chain
from cnf.config import Config
from gen3plus.inverter_g3p import InverterG3P
@@ -42,14 +43,15 @@ class ModbusTcp():
self.tim_restart = tim_restart
inverters = Config.get('inverters')
batteries = Config.get('batteries')
# logging.info(f'Inverters: {inverters}')
for inv in inverters.values():
for _, inv in chain(inverters.items(), batteries.items()):
if (type(inv) is dict
and 'monitor_sn' in inv
and 'client_mode' in inv):
client = inv['client_mode']
logger.info(f"'client_mode' for snr: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501
logger.info(f"'client_mode' for Monitoring-SN: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501
loop.create_task(self.modbus_loop(client['host'],
client['port'],
inv['monitor_sn'],

View File

@@ -1,6 +1,7 @@
import asyncio
import logging
import json
from itertools import chain
from cnf.config import Config
from mqtt import Mqtt
@@ -56,8 +57,9 @@ class Proxy():
# reset at midnight when you restart the proxy just before
# midnight!
inverters = Config.get('inverters')
batteries = Config.get('batteries')
# logger.debug(f'Proxys: {inverters}')
for inv in inverters.values():
for _, inv in chain(inverters.items(), batteries.items()):
if (type(inv) is dict):
node_id = inv['node_id']
cls.db_stat.reg_clr_at_midnight(f'{cls.entity_prfx}{node_id}',

View File

@@ -117,19 +117,19 @@ async def handle_shutdown(loop, web_task):
loop.stop()
def get_log_level() -> int:
def get_log_level() -> int | None:
'''checks if LOG_LVL is set in the environment and returns the
corresponding logging.LOG_LEVEL'''
log_level = os.getenv('LOG_LVL', 'INFO')
switch = {
'DEBUG': logging.DEBUG,
'WARN': logging.WARNING,
'INFO': logging.INFO,
'ERROR': logging.ERROR,
}
log_level = os.getenv('LOG_LVL', None)
logging.info(f"LOG_LVL : {log_level}")
if log_level == 'DEBUG':
log_level = logging.DEBUG
elif log_level == 'WARN':
log_level = logging.WARNING
else:
log_level = logging.INFO
return log_level
return switch.get(log_level, None)
def main(): # pragma: no cover
@@ -156,8 +156,10 @@ def main(): # pragma: no cover
setattr(logging.handlers, "log_path", args.log_path)
setattr(logging.handlers, "log_backups", args.log_backups)
os.makedirs(args.log_path, exist_ok=True)
logging.config.fileConfig('logging.ini')
src_dir = os.path.dirname(__file__) + '/'
logging.config.fileConfig(src_dir + 'logging.ini')
logging.info(f'Server "{serv_name} - {version}" will be started')
logging.info(f'current dir: {os.getcwd()}')
logging.info(f"config_path: {args.config_path}")
@@ -170,21 +172,21 @@ def main(): # pragma: no cover
logging.info(f"log_backups: {args.log_backups} days")
log_level = get_log_level()
logging.info('******')
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
logging.getLogger().setLevel(log_level)
logging.getLogger('msg').setLevel(log_level)
logging.getLogger('conn').setLevel(log_level)
logging.getLogger('data').setLevel(log_level)
logging.getLogger('tracer').setLevel(log_level)
logging.getLogger('asyncio').setLevel(log_level)
# logging.getLogger('mqtt').setLevel(log_level)
if log_level:
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
logging.getLogger().setLevel(log_level)
logging.getLogger('msg').setLevel(log_level)
logging.getLogger('conn').setLevel(log_level)
logging.getLogger('data').setLevel(log_level)
logging.getLogger('tracer').setLevel(log_level)
logging.getLogger('asyncio').setLevel(log_level)
# logging.getLogger('mqtt').setLevel(log_level)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# read config file
Config.init(ConfigReadToml("default_config.toml"))
Config.init(ConfigReadToml(src_dir + "cnf/default_config.toml"))
ConfigReadEnv()
ConfigReadJson(args.config_path + "config.json")
ConfigReadToml(args.config_path + "config.toml")

View File

@@ -76,7 +76,7 @@ def ConfigDefault():
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
'sensor_list': 0
},
'Y170000000000001': {
'modbus_polling': True,
@@ -91,8 +91,21 @@ def ConfigDefault():
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
'sensor_list': 0
}
},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'suggested_area': '',
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 0,
}
}
}
@@ -139,14 +152,49 @@ def ConfigComplete():
'pv4': {'manufacturer': 'man4',
'type': 'type4'},
'suggested_area': 'Garage2',
'sensor_list': 688},
'Y170000000000002': {'modbus_polling': False,
'modbus_scanning': {
'bytes': 16,
'start': 2048,
'step': 1024
},
'monitor_sn': 2000000001,
'node_id': 'PV-Garage3/',
'suggested_area': 'Garage3',
'sensor_list': 688}
},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'suggested_area': 'Garage3',
'node_id': 'Bat-Garage3/',
'pv1': {'manufacturer': 'man5',
'type': 'type5'},
'pv2': {'manufacturer': 'man6',
'type': 'type6'},
'sensor_list': 12326}
}
}
def test_default_config():
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
validated = Config.def_config
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 0,
'suggested_area': ''
}
},
'inverters': {
'allow_all': False,
'R170000000000001': {
@@ -158,7 +206,7 @@ def test_default_config():
'modbus_polling': False,
'monitor_sn': 0,
'suggested_area': '',
'sensor_list': 688},
'sensor_list': 0},
'Y170000000000001': {
'modbus_polling': True,
'monitor_sn': 2000000000,
@@ -172,7 +220,7 @@ def test_default_config():
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'suggested_area': '',
'sensor_list': 688}}}
'sensor_list': 0}}}
def test_full_config(ConfigComplete):
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -181,9 +229,15 @@ def test_full_config(ConfigComplete):
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'batteries': {
'4100000000000001': {'modbus_polling': True, 'monitor_sn': 3000000000, 'node_id': 'Bat-Garage3/', 'sensor_list': 0x3026, 'suggested_area': 'Garage3', 'pv1': {'type': 'type5', 'manufacturer': 'man5'}, 'pv2': {'type': 'type6', 'manufacturer': 'man6'}}
},
'inverters': {'allow_all': False,
'R170000000000001': {'modbus_polling': False, 'node_id': 'PV-Garage/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}},
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}}}}
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}},
'Y170000000000002': {'modbus_polling': False, 'monitor_sn': 2000000001, 'node_id': 'PV-Garage3/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage3', 'modbus_scanning': {'start': 2048, 'step': 1024, 'bytes': 16}}
}
}
try:
validated = Config.conf_schema.validate(cnf)
except Exception:
@@ -193,7 +247,7 @@ def test_full_config(ConfigComplete):
def test_read_empty(ConfigDefault):
test_buffer.rd = ""
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -216,14 +270,14 @@ def test_no_file():
assert defcnf == None
def test_no_file2():
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
assert Config.err == None
ConfigReadToml("_no__file__no_")
err = Config.get_error()
assert err == None
def test_invalid_filename():
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
assert Config.err == None
ConfigReadToml(None)
err = Config.get_error()
@@ -232,7 +286,7 @@ def test_invalid_filename():
def test_read_cnf1():
test_buffer.rd = "solarman.enabled = false"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -240,6 +294,19 @@ def test_read_cnf1():
assert err == None
cnf = Config.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 0,
'suggested_area': ''
}
},
'inverters': {
'allow_all': False,
'R170000000000001': {
@@ -251,7 +318,7 @@ def test_read_cnf1():
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
'sensor_list': 0
},
'Y170000000000001': {
'modbus_polling': True,
@@ -266,7 +333,7 @@ def test_read_cnf1():
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
'sensor_list': 0
}
}
}
@@ -279,7 +346,7 @@ def test_read_cnf1():
def test_read_cnf2():
test_buffer.rd = "solarman.enabled = 'FALSE'"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -287,6 +354,19 @@ def test_read_cnf2():
assert err == None
cnf = Config.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 0,
'suggested_area': ''
}
},
'inverters': {
'allow_all': False,
'R170000000000001': {
@@ -298,7 +378,7 @@ def test_read_cnf2():
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
'sensor_list': 0
},
'Y170000000000001': {
'modbus_polling': True,
@@ -313,7 +393,7 @@ def test_read_cnf2():
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
'sensor_list': 0
}
}
}
@@ -322,7 +402,7 @@ def test_read_cnf2():
def test_read_cnf3(ConfigDefault):
test_buffer.rd = "solarman.port = 'FALSE'"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -334,7 +414,7 @@ def test_read_cnf3(ConfigDefault):
def test_read_cnf4():
test_buffer.rd = "solarman.port = 5000"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -342,6 +422,19 @@ def test_read_cnf4():
assert err == None
cnf = Config.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'batteries': {
'4100000000000001': {
'modbus_polling': True,
'monitor_sn': 3000000000,
'node_id': '',
'pv1': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 0,
'suggested_area': ''
}
},
'inverters': {
'allow_all': False,
'R170000000000001': {
@@ -353,7 +446,7 @@ def test_read_cnf4():
'type': 'RSM40-8-395M'},
'pv2': {'manufacturer': 'Risen',
'type': 'RSM40-8-395M'},
'sensor_list': 688
'sensor_list': 0
},
'Y170000000000001': {
'modbus_polling': True,
@@ -368,7 +461,7 @@ def test_read_cnf4():
'type': 'RSM40-8-410M'},
'pv4': {'manufacturer': 'Risen',
'type': 'RSM40-8-410M'},
'sensor_list': 688
'sensor_list': 0
}
}
}
@@ -377,7 +470,7 @@ def test_read_cnf4():
def test_read_cnf5():
test_buffer.rd = "solarman.port = 1023"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()
@@ -386,7 +479,7 @@ def test_read_cnf5():
def test_read_cnf6():
test_buffer.rd = "solarman.port = 65536"
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadToml("config/config.toml")
err = Config.get_error()

View File

@@ -44,7 +44,7 @@ def test_extend_key():
assert conf == {'': 'testuser'}
def test_read_env_config():
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}
for _ in patch_getenv():

View File

@@ -84,7 +84,7 @@ def ConfigTomlEmpty():
def test_no_config(ConfigDefault):
test_buffer.rd = "" # empty buffer, no json
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadJson()
err = Config.get_error()
@@ -96,7 +96,7 @@ def test_no_config(ConfigDefault):
def test_no_file(ConfigDefault):
test_buffer.rd = "" # empty buffer, no json
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadJson("_no__file__no_")
err = Config.get_error()
@@ -108,7 +108,7 @@ def test_no_file(ConfigDefault):
def test_invalid_filename(ConfigDefault):
test_buffer.rd = "" # empty buffer, no json
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadJson(None)
err = Config.get_error()
@@ -340,7 +340,7 @@ def test_cnv6():
def test_empty_config(ConfigDefault):
test_buffer.rd = "{}" # empty json
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadJson()
err = Config.get_error()
@@ -380,6 +380,29 @@ def test_full_config(ConfigComplete):
"pv4.manufacturer": "man4",
"pv4.type": "type4",
"sensor_list": 688
},
{
"serial": "Y170000000000002",
"monitor_sn": 2000000001,
"modbus_polling": false,
"modbus_scanning.start": 2048,
"node_id": "PV-Garage3",
"suggested_area": "Garage3",
"sensor_list": 688
}
],
"batteries": [
{
"serial": "4100000000000001",
"modbus_polling": true,
"monitor_sn": 3000000000,
"node_id": "Bat-Garage3",
"suggested_area": "Garage3",
"pv1.manufacturer": "man5",
"pv1.type": "type5",
"pv2.manufacturer": "man6",
"pv2.type": "type6",
"sensor_list": 12326
}
],
"tsun.enabled": true,
@@ -401,7 +424,7 @@ def test_full_config(ConfigComplete):
]
}
"""
Config.init(ConfigReadToml("app/config/default_config.toml"))
Config.init(ConfigReadToml("app/src/cnf/default_config.toml"))
for _ in patch_open():
ConfigReadJson()
err = Config.get_error()

View File

@@ -93,6 +93,86 @@ def contr2_data_seq(): # Get Time Request message
msg += b'\x00\x00\x00'
return msg
@pytest.fixture
def contr3_data_seq(): # Get Time Request message
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' # | ..^.....9..+.T.R
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x32\x2e\x30\x31\x2e\x31\x33\x00' # | SW_400_V2.01.13.
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' # | .'.T.Raymon../.T
msg += b'\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88' # | .RSW-1-10001..Z.
msg += b'\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f' # | T.t.raymoniot.co
msg += b'\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61' # | m..Z.T.logger.ta
msg += b'\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e' # | lent-monitoring.
msg += b'\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | com../.T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32\xe8\x54\x10\xff' # | ...........2.T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .6.T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff\xff\xff\xff\xff' # | .......:.T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3e\xa0\x54' # | .............>.T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J
msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT..............
msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i
msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................
msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I..
msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I
msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I.......
msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I.....
msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI...
msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I.
msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........
msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I......
msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I....
msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I..
msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L.........
msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI...
msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I.
msg += b'\x00\x00\xff'
return msg
@pytest.fixture
def inv_data_seq(): # Data indication from the controller
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
@@ -140,6 +220,153 @@ def inv_data_seq2(): # Data indication from the controller
msg += b'\x53\x00\x00'
return msg
@pytest.fixture
def inv_data_seq3(): # Inverter indication from MS-2000
msg = b'\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
msg += b'\x53\x00\x66' # | S.f'
return msg
@pytest.fixture
def inv_data_new(): # Data indication from DSP V5.0.17
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
@@ -254,7 +481,7 @@ def inv_data_seq2_zero(): # Data indication from the controller
def test_parse_control(contr_data_seq):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
@@ -262,15 +489,23 @@ def test_parse_control(contr_data_seq):
def test_parse_control2(contr2_data_seq):
i = InfosG3()
for key, result in i.parse (contr2_data_seq):
for key, result in i.parse (contr2_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
{"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_control3(contr3_data_seq):
i = InfosG3()
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
{"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "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": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
def test_parse_inverter(inv_data_seq):
i = InfosG3()
for key, result in i.parse (inv_data_seq):
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
@@ -278,10 +513,10 @@ def test_parse_inverter(inv_data_seq):
def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq):
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
@@ -289,10 +524,29 @@ def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
"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},
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
def test_parse_cont_and_invert2(contr3_data_seq, inv_data_seq3):
i = InfosG3()
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq3, sensor=0x01900000):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps(
{
"collector": {"Collector_Fw_Version": "RSW_400_V2.01.13", "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": 98, "Power_On_Time": 335, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300},
"env": {"Inverter_Status": 0},
"events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0},
"input": {"iVal_1": 1, "Val_0": 655.28, "Val_1": 327.4, "Val_2": 0.0, "Val_3": 0.0, "iVal_2": 3, "iVal_3": 12, "iVal_4": 80, "Val_4": 327.4, "iVal_5": 0, "Val_10": 30.25, "Val_11": 1.35, "iVal_6": 12, "pv1": {"Voltage": 78.6, "Current": 0.0, "Power": 0.0}, "Val_5": 110.0, "pv2": {"Voltage": 58.1, "Current": 0.0, "Power": 0.0}, "Val_6": 110.0, "pv3": {"Voltage": 58.8, "Current": 0.0, "Power": 0.0}, "Val_7": 110.0, "Val_14": 0.01, "Val_15": 0.0, "Val_16": 33.0, "Val_17": 0.0, "Val_18": 0.0, "iVal_8": 2, "pv4": {"Voltage": 17.4, "Current": 0.02, "Power": 0.3}, "Val_8": 4.8, "iVal_10": 1, "pv5": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv6": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "Val_24": 0.0, "Val_25": 0.0, "Val_26": 32.99, "Val_27": 0.0, "Val_28": 0.0, "iVal_11": 2, "iVal_12": 3},
"grid": {"Voltage": 238.5, "Current": 0.05, "Frequency": 50.0, "Output_Power": 0.0},
"inverter": {"Max_Designed_Power": 3000},
"total": {"Total_Generation": 29.86}
})
def test_build_ha_conf1(contr_data_seq):
i = InfosG3()
i.static_init() # initialize counter
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
@@ -328,6 +582,7 @@ def test_build_ha_conf1(contr_data_seq):
def test_build_ha_conf2(contr_data_seq):
i = InfosG3()
i.static_init() # initialize counter
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
tests = 0
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
@@ -352,11 +607,12 @@ def test_build_ha_conf2(contr_data_seq):
def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq):
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq2):
for key, result in i.parse (inv_data_seq2, sensor=0x01900001):
pass # side effect in calling i.parse()
tests = 0
@@ -390,9 +646,10 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
def test_build_ha_conf4(contr_data_seq, inv_data_seq):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
i.set_db_def_value(Register.SENSOR_LIST, "01900001")
for key, result in i.parse (contr_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq):
for key, result in i.parse (inv_data_seq, sensor=0x01900001):
pass # side effect in calling i.parse()
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
@@ -414,10 +671,37 @@ def test_build_ha_conf4(contr_data_seq, inv_data_seq):
tests +=1
assert tests==1
def test_build_ha_conf5(contr3_data_seq, inv_data_seq3):
i = InfosG3()
i.set_db_def_value(Register.SENSOR_LIST, "01900000")
for key, result in i.parse (contr3_data_seq, sensor=0x0e100000):
pass # side effect in calling i.parse()
for key, result in i.parse (inv_data_seq3, sensor=0x01900000):
pass # side effect in calling i.parse()
i.set_db_def_value(Register.MAC_ADDR, "00a057123456")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
if id == 'signal_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:56"]]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
assert tests==1
i.set_db_def_value(Register.MAC_ADDR, "00:a0:57:12:34:57")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
if id == 'signal_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller - roof", "sa": "Controller - roof", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V2.01.13", "ids": ["controller_123"], "cns": [["mac", "00:a0:57:12:34:57"]]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
assert tests==1
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3()
tests = 0
for key, update in i.parse (inv_data_seq2):
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
if key == 'total' or key == 'inverter' or key == 'env':
assert update == True
tests +=1
@@ -426,7 +710,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 23})
tests = 0
for key, update in i.parse (inv_data_seq2):
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
if key == 'total' or key == 'env':
assert update == False
tests +=1
@@ -438,7 +722,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Work_Mode": 0, "Max_Designed_Power": -1, "Input_Coefficient": -0.1, "Output_Coefficient": 100.0, "No_Inputs": 2})
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
if key == 'total':
assert update == False
tests +=1
@@ -453,7 +737,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3()
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
if key == 'total':
assert update == False
tests +=1
@@ -467,7 +751,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
for key, update in i.parse (inv_data_seq2_zero, sensor=0x01900001):
if key == 'total' or key == 'env':
assert update == False
tests +=1
@@ -478,7 +762,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Status": 1, "Inverter_Temp": 0})
tests = 0
for key, update in i.parse (inv_data_seq2):
for key, update in i.parse (inv_data_seq2, sensor=0x01900001):
if key == 'total' or key == 'env':
tests +=1
@@ -489,7 +773,7 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
def test_new_data_types(inv_data_new):
i = InfosG3()
tests = 0
for key, update in i.parse (inv_data_new):
for key, update in i.parse (inv_data_new, sensor=0x01900001):
if key == 'events':
tests +=1
elif key == 'inverter':
@@ -514,7 +798,7 @@ def test_invalid_data_type(invalid_data_seq):
assert val == 0
for key, result in i.parse (invalid_data_seq):
for key, result in i.parse (invalid_data_seq, sensor=0x01900001):
pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})

View File

@@ -70,6 +70,28 @@ def inverter_data(): # 0x4210 ftype: 0x01
msg += b'\x00\x00\x00\x00'
return msg
@pytest.fixture
def batterie_data(): # 0x4210 ftype: 0x01
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x26\x30\xc7\xde'
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
msg += b'\x0d\x3a\x00\x70\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
return msg
@pytest.fixture
def batterie_data2(): # 0x4210 ftype: 0x01
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x26\x30\xc7\xde'
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
msg += b'\x0d\x3a\x00\x70\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e'
return msg
def test_default_db():
i = InfosG3P(client_mode=False)
@@ -101,11 +123,11 @@ def test_build_4110(str_test_ip, device_data: bytes):
build_msg[i] = device_data[i]
assert device_data == build_msg
def test_parse_4210(inverter_data: bytes):
def test_parse_4210_02b0(inverter_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
@@ -123,14 +145,56 @@ def test_parse_4210(inverter_data: bytes):
"other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024, "Prod_Compliance_Type": 6}
})
def test_parse_4210_3026(batterie_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (batterie_data, 0x42, 1, 0x3026):
pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
"controller": {"Sensor_List": "3026", "Power_On_Time": 4684},
"inverter": {"Serial_Number": "4101240701490314"},
"batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12},
"pv2": {"Voltage": 33.72, "Current": 0.0},
"Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0,
"Voltage": 51.34, "Current": -0.02, "SOC": 10.0,
"Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21},
"Temp_1": 15, "Temp_2": 15, "Temp_3": 15,
"out": {"Voltage": 0.14, "Current": 0.0, "Out_Status": 0, "Power": 0.0},
"Controller_Temp": 15, "Reg_74": 0, "Reg_76": 517, "Reg_78": 513,
"PV_Power": 37.9232, "Power": -1.0268000000000002},
})
def test_parse_4210_3026_incomplete(batterie_data2: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (batterie_data2, 0x42, 1, 0x3026):
pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
"controller": {"Sensor_List": "3026", "Power_On_Time": 4684},
"inverter": {"Serial_Number": "4101240701490314"},
"batterie": {"pv1": {"Voltage": 33.86, "Current": 1.12},
"pv2": {"Voltage": 33.72, "Current": 0.0},
"Reg_38": 0, "Total_Generation": 20.8, "Status_1": 0, "Status_2": 0,
"Voltage": 51.34, "Current": -0.02, "SOC": 10.0,
"Cell": {"Volt1": 3.21, "Volt2": 3.21, "Volt3": 3.21, "Volt4": 3.21, "Volt5": 3.21, "Volt6": 3.21, "Volt7": 3.21, "Volt8": 3.21, "Volt9": 3.21, "Volt10": 3.21, "Volt11": 3.21, "Volt12": 3.21, "Volt13": 3.21, "Volt14": 3.21, "Volt15": 3.21, "Volt16": 3.21},
"Temp_1": 15, "Temp_2": 15, "Temp_3": 15,
"out": {"Voltage": 0.14, "Current": None, "Out_Status": None, "Power": None},
"Controller_Temp": None, "Reg_74": None, "Reg_76": None, "Reg_78": None,
"PV_Power": 37.9232, "Power": -1.0268000000000002},
})
def test_build_4210(inverter_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
for i in range(11, 31):
build_msg[i] = inverter_data[i]
assert inverter_data == build_msg
@@ -138,6 +202,7 @@ def test_build_4210(inverter_data: bytes):
def test_build_ha_conf1():
i = InfosG3P(client_mode=False)
i.static_init() # initialize counter
i.set_db_def_value(Register.SENSOR_LIST, "02b0")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
@@ -212,6 +277,7 @@ def test_build_ha_conf2():
def test_build_ha_conf3():
i = InfosG3P(client_mode=True)
i.static_init() # initialize counter
i.set_db_def_value(Register.SENSOR_LIST, "02b0")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
@@ -283,56 +349,89 @@ def test_build_ha_conf4():
assert tests==1
def test_build_ha_conf5():
i = InfosG3P(client_mode=True)
i.static_init() # initialize counter
i.set_db_def_value(Register.SENSOR_LIST, "3026")
tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
if id == 'out_power_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Output Power", "stat_t": "tsun/garagendach/batterie", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{ (value_json['out']['Power'] | int)}}", "unit_of_meas": "W", "dev": {"name": "Batterie", "sa": "Batterie", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["batterie_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
elif id == 'daily_gen_123':
assert False
elif id == 'volt_pv1_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv1_123", "val_tpl": "{{ (value_json['pv1']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "batterie_123", "ids": ["bat_inp_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
elif id == 'volt_pv2_123':
assert comp == 'sensor'
assert d_json == json.dumps({"name": "Voltage", "stat_t": "tsun/garagendach/batterie", "dev_cla": "voltage", "stat_cla": "measurement", "uniq_id": "volt_pv2_123", "val_tpl": "{{ (value_json['pv2']['Voltage'] | float)}}", "unit_of_meas": "V", "ic": "mdi:gauge", "ent_cat": "diagnostic", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "batterie_123", "ids": ["bat_inp_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
elif id == 'signal_123':
assert comp == 'sensor'
assert d_json == json.dumps({})
tests +=1
elif id == 'inv_count_456':
assert False
else:
print(id)
assert tests==4
def test_exception_and_calc(inverter_data: bytes):
# patch table to convert temperature from °F to °C
ofs = RegisterMap.map[0x420100d8]['offset']
RegisterMap.map[0x420100d8]['quotient'] = 1.8
RegisterMap.map[0x420100d8]['offset'] = -32/1.8
ofs = RegisterMap.map_02b0[0x420100d8]['offset']
RegisterMap.map_02b0[0x420100d8]['quotient'] = 1.8
RegisterMap.map_02b0[0x420100d8]['offset'] = -32/1.8
# map PV1_VOLTAGE to invalid register
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.TEST_REG2
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
backup = RegisterMap.map[0x420100de]
RegisterMap.map[0x420100de] = 'invalid_entry'
backup = RegisterMap.map_02b0[0x420100de]
RegisterMap.map_02b0[0x420100de] = 'invalid_entry'
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:0xde] == inverter_data[32:0xde]
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
# remove a table entry and test parsing and building
del RegisterMap.map[0x420100d8]['quotient']
del RegisterMap.map[0x420100d8]['offset']
del RegisterMap.map_02b0[0x420100d8]['quotient']
del RegisterMap.map_02b0[0x420100d8]['offset']
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
# test restore table
RegisterMap.map[0x420100d8]['offset'] = ofs
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
RegisterMap.map[0x420100de] = backup # reset mapping
RegisterMap.map_02b0[0x420100d8]['offset'] = ofs
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
RegisterMap.map_02b0[0x420100de] = backup # reset mapping
# test orginial table
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:-1] == inverter_data[32:-1]

View File

@@ -7,18 +7,26 @@ from server import get_log_level
def test_get_log_level():
with patch.dict(os.environ, {'LOG_LVL': ''}):
with patch.dict(os.environ, {}):
log_lvl = get_log_level()
assert log_lvl == logging.INFO
assert log_lvl == None
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
log_lvl = get_log_level()
assert log_lvl == logging.DEBUG
with patch.dict(os.environ, {'LOG_LVL': 'INFO'}):
log_lvl = get_log_level()
assert log_lvl == logging.INFO
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
log_lvl = get_log_level()
assert log_lvl == logging.WARNING
with patch.dict(os.environ, {'LOG_LVL': 'ERROR'}):
log_lvl = get_log_level()
assert log_lvl == logging.ERROR
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
log_lvl = get_log_level()
assert log_lvl == logging.INFO
assert log_lvl == None

View File

@@ -11,6 +11,7 @@ from cnf.config import Config
from infos import Infos, Register
from modbus import Modbus
from messages import State, Message
from proxy import Proxy
pytest_plugins = ('pytest_asyncio',)
@@ -24,6 +25,8 @@ heartbeat = 60
class Mqtt():
def __init__(self):
self.clear()
def clear(self):
self.key = ''
self.data = ''
@@ -50,7 +53,6 @@ class MemoryStream(SolarmanV5):
self.mb_timeout = 0.5
self.sent_pdu = b''
self.ifc.tx_fifo.reg_trigger(self.write_cb)
self.mqtt = Mqtt()
self.__msg = msg
self.__msg_len = len(msg)
self.__chunks = chunks
@@ -62,7 +64,6 @@ class MemoryStream(SolarmanV5):
self.db.stat['proxy']['AT_Command'] = 0
self.db.stat['proxy']['AT_Command_Blocked'] = 0
self.test_exception_async_write = False
self.entity_prfx = ''
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
self.key = ''
self.data = ''
@@ -85,8 +86,8 @@ class MemoryStream(SolarmanV5):
self.__chunk_idx = 0
def publish_mqtt(self, key, data):
self.key = key
self.data = data
Proxy.mqtt.key = key
Proxy.mqtt.data = data
def _read(self) -> int:
copied_bytes = 0
@@ -130,6 +131,12 @@ def get_sn() -> bytes:
def get_sn_int() -> int:
return 2070233889
def get_dcu_sn() -> bytes:
return b'\x20\x43\x65\x7b'
def get_dcu_sn_int() -> int:
return 2070233888
def get_inv_no() -> bytes:
return b'T170000000000001'
@@ -560,6 +567,17 @@ def at_command_rsp_msg(): # 0x1510
msg += b'\x15'
return msg
@pytest.fixture
def at_command_interim_rsp_msg(): # 0x0510
msg = b'\xa5\x25\x00\x10\x05\x03\x03' +get_sn() +b'\x08\x01'
msg += total()
msg += hb()
msg += b'\x00\x00\x00\x00+ok=10\x2c'
msg += b'start download\x0d\x0a'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def heartbeat_ind_msg(): # 0x4710
msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn()
@@ -615,6 +633,15 @@ def msg_modbus_cmd_fwd():
msg += b'\x15'
return msg
@pytest.fixture
def msg_modbus_cmd_seq():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x05\x26\x30\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x06\x01\x00\x01'
msg += b'\x03\xe8'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def msg_modbus_cmd_crc_err():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
@@ -637,6 +664,32 @@ def msg_modbus_rsp(): # 0x1510
msg += b'\x15'
return msg
@pytest.fixture
def msg_modbus_interim_rsp(): # 0x0510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += b'\x0a\xe2\xfa\x33\x01\x03\x28\x40\x10\x08\xd8'
msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02'
msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04'
msg += b'\x00\x01\x00\x00\x6c\x68'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def msg_modbus_rsp_inv_id2(): # 0x1510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += b'\x0a\xe2\xfa\x33\x02\x03\x28\x40\x10\x08\xd8'
msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02'
msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04'
msg += b'\x00\x01\x00\x00\x2a\xaa'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def msg_modbus_invalid(): # 0x1510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00'
@@ -672,9 +725,94 @@ def msg_unknown_cmd_rsp(): # 0x1510
msg += b'\x15'
return msg
@pytest.fixture
def dcu_modbus_rsp(): # 0x1510
msg = b'\xa5\x6d\x00\x10\x15\x03\x03' +get_dcu_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += b'\x4d\x0d\x84\x34\x01\x03\x5a\x34\x31\x30\x31'
msg += b'\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34\x00\x32\x00\x00'
msg += b'\x00\x32\x00\x00\x00\x00\x10\x7b\x00\x02\x00\x02\x14\x9b\xfe\xfd'
msg += b'\x25\x28\x0c\xe1\x0c\xde\x0c\xe1\x0c\xe1\x0c\xe0\x0c\xe1\x0c\xe3'
msg += b'\x0c\xdf\x0c\xe0\x0c\xe2\x0c\xe1\x0c\xe1\x0c\xe2\x0c\xe2\x0c\xe3'
msg += b'\x0c\xdf\x00\x14\x00\x14\x00\x13\x0f\x94\x01\x4a\x00\x01\x00\x15'
msg += b'\x00\x00\x02\x05\x02\x01\x14\xab'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_dev_ind_msg(): # 0x4110
msg = b'\xa5\x3a\x01\x10\x41\x91\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32'
msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53'
msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35'
msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e'
msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26'
msg += b'\x30\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\x7a\x75\x68\x61\x75\x73\x65\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\x08\x01\x01\x01\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x01'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_dev_rsp_msg(): # 0x1110
msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_data_ind_msg(): # 0x4210
msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde'
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
msg += b'\x34\x31\x30\x31\x32\x34\x30\x37\x30\x31\x34\x39\x30\x33\x31\x34'
msg += b'\x0d\x3a\x00\x00\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_data_rsp_msg(): # 0x1210
msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def config_tsun_allow_all():
Config.act_config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
Config.act_config = {
'ha':{
'auto_conf_prefix': 'homeassistant',
'discovery_prefix': 'homeassistant',
'entity_prefix': 'tsun',
'proxy_node_id': 'test_1',
'proxy_unique_id': ''
},
'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
Proxy.class_init()
Proxy.mqtt = Mqtt() # set dummy mqtt instance
@pytest.fixture
def config_no_tsun_inv1():
@@ -682,7 +820,29 @@ def config_no_tsun_inv1():
@pytest.fixture
def config_tsun_inv1():
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
Config.act_config = {
'ha':{
'auto_conf_prefix': 'homeassistant',
'discovery_prefix': 'homeassistant',
'entity_prefix': 'tsun',
'proxy_node_id': 'test_1',
'proxy_unique_id': ''
},
'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
Proxy.class_init()
Proxy.mqtt = Mqtt()
@pytest.fixture
def config_tsun_scan():
Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0xffc0, 'step': 0x40, 'bytes':20}, 'suggested_area':'roof', 'sensor_list': 0}}}
@pytest.fixture
def config_tsun_scan_dcu():
Config.act_config = {'solarman':{'enabled': True},'inverters':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'modbus_scanning': {'start': 0x0000, 'step': 0x100, 'bytes':0x2d}, 'client_mode': {'host': '192.168.1.1.'}, 'suggested_area':'roof', 'sensor_list': 0}}}
@pytest.fixture
def config_tsun_dcu1():
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
def test_read_message(device_ind_msg):
Config.act_config = {'solarman':{'enabled': True}}
@@ -963,6 +1123,34 @@ def test_read_two_messages3(config_tsun_allow_all, device_ind_msg2, device_rsp_m
assert m.ifc.tx_fifo.get()==b''
m.close()
def test_read_two_messages4(config_tsun_dcu1, dcu_dev_ind_msg, dcu_dev_rsp_msg, dcu_data_ind_msg, dcu_data_rsp_msg):
_ = config_tsun_dcu1
m = MemoryStream(dcu_dev_ind_msg, (0,))
m.append_msg(dcu_data_ind_msg)
assert 0 == m.sensor_list
m._init_new_client_conn()
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 == 2070233888
assert m.unique_id == '2070233888'
assert m.msg_recvd[0]['control']==0x4110
assert m.msg_recvd[0]['seq']=='01:92'
assert m.msg_recvd[0]['data_len']==314
assert m.msg_recvd[1]['control']==0x4210
assert m.msg_recvd[1]['seq']=='02:93'
assert m.msg_recvd[1]['data_len']==111
assert '3026' == m.db.get_db_value(Register.SENSOR_LIST, None)
assert 0x3026 == m.sensor_list
assert m.ifc.fwd_fifo.get()==dcu_dev_ind_msg+dcu_data_ind_msg
assert m.ifc.tx_fifo.get()==dcu_dev_rsp_msg+dcu_data_rsp_msg
m._init_new_client_conn()
assert m.ifc.tx_fifo.get()==b''
m.close()
def test_unkown_frame_code(config_tsun_inv1, inverter_ind_msg_81, inverter_rsp_msg_81):
_ = config_tsun_inv1
m = MemoryStream(inverter_ind_msg_81, (0,))
@@ -1331,8 +1519,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
assert m.ifc.fwd_fifo.get()==b''
assert m.sent_pdu == b''
assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.append_msg(inverter_ind_msg)
m.read() # read inverter ind
@@ -1348,8 +1536,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
m.sent_pdu = bytearray()
assert str(m.seq) == '02:03'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.append_msg(at_command_rsp_msg)
m.read() # read at resp
@@ -1358,8 +1546,9 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
assert m.ifc.rx_get()==b''
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==b''
assert m.key == 'at_resp'
assert m.data == "+ok"
assert Proxy.mqtt.key == 'tsun/at_resp'
assert Proxy.mqtt.data == "+ok"
Proxy.mqtt.clear() # clear last test result
m.sent_pdu = bytearray()
m.test_exception_async_write = True
@@ -1371,8 +1560,8 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
assert m.sent_pdu == b''
assert str(m.seq) == '03:04'
assert m.forward_at_cmd_resp == False
assert m.mqtt.key == ''
assert m.mqtt.data == ""
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.close()
@pytest.mark.asyncio
@@ -1389,8 +1578,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==b''
assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.append_msg(inverter_ind_msg)
m.read()
@@ -1406,8 +1595,8 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
assert m.ifc.fwd_fifo.get()==b''
assert str(m.seq) == '02:02'
assert m.forward_at_cmd_resp == False
assert m.mqtt.key == 'at_resp'
assert m.mqtt.data == "'AT+WEBU' is forbidden"
assert Proxy.mqtt.key == 'tsun/at_resp'
assert Proxy.mqtt.data == "'AT+WEBU' is forbidden"
m.close()
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
@@ -1496,6 +1685,29 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
def test_msg_at_command_rsp3(config_tsun_inv1, at_command_interim_rsp_msg):
_ = config_tsun_inv1
m = MemoryStream(at_command_interim_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.db.stat['proxy']['Invalid_Msg_Format'] = 0
m.db.stat['proxy']['Unknown_Msg'] = 0
m.forward_at_cmd_resp = True
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.control == 0x0510
assert str(m.seq) == '03:03'
assert m.header_len==11
assert m.data_len==37
assert m.ifc.fwd_fifo.get()==at_command_interim_rsp_msg
assert m.ifc.tx_fifo.get()==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert m.db.stat['proxy']['Unknown_Msg'] == 0
m.close()
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
_ = config_tsun_inv1
m = MemoryStream(b'')
@@ -1524,6 +1736,34 @@ def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
def test_msg_modbus_req_seq(config_tsun_inv1, msg_modbus_cmd_seq):
_ = config_tsun_inv1
m = MemoryStream(b'')
m.snr = get_sn_int()
m.sensor_list = 0x2b0
m.state = State.up
c = m.createClientStream(msg_modbus_cmd_seq)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.db.stat['proxy']['Invalid_Msg_Format'] = 0
c.read() # read complete msg, and dispatch msg
assert not c.header_valid # must be invalid, since msg was handled and buffer flushed
assert c.msg_count == 1
assert c.control == 0x4510
assert str(c.seq) == '03:02'
assert c.header_len==11
assert c.data_len==23
assert c.ifc.fwd_fifo.get()==msg_modbus_cmd_seq
assert c.ifc.tx_fifo.get()==b''
assert m.sent_pdu == b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['AT_Command'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
_ = config_tsun_inv1
m = MemoryStream(b'')
@@ -1762,6 +2002,79 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
assert next(m.mb_timer.exp_count) == 4
m.close()
@pytest.mark.asyncio
async def test_modbus_scaning(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp_msg, msg_modbus_rsp, msg_modbus_rsp_inv_id2):
_ = config_tsun_scan
assert asyncio.get_running_loop()
m = MemoryStream(heartbeat_ind_msg, (0x15,0x56,0))
m.append_msg(msg_modbus_rsp)
m.append_msg(msg_modbus_rsp_inv_id2)
assert m.mb_scan == False
assert asyncio.get_running_loop() == m.mb_timer.loop
m.db.stat['proxy']['Unknown_Ctrl'] = 0
assert m.mb_timer.tim == None
m.read() # read complete msg, and dispatch msg
assert m.mb_scan == True
assert m.mb_start_reg == 0xff80
assert m.mb_step == 0x40
assert m.mb_bytes == 0x14
assert asyncio.get_running_loop() == m.mb_timer.loop
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.snr == 2070233889
assert m.control == 0x4710
assert m.msg_recvd[0]['control']==0x4710
assert m.msg_recvd[0]['seq']=='84:11'
assert m.msg_recvd[0]['data_len']==0x1
assert m.ifc.tx_fifo.get()==heartbeat_rsp_msg
assert m.ifc.fwd_fifo.get()==heartbeat_ind_msg
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.ifc.tx_clear() # clear send buffer for next test
assert isclose(m.mb_timeout, 0.5)
assert next(m.mb_timer.exp_count) == 0
await asyncio.sleep(0.5)
assert m.sent_pdu==b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \
b'\x00\x00\x00\x00\x00\x00\x01\x03\xff\xc0\x00\x14\x75\xed\x33\x15'
assert m.ifc.tx_fifo.get()==b''
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.msg_recvd[1]['control']==0x1510
assert m.msg_recvd[1]['seq']=='03:03'
assert m.msg_recvd[1]['data_len']==0x3b
assert m.mb.last_addr == 1
assert m.mb.last_fcode == 3
assert m.mb.last_reg == 0xffc0 # mb_start_reg + mb_step
assert m.mb.last_len == 20
assert m.mb.err == 0
await asyncio.sleep(0.5)
assert m.sent_pdu==b'\xa5\x17\x00\x10E\x04\x03!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \
b'\x00\x00\x00\x00\x00\x00\x02\x03\x00\x00\x00\x14\x45\xf6\xbf\x15'
assert m.ifc.tx_fifo.get()==b''
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 == 3
assert m.msg_recvd[2]['control']==0x1510
assert m.msg_recvd[2]['seq']=='03:03'
assert m.msg_recvd[2]['data_len']==0x3b
assert m.mb.last_addr == 2
assert m.mb.last_fcode == 3
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
assert m.mb.last_len == 20
assert m.mb.err == 0
assert next(m.mb_timer.exp_count) == 3
m.close()
@pytest.mark.asyncio
async def test_start_client_mode(config_tsun_inv1, str_test_ip):
_ = config_tsun_inv1
@@ -1794,6 +2107,79 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
assert next(m.mb_timer.exp_count) == 3
m.close()
@pytest.mark.asyncio
async def test_start_client_mode_scan(config_tsun_scan_dcu, str_test_ip, dcu_modbus_rsp):
_ = config_tsun_scan_dcu
assert asyncio.get_running_loop()
m = MemoryStream(dcu_modbus_rsp, (131,0,))
m.append_msg(dcu_modbus_rsp)
assert m.state == State.init
assert m.no_forwarding == False
assert m.mb_timer.tim == None
assert asyncio.get_running_loop() == m.mb_timer.loop
await m.send_start_cmd(get_dcu_sn_int(), str_test_ip, False, m.mb_first_timeout)
assert m.mb_start_reg == 0x0000
assert m.mb_step == 0x100
assert m.mb_bytes == 0x2d
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x95\x15')
assert m.mb_scan == True
m.mb_step = 0
assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120
assert m.state == State.up
assert m.no_forwarding == True
assert m.ifc.tx_fifo.get()==b''
assert isclose(m.mb_timeout, 0.5)
assert m.ifc.tx_fifo.get()==b''
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.msg_recvd[0]['control']==0x1510
assert m.msg_recvd[0]['seq']=='03:03'
assert m.msg_recvd[0]['data_len']==109
assert m.mb.last_addr == 1
assert m.mb.last_fcode == 3
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
assert m.mb.last_len == 45
assert m.mb.err == 0
assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225)
assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604)
assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0)
assert m.new_data['batterie'] == True
m.new_data['batterie'] = False
await asyncio.sleep(0.5)
assert m.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x04\x03 Ce{\x02&0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00-\x85\xd7\x9b\x15')
assert m.ifc.tx_fifo.get()==b''
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.msg_recvd[1]['control']==0x1510
assert m.msg_recvd[1]['seq']=='03:03'
assert m.msg_recvd[1]['data_len']==109
assert m.mb.last_addr == 1
assert m.mb.last_fcode == 3
assert m.mb.last_reg == 0x0000 # mb_start_reg + mb_step
assert m.mb.last_len == 45
assert m.mb.err == 0
assert isclose(m.db.get_db_value(Register.BATT_PWR, None), -136.6225)
assert isclose(m.db.get_db_value(Register.BATT_OUT_PWR, None), 131.604)
assert isclose(m.db.get_db_value(Register.BATT_PV_PWR, None), 0.0)
assert m.new_data['batterie'] == False
assert next(m.mb_timer.exp_count) == 1
m.close()
def test_timeout(config_tsun_inv1):
_ = config_tsun_inv1
m = MemoryStream(b'')

View File

@@ -7,6 +7,7 @@ from cnf.config import Config
from infos import Infos, Register
from modbus import Modbus
from messages import State
from mock import patch
pytest_plugins = ('pytest_asyncio',)
@@ -468,15 +469,15 @@ def config_tsun_allow_all():
@pytest.fixture
def config_no_tsun_inv1():
Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture
def config_tsun_inv1():
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0x01900001, 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture
def config_no_modbus_poll():
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}}
Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'sensor_list': 0, 'modbus_polling': False, 'suggested_area':'roof'}}}
@pytest.fixture
def msg_ota_req(): # Over the air update request from tsun cloud
@@ -817,6 +818,236 @@ def multiple_recv_buf(): # There are three message in the buffer, but the second
msg += b'\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' # | 0...<T.A,B,C'
return msg
@pytest.fixture
def msg_controller_ms3000_ind(): # Data indication from the controller
msg = b'\x00\x00\x04\xdf\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
msg += b'\x01\x00\x00\x01'
msg += b'\x95\x18\x5e\x19\x98\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' # | ..^.....9..+.T.R
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x32\x2e\x30\x31\x2e\x31\x33\x00' # | SW_400_V2.01.13.
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' # | .'.T.Raymon../.T
msg += b'\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88' # | .RSW-1-10001..Z.
msg += b'\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f' # | T.t.raymoniot.co
msg += b'\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61' # | m..Z.T.logger.ta
msg += b'\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e' # | lent-monitoring.
msg += b'\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | com../.T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32\xe8\x54\x10\xff' # | ...........2.T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .6.T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff\xff\xff\xff\xff' # | .......:.T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3e\xa0\x54' # | .............>.T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...B.T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54\x10\xff\xff\xff' # | .........FpT....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4a' # | ...............J
msg += b'\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | XT..............
msg += b'\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....N@T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52\x28\x54\x10\xff' # | ...........R(T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .V.T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff\xff\xff\xff\xff' # | .......Y.T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x5d\xe0\x54' # | .............].T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...a.T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54\x10\xff\xff\xff' # | .........e.T....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x69' # | ...............i
msg += b'\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
msg += b'\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....m.T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71\x68\x54\x10\xff' # | ...........qhT..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .uPT............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff\xff\xff\xff\xff' # | .......y8T......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x7d\x20\x54' # | .............} T
msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T..........
msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54\x10\xff\xff\xff' # | ...........T....
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x88' # | ................
msg += b'\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T..............
msg += b'\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90\xa8\x54\x10\xff' # | .............T..
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................
msg += b'\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...T............
msg += b'\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff\xff\xff\xff\xff' # | ........xT......
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x9c\x60\x54' # | ..............`T
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................
msg += b'\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00' # | ... I......5.I..
msg += b'\x00\x62\x00\x0c\x96\xa8\x49\x00\x00\x01\x4f\x00\x0c\x7f\x38\x49' # | .b....I...O...8I
msg += b'\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8' # | .......8I.......
msg += b'\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00' # | PI...,..c.I.....
msg += b'\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00' # | .g.I......PXI...
msg += b'\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00' # | ...^pI......^.I.
msg += b'\x00\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08' # | .....[PI........
msg += b'\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c' # | I........I......
msg += b'\x50\x59\x49\x00\x00\x00\x2d\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' # | PYI...-...`I....
msg += b'\x00\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff' # | ..#HI......'0I..
msg += b'\xff\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00' # | ....+.L.........
msg += b'\x0c\xa2\x60\x49\x00\x00\x00\x00\x00\x0d\xa0\x48\x49\x00\x00\x00' # | ..`I.......HI...
msg += b'\x00\x00\x0d\xa4\x30\x49\x00\x00\x00\xff\x00\x0d\xa8\x18\x49\x00' # | ....0I........I.
msg += b'\x00\x00\xff'
return msg
@pytest.fixture
def msg_inverter_ms3000_ind(): # Data indication from the controller
msg = b'\x00\x00\x08\xff\x10R170000000000001\x91\x04\x01\x90\x00\x00\x10R170000000000001'
msg += b'\x01\x00\x00\x01'
msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
msg += b'\x53\x00\x66' # | S.f'
return msg
def test_read_message(msg_contact_info):
Config.act_config = {'tsun':{'enabled': True}}
m = MemoryStream(msg_contact_info, (0,))
@@ -1570,6 +1801,64 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
m.close()
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
def test_msg_inv_ind4(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack):
'''Check sonar_lists of MS-3000 inverter'''
_ = config_tsun_inv1
tracer.setLevel(logging.DEBUG)
with patch.object(logging, 'warning') as spy:
m = MemoryStream(msg_inverter_ms3000_ind, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Invalid_Data_Type'] = 0
m.read() # read complete msg, and dispatch msg
spy.assert_not_called()
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==4
assert m.header_len==23
assert m.data_len==2284
m.ts_offset = 0
m._update_header(m.ifc.fwd_fifo.peek())
assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind
assert m.ifc.tx_fifo.get()==msg_inverter_ack
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
assert m.db.get_db_value(Register.TS_GRID) == 1739866976
m.db.db['grid'] = {'Output_Power': 100}
m.close()
def test_msg_inv_ind5(config_tsun_inv1, msg_inverter_ms3000_ind, msg_inverter_ack):
'''Check that unexpected sonar_lists will log a warning'''
_ = config_tsun_inv1
tracer.setLevel(logging.DEBUG)
with patch.object(logging, 'warning') as spy:
m = MemoryStream(msg_inverter_ms3000_ind, (0,))
m.sensor_list = 0x01900002 # change the expected sensor_list
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Invalid_Data_Type'] = 0
m.read() # read complete msg, and dispatch msg
spy.assert_called()
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert int(m.ctrl)==145
assert m.msg_id==4
assert m.header_len==23
assert m.data_len==2284
m.ts_offset = 0
m._update_header(m.ifc.fwd_fifo.peek())
assert m.ifc.fwd_fifo.get()==msg_inverter_ms3000_ind
assert m.ifc.tx_fifo.get()==msg_inverter_ack
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
assert m.db.get_db_value(Register.TS_GRID) == 1739866976
m.db.db['grid'] = {'Output_Power': 100}
m.close()
def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
_ = config_tsun_inv1
@@ -1614,6 +1903,18 @@ def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
def test_build_modell_3000(config_tsun_allow_all, msg_inverter_ms3000_ind):
_ = config_tsun_allow_all
m = MemoryStream(msg_inverter_ms3000_ind, (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 3000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert 0 == m.db.get_db_value(Register.RATED_POWER, 0)
assert 'TSOL-MS3000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
m.close()
def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
_ = config_tsun_inv1
m = MemoryStream(msg_ota_req, (0,), False)
@@ -2184,6 +2485,56 @@ async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
assert next(m.mb_timer.exp_count) == 4
m.close()
@pytest.mark.asyncio
async def test_modbus_scaning(config_tsun_inv1, msg_inverter_ind, msg_modbus_rsp21):
_ = config_tsun_inv1
assert asyncio.get_running_loop()
m = MemoryStream(msg_inverter_ind, (0x8f,0))
m.append_msg(msg_modbus_rsp21)
m.mb_scan = True
m.mb_start_reg = 0x4560
m.mb_bytes = 0x14
assert asyncio.get_running_loop() == m.mb_timer.loop
m.db.stat['proxy']['Unknown_Ctrl'] = 0
assert m.mb_timer.tim == None
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
assert m.msg_recvd[0]['ctrl']==145
assert m.msg_recvd[0]['msg_id']==4
assert m.msg_recvd[0]['header_len']==23
assert m.msg_recvd[0]['data_len']==120
assert m.ifc.fwd_fifo.get()==msg_inverter_ind
assert m.ifc.tx_fifo.get()==b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.ifc.tx_clear() # clear send buffer for next test
assert isclose(m.mb_timeout, 0.5)
assert next(m.mb_timer.exp_count) == 0
await asyncio.sleep(0.5)
assert m.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x03\x45\x60\x00\x14\x50\xd7'
assert m.ifc.tx_fifo.get()==b''
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.msg_recvd[1]['ctrl']==145
assert m.msg_recvd[1]['msg_id']==119
assert m.msg_recvd[1]['header_len']==23
assert m.msg_recvd[1]['data_len']==50
assert m.mb.last_addr == 1
assert m.mb.last_fcode == 3
assert m.mb.last_reg == 0x4560
assert m.mb.last_len == 20
assert m.mb.err == 0
assert next(m.mb_timer.exp_count) == 2
m.close()
def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf):
_ = config_tsun_allow_all
m = MemoryStream(broken_recv_buf, (0,))

View File

@@ -1,2 +1,3 @@
.data.json
config.yaml
config.yaml
apparmor.txt

View File

@@ -8,23 +8,71 @@ JINJA = jinja2
IMAGE = tsun-gen3-addon
# Folders
# Source folders for building the local add-on
SRC=../app
SRC_PROXY=$(SRC)/src
CNF_PROXY=$(SRC)/config
# Target folders for building the local add-on and the docker container
ADDON_PATH = ha_addon
DST=$(ADDON_PATH)/rootfs
DST_PROXY=$(DST)/home/proxy
INST_BASE=../../ha-addons/ha-addons
# base director of the add-on repro for installing the add-on git repros
INST_BASE=../../ha-addons
# Template folder for build the config.yaml variants
TEMPL=templates
# help variable STAGE determine the target to build
STAGE=dev
debug : STAGE=debug
rc : STAGE=rc
rel : STAGE=rel
export BUILD_DATE := ${shell date -Iminutes}
BUILD_ID := ${shell date +'%y%m%d%H%M'}
VERSION := $(shell cat $(SRC)/.version)
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
build: local_add_on
dev debug: local_add_on
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
export VERSION=$(VERSION)-$@-$(BUILD_ID) && \
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
rc rel: local_add_on
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
export VERSION=$(VERSION)-$@ && \
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
clean:
rm -r -f $(DST_PROXY)
rm -f $(DST)/requirements.txt
rm -f $(ADDON_PATH)/config.yaml
rm -f $(TEMPL)/.data.json
docker logout ghcr.io
#############
# Build the local add-on with a rootfs and config.yaml
# The rootfs is needed to build the add-on Docker container
#
local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt
# collect source files
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
$(wildcard $(SRC_PROXY)/*.ini)\
$(wildcard $(SRC_PROXY)/cnf/*.py)\
$(wildcard $(SRC_PROXY)/cnf/*.toml)\
$(wildcard $(SRC_PROXY)/gen3/*.py)\
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
@@ -33,49 +81,8 @@ CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
export BUILD_DATE := ${shell date -Iminutes}
VERSION := $(shell cat $(SRC)/.version)
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
dev debug: build
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
export VERSION=$(VERSION)-$@ && \
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
rc rel: build
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
export VERSION=$(VERSION)-$@ && \
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
build: rootfs $(ADDON_PATH)/config.yaml repro
clean:
rm -r -f $(DST_PROXY)
rm -f $(DST)/requirements.txt
rm -f $(ADDON_PATH)/config.yaml
rm -f $(TEMPL)/.data.json
#
# Build rootfs and config.yaml as local add-on
# The rootfs is needed to build the add-on Dockercontainers
#
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
STAGE=dev
debug : STAGE=debug
rc : STAGE=rc
rel : STAGE=rel
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
@echo Copy $< to $@
@mkdir -p $(@D)
@@ -91,46 +98,84 @@ $(DST)/requirements.txt : $(SRC)/requirements.txt
@cp $< $@
$(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json
$(JINJA) --strict -D AppVersion=$(VERSION) --format=json $^ -o $@
$(JINJA) --strict -D AppVersion=$(VERSION) -D BuildID=$(BUILD_ID) --format=json $^ -o $@
$(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json
$(JINJA) --strict --format=json $^ -o $@
# build a common data.json file from STAGE depending source files
# don't touch the destination if the checksum of src and dst is equal
$(TEMPL)/.data.json: FORCE
rsync --checksum $(TEMPL)/$(STAGE)_data.json $@
FORCE : ;
#
# Build repository for Home Assistant Add-On
#############
# Build repository for Home Assistant Add-Onx
#
INST=$(INST_BASE)/ha_addon_dev
repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml
repro_root = CHANGELOG.md
repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml rootfs/run.sh
repro_root = CHANGELOG.md LICENSE.md
repro_templates = config.yaml
repro_subdirs = translations
repro_vers = debug dev rel
repro_apparmor = apparmor.txt
repro_subdirs = translations rootfs
repro_vers = debug dev rc rel
repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_apparmor := $(foreach dir,$(repro_vers), $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro: $(repro_all_subdirs) $(repro_all_templates) $(repro_all_files) $(repro_root_files)
debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_debug/$(file))
dev: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_dev/$(file))
rc: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rc/$(file))
rel: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rel/$(file))
$(repro_all_subdirs) :
mkdir -p $@
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version
$(JINJA) --strict -D AppVersion=$(VERSION)-$* $< $(filter %.json,$^) -o $@
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version FORCE
$(JINJA) --strict -D AppVersion=$(VERSION)-$* -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@
$(repro_root_files) : %/CHANGELOG.md : ../CHANGELOG.md
$(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json
$(JINJA) --strict $< $(filter %.json,$^) -o $@
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../%
cp $< $@
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../%
cp $< $@
$(filter $(INST_BASE)/ha_addon_rc/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rc/% : ../%
cp $< $@
$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_rel/% : ../%
cp $< $@
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_debug/% : ha_addon/%
cp $< $@
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_dev/% : ha_addon/%
cp $< $@
$(filter $(INST_BASE)/ha_addon_rc/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rc/% : ha_addon/%
cp $< $@
$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rel/% : ha_addon/%
cp $< $@

View File

@@ -74,12 +74,12 @@ target "_prod" {
}
target "debug" {
inherits = ["_common", "_debug"]
tags = ["${IMAGE}:debug"]
tags = ["${IMAGE}:debug", "${IMAGE}:${VERSION}"]
}
target "dev" {
inherits = ["_common"]
tags = ["${IMAGE}:dev"]
tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"]
}
target "preview" {

View File

@@ -67,9 +67,9 @@ Example add-on configuration for GEN3PLUS inverters:
```yaml
inverters:
- serial: Y17000000000000
monitor_sn: '2000000000'
node_id: PV-Garage
suggested_area: Garage
monitor_sn: 2000000000
node_id: inv_1
suggested_area: Roof
modbus_polling: true
client_mode.host: 192.168.x.x
client_mode.port: 8899
@@ -84,6 +84,21 @@ inverters:
pv4.type: SF-M18/144550
```
Example add-on configuration for GEN3PLUS energie storages:
```yaml
batteries:
- serial: 4100000000000000
monitor_sn: 2300000000
node_id: bat_1
suggested_area: Garage
modbus_polling: false
pv1.manufacturer: Shinefar
pv1.type: SF-M18/144550
pv2.manufacturer: Shinefar
pv2.type: SF-M18/144550
```
**Note**: _This is just an example, you need to replace the values with your own!_
more information about the configuration can be found in the [configuration details page][configdetails].
@@ -159,4 +174,4 @@ SOFTWARE.
[AdGuard]: https://github.com/hassio-addons/addon-adguard-home
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fs-allius%2Fha-addons
[configdetails]: https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
[configdetails]: https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-toml

View File

@@ -13,12 +13,12 @@
# 1 Build Base Image #
######################
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.0.1"
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.2"
# hadolint ignore=DL3006
FROM $BUILD_FROM AS base
# Installiere Python, pip und virtuelle Umgebungstools
RUN apk add --no-cache python3=3.12.8-r1 py3-pip=24.3.1-r0 && \
RUN apk add --no-cache python3=3.12.9-r0 py3-pip=24.3.1-r0 && \
python -m venv /opt/venv && \
. /opt/venv/bin/activate
@@ -47,6 +47,8 @@ FROM base AS runtime
ARG SERVICE_NAME
ARG VERSION
ARG LOG_LVL=INFO
ENV LOG_LVL=$LOG_LVL
ENV SERVICE_NAME=${SERVICE_NAME}

View File

@@ -10,8 +10,21 @@ configuration:
Weitere wechselrichterspezifische Parameter (z.B. Polling Mode) können im
Konfigurationsblock gesetzt werden.
Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` und die der GEN3PLUS
Wechselrichter mir `Y17`oder `47`!
Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` oder `R47` und die der GEN3PLUS
Wechselrichter mit `Y17`oder `Y47`!
Siehe Beispielkonfiguration im Dokumentations-Tab
batteries:
name: Batterien
description: >+
Für jeden Energiespeicher muss die Seriennummer des Speichers einer MQTT
Definition zugeordnet werden. Dazu wird der entsprechende Konfigurationsblock mit der
16-stellige Seriennummer gestartet, so dass alle nachfolgenden Parameter diesem
Speicher zugeordnet sind.
Weitere speicherspezifische Parameter (z.B. Polling Mode) können im
Konfigurationsblock gesetzt werden.
Die Seriennummer der GEN3PLUS Batteriespeicher beginnen mit `410`!
Siehe Beispielkonfiguration im Dokumentations-Tab
@@ -25,14 +38,14 @@ configuration:
ein => normaler Proxy-Betrieb.
aus => Der Wechselrichter wird vom Internet isoliert.
solarman.enabled:
name: Verbindung zur Solarman Cloud - nur für GEN3PLUS Wechselrichter
name: Verbindung zur Solarman/TSUN Cloud - nur für GEN3PLUS Wechselrichter
description: >+
Schaltet die Verbindung zur Solarman Cloud ein/aus.
Diese Verbindung ist erforderlich, wenn Sie Daten an die Solarman Cloud senden möchten,
z.B. um die Solarman Apps zu nutzen oder Firmware-Updates zu erhalten.
Schaltet die Verbindung zur Solarman oder TSUN Cloud ein/aus.
Diese Verbindung ist erforderlich, wenn Sie Daten an die Cloud senden möchten,
z.B. um die Solarman App oder TSUN Smart App zu nutzen oder Firmware-Updates zu erhalten.
ein => normaler Proxy-Betrieb.
aus => Der Wechselrichter wird vom Internet isoliert.
aus => Die GEN3PLUS Geräte werden vom Internet isoliert.
inverters.allow_all:
name: Erlaube Verbindungen von sämtlichen Wechselrichtern
description: >-

View File

@@ -7,13 +7,27 @@ configuration:
definition. To do this, the corresponding configuration block is started with
16-digit serial number so that all subsequent parameters are assigned
to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
in the configuration block
in the configuration block.
The serial numbers of all GEN3 inverters start with `R17` and that of the GEN3PLUS
inverters with Y17 or 47!
The serial numbers of all GEN3 inverters start with `R17` or `R47` and that of the GEN3PLUS
inverters with Y17 or Y47!
For reference see example configuration in Documentation Tab
batteries:
name: Energy Storages
description: >+
For each energy storage device, the serial number of the storage device must be
assigned to an MQTT definition. To do this, the corresponding configuration block
is started with the 16-digit serial number so that all subsequent parameters are
assigned to this energy storage. Further inverter-specific parameters (e.g. polling
mode) can be set in the configuration block.
The serial numbers of all GEN3PLUS energy storages start with 410!
For reference see example configuration in Documentation Tab
tsun.enabled:
name: Connection to TSUN Cloud - for GEN3 inverter only
description: >+
@@ -24,14 +38,14 @@ configuration:
on => normal proxy operation.
off => The Inverter become isolated from Internet.
solarman.enabled:
name: Connection to Solarman Cloud - for GEN3PLUS inverter only
name: Connection to Solarman/TSUN Cloud - for GEN3PLUS inverter only
description: >+
switch on/off connection to the Solarman cloud.
This connection is only required if you want send data to the Solarman cloud
eg. to use the Solarman APPs or receive firmware updates.
switch on/off connection to the Solarman or TSUN cloud.
This connection is only required if you want send data to the cloud
eg. to use the Solarman APP, the TSUN Smart APP or receive firmware updates.
on => normal proxy operation.
off => The Inverter become isolated from Internet
off => The GEN3PLUS devices become isolated from Internet
inverters.allow_all:
name: Allow all connections from all inverters
description: >-

View File

@@ -0,0 +1,52 @@
#include <tunables/global>
profile {{slug}} flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Capabilities
file,
signal (send) set=(kill,term,int,hup,cont),
# S6-Overlay
/init ix,
/bin/** ix,
/usr/bin/** ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/run/{,**} rwk,
/dev/tty rw,
# Bashio
/usr/lib/bashio/** ix,
/tmp/** rwk,
# Access to options.json and other files within your addon
/data/** rw,
# Start new profile for service
/usr/bin/myprogram cx -> myprogram,
profile myprogram flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Receive signals from S6-Overlay
signal (receive) peer=*_{{slug}},
# Access to options.json and other files within your addon
/data/** rw,
# Access to mapped volumes specified in config.json
/share/** rw,
# Access required for service functionality
/usr/bin/myprogram r,
/bin/bash rix,
/bin/echo ix,
/etc/passwd r,
/dev/tty rw,
}
}

View File

@@ -1,7 +1,7 @@
name: {{name}}
description: {{description}}
version: {% if version is defined and version|length %} {{version}} {% else %} {{AppVersion}} {% endif %}
image: docker.io/sallius/tsun-gen3-addon
version: {% if version is defined and version|length %} {{version}} {% elif BuildID is defined and BuildID|length %} {{AppVersion}}-{{BuildID}} {% else %} {{AppVersion}} {% endif %}
image: {{image}}
url: https://github.com/s-allius/tsun-gen3-proxy
slug: {{slug}}
advanced: {{advanced}}
@@ -24,29 +24,25 @@ ports:
5005/tcp: 5005
10000/tcp: 10000
# FIXME: we disabled the watchdog due to exceptions in the ha supervisor. See: https://github.com/s-allius/tsun-gen3-proxy/issues/249
# watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
# Definition of parameters in the configuration tab of the addon
# parameters are available within the container as /data/options.json
# and should become picked up by the proxy - current workaround as a transfer script
# TODO: check again for multi hierarchie parameters
# TODO: implement direct reading of the configuration file
schema:
inverters:
- serial: str
- serial: match(^(R17|R47|Y17|Y47).{13}$)
monitor_sn: int?
node_id: str
suggested_area: str
modbus_polling: bool
client_mode.host: str?
client_mode.port: int?
client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)?
client_mode.port: port?
client_mode.forward: bool?
#strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt.
# - string: str
# type: str
# manufacturer: str
# daher diese variante
modbus_scanning.start: int(0,65535)?
modbus_scanning.step: int(0,65535)?
modbus_scanning.bytes: int(1,80)?
pv1.manufacturer: str?
pv1.type: str?
pv2.manufacturer: str?
@@ -62,10 +58,24 @@ schema:
tsun.enabled: bool
solarman.enabled: bool
inverters.allow_all: bool
batteries:
- serial: match(^(410).{13}$)
monitor_sn: int
node_id: str
suggested_area: str
modbus_polling: bool
client_mode.host: match(\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b)?
client_mode.port: port?
client_mode.forward: bool?
pv1.manufacturer: str?
pv1.type: str?
pv2.manufacturer: str?
pv2.type: str?
# optionale parameter
# TODO besser strukturieren und vervollständigen
mqtt.host: str?
mqtt.port: int?
mqtt.port: port?
mqtt.user: str?
mqtt.passwd: password?
ha.auto_conf_prefix: str? # suggeriert optionale konfigurationsoption -> es darf jedoch kein default unter "options" angegeben werden
@@ -89,17 +99,21 @@ schema:
# If any default value is given, the option becomes a required value.
options:
inverters:
- serial: R17E760702080400
node_id: PV-Garage
- serial: R17E000000000000
monitor_sn: 0
node_id: inv_1
suggested_area: Roof
modbus_polling: false
pv1.manufacturer: Shinefar
pv1.type: SF-M18/144550
pv2.manufacturer: Shinefar
pv2.type: SF-M18/144550
batteries:
- serial: 4100000000000000
monitor_sn: 0
node_id: bat_1
suggested_area: Garage
modbus_polling: false
# strings:
# - string: PV1
# type: SF-M18/144550
# manufacturer: Shinefar
# - string: PV2
# type: SF-M18/144550
# manufacturer: Shinefar
pv1.manufacturer: Shinefar
pv1.type: SF-M18/144550
pv2.manufacturer: Shinefar

View File

@@ -2,7 +2,7 @@
{
"name": "TSUN-Proxy (Debug)",
"description": "MQTT Proxy for TSUN Photovoltaic Inverters with Debug Logging",
"version": "debug",
"image": "docker.io/sallius/tsun-gen3-addon",
"slug": "tsun-proxy-debug",
"advanced": true,
"stage": "experimental"

View File

@@ -2,7 +2,7 @@
{
"name": "TSUN-Proxy (Dev)",
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
"version": "dev",
"image": "docker.io/sallius/tsun-gen3-addon",
"slug": "tsun-proxy-dev",
"advanced": false,
"stage": "experimental"

View File

@@ -0,0 +1,10 @@
{
"name": "TSUN-Proxy (Release Candidate)",
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
"version": "rc",
"image": "ghcr.io/s-allius/tsun-gen3-addon",
"slug": "tsun-proxy-rc",
"advanced": true,
"stage": "experimental"
}

View File

@@ -2,6 +2,7 @@
{
"name": "TSUN-Proxy",
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
"image": "ghcr.io/s-allius/tsun-gen3-addon",
"slug": "tsun-proxy",
"advanced": false,
"stage": "stable"

View File

@@ -15,6 +15,10 @@ sonar.sources=app/src/
sonar.python.version=3.12
sonar.tests=system_tests/,app/tests/
sonar.exclusions=**/.vscode/**/*
# disable code dupication check for config grammar
sonar.cpd.exclusions=app/src/cnf/config.py
# Name your criteria
sonar.issue.ignore.multicriteria=e1,e2

View File

@@ -93,6 +93,155 @@ def msg_inverter_ind(): # Data indication from the inverter
msg += b'\x53\x00\x00'
return msg
@pytest.fixture
def msg_inverter_ind2(): # Data indication from the inverter
msg = b'\x00\x00\x08\xff\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x00\x10'+get_inv_no()
msg += b'\x01\x00\x00\x01'
msg += b'\x95\x18\x5e\x1d\x80\x00\x00\x01\x2c\x00\x00\x00\x64\x53\x00\x00' # | ..^.....,...dS..
msg += b'\x00\x00\x00\xc8\x53\x44\x00\x00\x00\x01\x2c\x53\x00\x00\x00\x00' # | ....SD....,S....
msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S....
msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01' # | ...S......S.....
msg += b'\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00' # | ....S......I....
msg += b'\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00\x00\x02\x02\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00' # | ...XI.......YS..
msg += b'\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00' # | ...ZS.....[S....
msg += b'\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e' # | .\S.....]S.....^
msg += b'\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02\x60\x53\x00' # | S....._S.....`S.
msg += b'\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00' # | ....aS.....bS...
msg += b'\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02' # | ..cS.....dS.....
msg += b'\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53' # | eS.....fS.....gS
msg += b'\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00' # | .....hS......I..
msg += b'\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00\x02\xc5\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02' # | ...S......S.....
msg += b'\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x03\x20\x53\x00\x01\x00\x00\x03\x84\x53\x11\x68' # | ..... S......S.h
msg += b'\x00\x00\x03\xe8\x46\x44\x23\xd1\xec\x00\x00\x04\x4c\x46\x43\xa3' # | ....FD#.....LFC.
msg += b'\xb3\x33\x00\x00\x04\xb0\x46\x00\x00\x00\x00\x00\x00\x05\x14\x46' # | .3....F........F
msg += b'\x43\x6e\x80\x00\x00\x00\x05\x78\x46\x3d\x4c\xcc\xcd\x00\x00\x05' # | Cn.....xF=L.....
msg += b'\xdc\x46\x00\x00\x00\x00\x00\x00\x06\x40\x46\x42\x48\x00\x00\x00' # | .F.......@FBH...
msg += b'\x00\x06\xa4\x53\x00\x03\x00\x00\x07\x08\x53\x00\x0c\x00\x00\x07' # | ...S......S.....
msg += b'\x6c\x53\x00\x50\x00\x00\x07\xd0\x46\x43\xa3\xb3\x33\x00\x00\x08' # | lS.P....FC..3...
msg += b'\x34\x53\x0b\xb8\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08' # | 4S......F.......
msg += b'\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x41\xee\xe1\x48\x00' # | .F.......`FA..H.
msg += b'\x00\x09\xc4\x53\x00\x00\x00\x00\x0a\x28\x46\x41\xf2\x00\x00\x00' # | ...S.....(FA....
msg += b'\x00\x0a\x8c\x46\x3f\xac\x28\xf6\x00\x00\x0a\xf0\x53\x00\x0c\x00' # | ...F?.(.....S...
msg += b'\x00\x0b\x54\x53\x00\x00\x00\x00\x0b\xb8\x53\x00\x00\x00\x00\x0c' # | ..TS......S.....
msg += b'\x1c\x53\x00\x00\x00\x00\x0c\x80\x53\x00\x00\x00\x00\x0c\xe4\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x0d\x48\x53\x00\x00\x00\x00\x0d\xac\x53\x00\x00' # | .....HS......S..
msg += b'\x00\x00\x0e\x10\x53\x00\x00\x00\x00\x0e\x74\x53\x00\x00\x00\x00' # | ....S.....tS....
msg += b'\x0e\xd8\x53\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0' # | ..S.....<S......
msg += b'\x53\x00\x00\x00\x00\x10\x04\x53\x00\x00\x00\x00\x10\x68\x53\x00' # | S......S.....hS.
msg += b'\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00' # | .....S.....0S...
msg += b'\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\x00\x00\x00\x00\x12' # | ...S......S.....
msg += b'\x5c\x53\x00\x00\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x46' # | \S......S.....$F
msg += b'\x42\x9d\x33\x33\x00\x00\x13\x88\x46\x00\x00\x00\x00\x00\x00\x13' # | B.33....F.......
msg += b'\xec\x46\x00\x00\x00\x00\x00\x00\x14\x50\x46\x42\xdc\x00\x00\x00' # | .F.......PFB....
msg += b'\x00\x14\xb4\x53\x00\x00\x00\x00\x15\x18\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x7c\x53\x00\x00\x00\x00\x15\x7d\x53\x00\x00\x00\x00\x15\x7e\x53' # | |S.....}S.....~S
msg += b'\x00\x00\x00\x00\x15\x7f\x53\x00\x00\x00\x00\x15\x80\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x15\x81\x53\x00\x00\x00\x00\x15\x82\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x15\x83\x53\x00\x00\x00\x00\x15\x84\x53\x00\x00\x00\x00\x15\x85' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x15\x86\x53\x00\x00\x00\x00\x15\x87\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x15\x88\x53\x00\x00\x00\x00\x15\x89\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x15\x8a\x53\x00\x00\x00\x00\x15\x8b\x53\x00\x00\x00\x00\x15' # | ...S......S.....
msg += b'\x8c\x53\x00\x00\x00\x00\x15\xe0\x46\x42\x68\x66\x66\x00\x00\x16' # | .S......FBhff...
msg += b'\x44\x46\x00\x00\x00\x00\x00\x00\x16\xa8\x46\x00\x00\x00\x00\x00' # | DF........F.....
msg += b'\x00\x17\x0c\x46\x42\xdc\x00\x00\x00\x00\x17\x70\x53\x00\x00\x00' # | ...FB......pS...
msg += b'\x00\x17\xd4\x53\x00\x00\x00\x00\x18\x38\x53\x00\x00\x00\x00\x18' # | ...S.....8S.....
msg += b'\x39\x53\x00\x00\x00\x00\x18\x3a\x53\x00\x00\x00\x00\x18\x3b\x53' # | 9S.....:S.....;S
msg += b'\x00\x00\x00\x00\x18\x3c\x53\x00\x00\x00\x00\x18\x3d\x53\x00\x00' # | .....<S.....=S..
msg += b'\x00\x00\x18\x3e\x53\x00\x00\x00\x00\x18\x3f\x53\x00\x00\x00\x00' # | ...>S.....?S....
msg += b'\x18\x40\x53\x00\x00\x00\x00\x18\x41\x53\x00\x00\x00\x00\x18\x42' # | .@S.....AS.....B
msg += b'\x53\x00\x00\x00\x00\x18\x43\x53\x00\x00\x00\x00\x18\x44\x53\x00' # | S.....CS.....DS.
msg += b'\x00\x00\x00\x18\x45\x53\x00\x00\x00\x00\x18\x46\x53\x00\x00\x00' # | ....ES.....FS...
msg += b'\x00\x18\x47\x53\x00\x00\x00\x00\x18\x48\x53\x00\x00\x00\x00\x18' # | ..GS.....HS.....
msg += b'\x9c\x46\x42\x6b\x33\x33\x00\x00\x19\x00\x46\x00\x00\x00\x00\x00' # | .FBk33....F.....
msg += b'\x00\x19\x64\x46\x00\x00\x00\x00\x00\x00\x19\xc8\x46\x42\xdc\x00' # | ..dF........FB..
msg += b'\x00\x00\x00\x1a\x2c\x53\x00\x00\x00\x00\x1a\x90\x53\x00\x00\x00' # | ....,S......S...
msg += b'\x00\x1a\xf4\x53\x00\x00\x00\x00\x1a\xf5\x53\x00\x00\x00\x00\x1a' # | ...S......S.....
msg += b'\xf6\x53\x00\x00\x00\x00\x1a\xf7\x53\x00\x00\x00\x00\x1a\xf8\x53' # | .S......S......S
msg += b'\x00\x00\x00\x00\x1a\xf9\x53\x00\x00\x00\x00\x1a\xfa\x53\x00\x00' # | ......S......S..
msg += b'\x00\x00\x1a\xfb\x53\x00\x00\x00\x00\x1a\xfc\x53\x00\x00\x00\x00' # | ....S......S....
msg += b'\x1a\xfd\x53\x00\x00\x00\x00\x1a\xfe\x53\x00\x00\x00\x00\x1a\xff' # | ..S......S......
msg += b'\x53\x00\x00\x00\x00\x1b\x00\x53\x00\x00\x00\x00\x1b\x01\x53\x00' # | S......S......S.
msg += b'\x00\x00\x00\x1b\x02\x53\x00\x00\x00\x00\x1b\x03\x53\x00\x00\x00' # | .....S......S...
msg += b'\x00\x1b\x04\x53\x00\x00\x00\x00\x1b\x58\x53\x00\x00\x00\x00\x1b' # | ...S.....XS.....
msg += b'\xbc\x53\x11\x3d\x00\x00\x1c\x20\x46\x3c\x23\xd7\x0a\x00\x00\x1c' # | .S.=... F<#.....
msg += b'\x84\x46\x00\x00\x00\x00\x00\x00\x1c\xe8\x46\x42\x04\x00\x00\x00' # | .F........FB....
msg += b'\x00\x1d\x4c\x46\x00\x00\x00\x00\x00\x00\x1d\xb0\x46\x00\x00\x00' # | ..LF........F...
msg += b'\x00\x00\x00\x1e\x14\x53\x00\x02\x00\x00\x1e\x78\x46\x41\x8b\x33' # | .....S.....xFA.3
msg += b'\x33\x00\x00\x1e\xdc\x46\x3c\xa3\xd7\x0a\x00\x00\x1f\x40\x46\x3e' # | 3....F<......@F>
msg += b'\x99\x99\x9a\x00\x00\x1f\xa4\x46\x40\x99\x99\x9a\x00\x00\x20\x08' # | .......F@..... .
msg += b'\x53\x00\x00\x00\x00\x20\x6c\x53\x00\x00\x00\x00\x20\xd0\x53\x05' # | S.... lS.... .S.
msg += b'\x00\x00\x00\x20\xd1\x53\x00\x00\x00\x00\x20\xd2\x53\x00\x00\x00' # | ... .S.... .S...
msg += b'\x00\x20\xd3\x53\x00\x00\x00\x00\x20\xd4\x53\x00\x00\x00\x00\x20' # | . .S.... .S....
msg += b'\xd5\x53\x00\x00\x00\x00\x20\xd6\x53\x00\x00\x00\x00\x20\xd7\x53' # | .S.... .S.... .S
msg += b'\x00\x00\x00\x00\x20\xd8\x53\x00\x00\x00\x00\x20\xd9\x53\x00\x01' # | .... .S.... .S..
msg += b'\x00\x00\x20\xda\x53\x00\x00\x00\x00\x20\xdb\x53\x00\x01\x00\x00' # | .. .S.... .S....
msg += b'\x20\xdc\x53\x00\x00\x00\x00\x20\xdd\x53\x00\x00\x00\x00\x20\xde' # | .S.... .S.... .
msg += b'\x53\x00\x00\x00\x00\x20\xdf\x53\x00\x00\x00\x00\x20\xe0\x53\x00' # | S.... .S.... .S.
msg += b'\x00\x00\x00\x21\x34\x46\x00\x00\x00\x00\x00\x00\x21\x98\x46\x00' # | ...!4F......!.F.
msg += b'\x00\x00\x00\x00\x00\x21\xfc\x46\x00\x00\x00\x00\x00\x00\x22\x60' # | .....!.F......"`
msg += b'\x46\x00\x00\x00\x00\x00\x00\x22\xc4\x53\x00\x00\x00\x00\x23\x28' # | F......".S....#(
msg += b'\x53\x00\x00\x00\x00\x23\x8c\x53\x00\x00\x00\x00\x23\x8d\x53\x00' # | S....#.S....#.S.
msg += b'\x00\x00\x00\x23\x8e\x53\x00\x00\x00\x00\x23\x8f\x53\x00\x00\x00' # | ...#.S....#.S...
msg += b'\x00\x23\x90\x53\x00\x00\x00\x00\x23\x91\x53\x00\x00\x00\x00\x23' # | .#.S....#.S....#
msg += b'\x92\x53\x00\x00\x00\x00\x23\x93\x53\x00\x00\x00\x00\x23\x94\x53' # | .S....#.S....#.S
msg += b'\x00\x00\x00\x00\x23\x95\x53\x00\x00\x00\x00\x23\x96\x53\x00\x00' # | ....#.S....#.S..
msg += b'\x00\x00\x23\x97\x53\x00\x00\x00\x00\x23\x98\x53\x00\x00\x00\x00' # | ..#.S....#.S....
msg += b'\x23\x99\x53\x00\x00\x00\x00\x23\x9a\x53\x00\x00\x00\x00\x23\x9b' # | #.S....#.S....#.
msg += b'\x53\x00\x00\x00\x00\x23\x9c\x53\x00\x00\x00\x00\x23\xf0\x46\x00' # | S....#.S....#.F.
msg += b'\x00\x00\x00\x00\x00\x24\x54\x46\x00\x00\x00\x00\x00\x00\x24\xb8' # | .....$TF......$.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x25\x1c\x46\x00\x00\x00\x00\x00\x00' # | F......%.F......
msg += b'\x25\x80\x53\x00\x00\x00\x00\x25\xe4\x53\x00\x00\x00\x00\x26\x48' # | %.S....%.S....&H
msg += b'\x53\x00\x00\x00\x00\x26\x49\x53\x00\x00\x00\x00\x26\x4a\x53\x00' # | S....&IS....&JS.
msg += b'\x00\x00\x00\x26\x4b\x53\x00\x00\x00\x00\x26\x4c\x53\x00\x00\x00' # | ...&KS....&LS...
msg += b'\x00\x26\x4d\x53\x00\x00\x00\x00\x26\x4e\x53\x00\x00\x00\x00\x26' # | .&MS....&NS....&
msg += b'\x4f\x53\x00\x00\x00\x00\x26\x50\x53\x00\x00\x00\x00\x26\x51\x53' # | OS....&PS....&QS
msg += b'\x00\x00\x00\x00\x26\x52\x53\x00\x00\x00\x00\x26\x53\x53\x00\x00' # | ....&RS....&SS..
msg += b'\x00\x00\x26\x54\x53\x00\x00\x00\x00\x26\x55\x53\x00\x00\x00\x00' # | ..&TS....&US....
msg += b'\x26\x56\x53\x00\x00\x00\x00\x26\x57\x53\x00\x00\x00\x00\x26\x58' # | &VS....&WS....&X
msg += b'\x53\x00\x00\x00\x00\x26\xac\x53\x00\x00\x00\x00\x27\x10\x53\x11' # | S....&.S....'.S.
msg += b'\x3d\x00\x00\x27\x74\x46\x00\x00\x00\x00\x00\x00\x27\xd8\x46\x00' # | =..'tF......'.F.
msg += b'\x00\x00\x00\x00\x00\x28\x3c\x46\x42\x03\xf5\xc3\x00\x00\x28\xa0' # | .....(<FB.....(.
msg += b'\x46\x00\x00\x00\x00\x00\x00\x29\x04\x46\x00\x00\x00\x00\x00\x00' # | F......).F......
msg += b'\x29\x68\x53\x00\x02\x00\x00\x29\xcc\x53\x00\x03\x00\x00\x2a\x30' # | )hS....).S....*0
msg += b'\x46\x42\x20\x00\x00\x00\x00\x2a\x94\x46\x42\x20\x00\x00\x00\x00' # | FB ....*.FB ....
msg += b'\x2a\xf8\x46\x44\x20\x00\x00\x00\x00\x2b\x5c\x46\x43\x7b\x00\x00' # | *.FD ....+\FC{..
msg += b'\x00\x00\x2b\xc0\x46\x43\x50\x00\x00\x00\x00\x2c\x24\x46\x42\x48' # | ..+.FCP....,$FBH
msg += b'\x5c\x29\x00\x00\x2c\x88\x46\x42\x47\xa3\xd7\x00\x00\x2c\xec\x53' # | \)..,.FBG....,.S
msg += b'\x00\x00\x00\x00\x2d\x50\x46\x43\x42\x00\x00\x00\x00\x2d\xb4\x46' # | ....-PFCB....-.F
msg += b'\x42\xbc\x00\x00\x00\x00\x2e\x18\x46\x3f\xe6\x66\x66\x00\x00\x2e' # | B.......F?.ff...
msg += b'\x7c\x46\x3f\xe6\x66\x66\x00\x00\x2e\xe0\x46\x43\x7e\x00\x00\x00' # | |F?.ff....FC~...
msg += b'\x00\x2f\x44\x46\x43\x83\xf3\x33\x00\x00\x2f\xa8\x46\x3f\xe6\x66' # | ./DFC..3../.F?.f
msg += b'\x66\x00\x00\x30\x0c\x46\x3f\xe6\x66\x66\x00\x00\x30\x70\x46\x43' # | f..0.F?.ff..0pFC
msg += b'\x7e\x00\x00\x00\x00\x30\xd4\x46\x42\x3f\xeb\x85\x00\x00\x31\x38' # | ~....0.FB?....18
msg += b'\x46\x42\x3d\xeb\x85\x00\x00\x31\x9c\x46\x3e\x4c\xcc\xcd\x00\x00' # | FB=....1.F>L....
msg += b'\x32\x00\x46\x3e\x4c\xcc\xcd\x00\x00\x32\x64\x46\x42\x4c\x14\x7b' # | 2.F>L....2dFBL.{
msg += b'\x00\x00\x32\xc8\x46\x42\x4d\xeb\x85\x00\x00\x33\x2c\x46\x3e\x4c' # | ..2.FBM....3,F>L
msg += b'\xcc\xcd\x00\x00\x33\x90\x46\x3e\x4c\xcc\xcd\x00\x00\x33\xf4\x53' # | ....3.F>L....3.S
msg += b'\x00\x00\x00\x00\x34\x58\x53\x00\x00\x00\x00\x34\xbc\x53\x04\x00' # | ....4XS....4.S..
msg += b'\x00\x00\x35\x20\x53\x00\x01\x00\x00\x35\x84\x53\x13\x9c\x00\x00' # | ..5 S....5.S....
msg += b'\x35\xe8\x53\x0f\xa0\x00\x00\x36\x4c\x53\x00\x00\x00\x00\x36\xb0' # | 5.S....6LS....6.
msg += b'\x53\x00\x66' # | S.f'
return msg
@pytest.fixture
def msg_ota_update_req(): # Over the air update request from talent cloud
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
@@ -234,3 +383,18 @@ def test_ota_req(client_connection, msg_ota_update_req):
_ = s.recv(1024)
except TimeoutError:
pass
def test_send_inv_data2(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inverter_ind2):
s = client_connection
try:
s.sendall(msg_timestamp_req)
_ = s.recv(1024)
except TimeoutError:
pass
# time.sleep(32.5)
# assert data == msg_timestamp_resp
try:
s.sendall(msg_inverter_ind2)
_ = s.recv(1024)
except TimeoutError:
pass

View File

@@ -10,6 +10,12 @@ SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
def get_sn() -> bytes:
return bytes.fromhex(SOLARMAN_SNR)
def get_dcu_sn() -> bytes:
return b'\x20\x43\x65\x7b'
def get_dcu_no() -> bytes:
return b'4100000000000001'
def get_inv_no() -> bytes:
return b'T170000000000001'
@@ -105,6 +111,62 @@ def MsgInvalidInfo(): # Contact Info message wrong start byte
msg += b'\x15'
return msg
@pytest.fixture
def dcu_dev_ind_msg(): # 0x4110
msg = b'\xa5\x3a\x01\x10\x41\x00\x01' +get_dcu_sn() +b'\x02\xc6\xde\x2d\x32'
msg += b'\x27\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x5c\x01\x4c\x53'
msg += b'\x57\x35\x5f\x30\x31\x5f\x33\x30\x32\x36\x5f\x4e\x53\x5f\x30\x35'
msg += b'\x5f\x30\x31\x2e\x30\x30\x2e\x30\x30\x2e\x30\x30\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\xd4\x27\x87\x12\xad\xc0\x31\x39\x32\x2e'
msg += b'\x31\x36\x38\x2e\x39\x2e\x31\x34\x00\x00\x00\x00\x01\x00\x01\x26'
msg += b'\x30\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\x7a\x75\x68\x61\x75\x73\x65\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\x08\x01\x01\x01\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x01'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_dev_rsp_msg(): # 0x1110
msg = b'\xa5\x0a\x00\x10\x11\x92\x01' +get_dcu_sn() +b'\x02\x01\x4a\xf6\xa6'
msg += b'\x67\x3c\x00\x00\x00'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_data_ind_msg(): # 0x4210
msg = b'\xa5\x6f\x00\x10\x42\x92\x02' +get_dcu_sn() +b'\x01\x26\x30\xc7\xde'
msg += b'\x2d\x32\x28\x00\x00\x00\x84\x17\x79\x35\x01\x00\x4c\x12\x00\x00'
msg += get_dcu_no()
msg += b'\x0d\x3a\x00\x0a\x0d\x2c\x00\x00\x00\x00\x08\x20\x00\x00\x00\x00'
msg += b'\x14\x0e\xff\xfe\x03\xe8\x0c\x89\x0c\x89\x0c\x89\x0c\x8a\x0c\x89'
msg += b'\x0c\x89\x0c\x8a\x0c\x89\x0c\x89\x0c\x8a\x0c\x8a\x0c\x89\x0c\x89'
msg += b'\x0c\x89\x0c\x89\x0c\x88\x00\x0f\x00\x0f\x00\x0f\x00\x0e\x00\x00'
msg += b'\x00\x00\x00\x0f\x00\x00\x02\x05\x02\x01'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture
def dcu_data_rsp_msg(): # 0x1210
msg = b'\xa5\x0a\x00\x10\x12\x93\x02' +get_dcu_sn() +b'\x01\x01\xd1\x96\x04'
msg += b'\x66\x3c\x00\x00\x00'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture(scope="session")
def ClientConnection():
@@ -181,4 +243,24 @@ def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactR
# time.sleep(2.5)
checkResponse(data, MsgContactResp)
def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
s = ClientConnection
try:
s.sendall(dcu_dev_ind_msg)
# time.sleep(2.5)
data = s.recv(1024)
except TimeoutError:
pass
# time.sleep(2.5)
checkResponse(data, dcu_dev_rsp_msg)
def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg):
s = ClientConnection
try:
s.sendall(dcu_data_ind_msg)
# time.sleep(2.5)
data = s.recv(1024)
except TimeoutError:
pass
# time.sleep(2.5)
checkResponse(data, dcu_data_rsp_msg)

View File

@@ -4,11 +4,11 @@
"path": "."
},
{
"path": "../wiki"
"path": "../tsun-gen3-proxy.wiki"
},
{
"name": "ha-addons",
"path": "../ha-addons/ha-addons"
"path": "../ha-addons"
}
],
"settings": {}