Compare commits

...

219 Commits

Author SHA1 Message Date
Stefan Allius
ec8ec1c855 Update ghcr.io/hassio-addons/base Docker tag to v17.2.5 2025-05-10 20:23:26 +02:00
Stefan Allius
770ae6fc27 S allius/issue398 (#406)
* setup logger for hypercorn and dashboard

* use logger.ini to setup dashboard logger

* workaround: restore the hypercorn logger config

- quart/hyercorn overwrites the logger config.
  as a workaround we restore the config at the
  beginning of a request

* fix the hypercorn log handler only once

* change proxy into a ASGI application

- move Quart init from server.py into app.py
- create Server class for config and loggin setup
- restore hypercorn logging configuration after
  start of Quart/Hypercorn

* move get_log_level into Server class

* define config in test_emu_init_close

* remove Web() instance from the testcase

- with importing app.py The blueprint Web() will
  automatically created and a second call in test-
  cases must avoided

* add unit tests

* move code from app.py into server.py

* test the init_logging_system() method

* add HypercornLogHndl tests

* fix deprecated pytest async warning

- Cleanup pending async tasks
- fix deprecated warning about event_loop

* add unit test for error handling in build_config()

* coverage: ignore quart template files

* check print output in test_save_and_restore

* update changelog
2025-05-10 20:22:25 +02:00
renovate[bot]
b94895a113 Update ghcr.io/hassio-addons/base Docker tag to v17.2.5 2025-05-10 18:20:44 +00:00
Stefan Allius
48965ffda9 S allius/issue398 (#406)
* setup logger for hypercorn and dashboard

* use logger.ini to setup dashboard logger

* workaround: restore the hypercorn logger config

- quart/hyercorn overwrites the logger config.
  as a workaround we restore the config at the
  beginning of a request

* fix the hypercorn log handler only once

* change proxy into a ASGI application

- move Quart init from server.py into app.py
- create Server class for config and loggin setup
- restore hypercorn logging configuration after
  start of Quart/Hypercorn

* move get_log_level into Server class

* define config in test_emu_init_close

* remove Web() instance from the testcase

- with importing app.py The blueprint Web() will
  automatically created and a second call in test-
  cases must avoided

* add unit tests

* move code from app.py into server.py

* test the init_logging_system() method

* add HypercornLogHndl tests

* fix deprecated pytest async warning

- Cleanup pending async tasks
- fix deprecated warning about event_loop

* add unit test for error handling in build_config()

* coverage: ignore quart template files

* check print output in test_save_and_restore

* update changelog
2025-05-10 19:32:13 +02:00
renovate[bot]
f1628a0629 Update dependency aiomqtt to v2.4.0 (#404)
* Update dependency aiomqtt to v2.4.0

* update changelog

---------

Co-authored-by: Stefan Allius <122395479+s-allius@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-05-04 19:20:45 +02:00
Stefan Allius
888e1475e4 S allius/issue397 (#405)
* add Dashboards log handler to all known loggers

* add list of last 3 warnings/errors to page

* add note list to page

* create LogHandler for the dashboard

- simple memory log handler which stores the last
  64 warnings/errors for the dashboard

* render warnings/errors as note list

* add page for warnings and errors

* fix double defined build target

* add well done message if no errors in the logs

* translate page titles

* more translations

* add Notes page and table for important messages

* add unit tests
2025-05-04 18:50:31 +02:00
Stefan Allius
e15db8c92a S allius/issue393 (#403)
* display proxy version on dashboard

* add MQTT page

* styles adjusted on the different pages

- use same colors
- add bordered shadow to all cards and tables

* fix unit tests

* migrate the conn table to a general table

- rename the template file
- get headline from table description

* remove footer from index page

* make version string translateable

* cleanup

* remove stripped table rows

* add mqtt info table

* translate mqtt page

* don't fetch notes list for the log-page

* fix Mqtt init call for unit tests

* add mqtt-fetch test

* check received counter in unit test
2025-05-03 23:45:10 +02:00
Stefan Allius
41515f4be3 S allius/issue401 (#402)
* add route for log file deletion

* add modal for senity check before file deletion

* add trash icon which unhide the modal

* add more translations

* increase test coverage

* cleanup
2025-05-02 19:47:16 +02:00
Stefan Allius
aadbe6855e S allius/issue394 (#400)
* store logging path in Config class

* rename template files and page files

* jump to referer page

- after changing the language, we jump to
  the referer page, if the attribute exists

* build and send list of log-files

* rename Download page into Log files

* initialize log-path in test config

* improve dashboard unit tests

 - add log file tests
 - check content-languages after language switch

* initialize config structure for log-file tests

* add test log file to project

* add sub_dir to test log path

- non files must be skipped. To test this we add
  a sub directory to the test log directory

* add german translations

* set quart debug flag for debug versions

* update changelog
2025-05-01 19:34:46 +02:00
Stefan Allius
7542c112f7 S allius/issue395 (#399)
* add button for languages setting

* build a web module for the dashboard

- load all python module from local dir
- initialize Blueprint and Babel

* set a default key for secure cookies

* add translations to docker container

* improve translation build  

- clean target erases the *.pot
- don't modify the resurt of url_for() calls
- don't translate the language description

* translate connection table, fix icon

* build relative urls for HA ingress

* fix unit test, increase coverage
2025-04-29 00:07:59 +02:00
Stefan Allius
093ec03d60 S allius/issue391 (#392)
* design counter on connection board

* display time of last update and add reload button

* chance `Updated` field to a real button

* Provide counter values for the dashboard

* change background color ot the top branch

- use dark-grey instead of black to reduce the contrast

* change color of counter tiles

* test proxy connection counter handling

* prepare conn-table and notes list building

* remove obsolete menue points

* store client mode for dashboard

* store inverters serial number for the dashboard

* store inverters serial number

* build connection table for dashboard

* add connection table to dashboard

* fix responsiveness of the tiles

* adapt unit tests

* remove test fake code

* increase test coverage, remove obsolete if statement
2025-04-24 23:12:26 +02:00
Stefan Allius
ff5ed1e606 S allius/issue387 (#389)
* add optional java script to fetch data regulary


* change file extension to `html.j2` for templates

* fix route for fetch data

- for running in iframes (e.g. HA ingress) we must
  use relative path in the URLs

* increase test coverage

* remove unused statements

* update favicon.svg
2025-04-20 01:51:04 +02:00
Stefan Allius
cbabbbd820 S allius/issue387 (#388)
* add optional java script to fetch data regullary

* change file extension to `html.j2` for templates

* fix route for fetch data

- for running in iframes (e.g. HA ingress) we must
  use relative path in the URLs

* increase test coverage

* remove unused statements
2025-04-20 00:53:31 +02:00
Stefan Allius
c270edff15 S allius/issue385 (#386)
* ignore translation and log files

* add quart-babel

* build and install translation files

* don't export the web-ui port 8127

- for security reason the user should use the
  HA ingress and not the direkt access to the web
  dashboard

* set 'lang' in html tag
2025-04-19 16:51:06 +02:00
Stefan Allius
b82941bd38 S allius/issue383 (#384)
* configure path to web files for Quart

* copy web-file into the add-on container

* add test template and stylesheet

* add w3.css dashboard

* fix unit test for test dashboard

* add Roboto font

* add awesome web font

* add all favicon formats

* load fonts locally

* adapt unit tests

* add all font and favicons to add-on

* fix sonarqube warnings

* add unit tests for all favicons
2025-04-18 03:47:38 +02:00
Stefan Allius
80cefc8082 S allius/issue378 (#382)
* move home route into web diretory

* add web UI to add-on

* update changelog

* use Blueprints
2025-04-16 01:49:53 +02:00
Stefan Allius
7d5670b6b5 remove aiohttp by quart (#381)
* remove aiohttp by quart

* remove global proxy_is_up

* add unit test for some routes
2025-04-16 00:54:02 +02:00
Stefan Allius
f98273a3eb S allius/issue362 (#379)
* allow serial number starting with `Y00`

* rollback to python version 3.13.2
2025-04-15 17:54:45 +02:00
Stefan Allius
5452b8efc2 Start version 0.14 (#374)
* bump version to 0.14.0

* update changelog

* set BUILD_ID only for dev and debug versions
2025-04-14 00:01:06 +02:00
renovate[bot]
5568a017ec Update python Docker tag to v3.13.3 (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-13 23:39:44 +02:00
renovate[bot]
4a70f366f1 Update dependency aiomqtt to v2.3.2 (#358)
* Update dependency aiomqtt to v2.3.2

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-13 23:38:38 +02:00
Stefan Allius
aac89065fd Update rel 0.13.0 (#370)
* bump version
2025-04-13 20:56:10 +02:00
Stefan Allius
fe250d478a Fix rel build (#368)
* fix rel build run

* disable cache for rc build

* bump python version to 3.12.10-r0
2025-04-13 20:35:21 +02:00
Stefan Allius
83a723c959 Update rel 0.13.0 (#367)
* fix link

(cherry picked from commit 3d422f9249)

* update compose help link

(cherry picked from commit 6d4ff0d508)

* fix link

(cherry picked from commit 3d422f9249)

* fix rel build run
2025-04-13 19:59:05 +02:00
Stefan Allius
abe6bfb013 update compose help link (#361) 2025-04-10 23:58:59 +02:00
renovate[bot]
31f4f05bed Update ghcr.io/hassio-addons/base Docker tag to v17.2.4 (#355)
* Update ghcr.io/hassio-addons/base Docker tag to v17.2.3

* Update ghcr.io/hassio-addons/base Docker tag to v17.2.4

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-08 20:47:11 +02:00
Stefan Allius
d9f7552cc3 define the value 2 for the out status (#356) 2025-04-08 20:18:51 +02:00
Stefan Allius
8ca91c2fdd define the value 2 for the out status (#356) 2025-04-07 23:47:29 +02:00
renovate[bot]
e0ba14ef75 Update ghcr.io/hassio-addons/base Docker tag to v17.2.4 2025-04-07 18:57:44 +00:00
Stefan Allius
ea749dcce6 enforce numbered release candidates (#353) 2025-04-06 22:28:32 +02:00
Stefan Allius
af5604d029 add alarm bitfields (#352)
- fix bitfield of the inverter alarms
- add batterie alarms
2025-04-06 20:07:17 +02:00
renovate[bot]
015b6b8db0 Update dependency pytest-cov to v6.1.1 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 01:28:27 +02:00
Stefan Allius
7782a3cb57 Add two states build from the measurements (#351)
* Add two states build from the measurements
- Batterie Status calculated from the batt current
- Power Supply State calc from the out Power

* improve test coverage
2025-04-06 01:21:41 +02:00
Stefan Allius
3d073acc58 Cleanup MQTT json format for DCU batterie (#349)
* Cleanup MQTT json format for DCU batterie
- add hw and sw version
- rename total generation into total charging energy
- rename cell temperature sensors
- restructure json format
- adapt unit tests

* revert changed test packages
2025-04-05 22:30:57 +02:00
Stefan Allius
6974672ba0 S allius/issue334 (#335)
* move forward_at_cmd_resp into InfosG3P class

- the variable is shared between the two connections
of an inverter. One is for the TSUN cloud and the
other for the device.

* use inverter class to share values between
the two protocol instances of a proxy
- move forward_at_cmd_resp into class InverterG3P
- store inverter ptr in Solarman_V5 instances
- add inverter ptr to all constructurs of protocols
- adapt doku and unit tests-
- add integration tests for AT+ commands which
  check the forwarding from and to the TSUN cloud

* adapt and improve the unit tests
- fix node_id declaration, which always has a / at
  the end. See config grammar for this rule
- set global var test to default after test run
2025-04-05 14:37:52 +02:00
Stefan Allius
4988a29a34 S allius/issue340 (#345)
* build the README.md files for the HA Add-ons
2025-04-04 20:11:43 +02:00
Stefan Allius
970b611d47 fix systemtest (#344) 2025-04-04 18:38:17 +02:00
renovate[bot]
1ec97a3e9c Update ghcr.io/hassio-addons/base Docker tag to v17.2.3 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-04 14:51:13 +02:00
Stefan Allius
c6a80bc783 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into renovate/ghcr.io-hassio-addons-base-17.x 2025-04-04 14:34:46 +02:00
renovate[bot]
2707582a45 Update dependency flake8 to v7.2.0 (#330)
* Update dependency flake8 to v7.2.0

* Flake8: ignore F821 errors, due of False Positives

# cleanup some unit tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-04 14:29:00 +02:00
renovate[bot]
a93b0d32fd Update ghcr.io/hassio-addons/base Docker tag to v17.2.3 2025-04-04 09:57:57 +00:00
renovate[bot]
bcec8dd843 Update dependency aiohttp to v3.11.16 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 21:00:43 +02: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
Stefan Allius
55c403a754 Dev 0.12 (#258)
* 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:35:12 +01:00
Stefan Allius
3bf245300d Dev 0.12 (#257)
* 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:25:50 +01:00
Stefan Allius
badc065b7a Merge pull request #242 from s-allius/ha-repro
move ha repro file into root dir
2024-12-10 19:09:33 +01:00
Stefan Allius
aea6cc9763 move file into root dir 2024-12-10 19:06:29 +01:00
Stefan Allius
92d1e648ae Merge pull request #241 from s-allius/renovate/python-3.x
Update python Docker tag
2024-12-09 21:58:41 +01:00
renovate[bot]
879b6608b3 Update python Docker tag 2024-12-09 20:56:38 +00:00
Stefan Allius
b69e7e2242 Merge pull request #240 from s-allius/renovate/aiohttp-3.x
Update dependency aiohttp to v3.11.10
2024-12-09 21:54:28 +01:00
renovate[bot]
0913fde126 Update dependency aiohttp to v3.11.10 2024-12-09 20:50:01 +00:00
Stefan Allius
bedbe08eeb Merge pull request #237 from s-allius/dev-0.12
Dev 0.12
2024-12-08 18:59:50 +01:00
Stefan Allius
3c81d446dd update changelog 2024-12-08 18:57:40 +01:00
Stefan Allius
b335881500 S allius/issue217 2 (#230)
* add some reader classes to get the configuration

* adapt unittests

* get config from json or toml file

* loop over all config readers to get the configuration

* rename config test files

* use relative paths for coverage test in vscode

* do not throw an error for missing config files

* remove obsolete tests

* use dotted key notation for pv sub dictonary

* log config reading progress

* remove create_config_toml.py

* remove obsolete tests for the ha_addon

* disable mosquitto tests if the server is down

* ignore main method for test coverage

* increase test coverage

* pytest-cov: use relative_files only on github, so coverage will work with vscode locally

* remove unneeded imports

* add missing test cases

* disable branch coverage, cause its not reachable
2024-12-08 13:25:04 +01:00
Stefan Allius
ac7b02bde9 init act_config, def_config even without init() call 2024-12-03 22:49:38 +01:00
Stefan Allius
47a89c269f fix some flake8 warnings 2024-12-03 22:48:52 +01:00
Stefan Allius
be3b4d6df0 S allius/issue206 (#213)
* update changelog

* add addon-dev target

* initial version

* use prebuild docker image

* initial version for multi arch images

* fix missing label latest

* create log and config folder first.

* clean up and translate to english

* set labels with docker bake

* add addon-debug and addon-dev targets

* pass version number to proxy at runtime

* add two more callbacks

* get addon version from app

* deploy rc addon container to ghcr

* move ha_addon test into subdir

* fix crash on container restart

- mkdir -p returns no error even if the director
  exists

* prepation for unit testing

- move script into a method

* added further config to schema

* typo fixed

* added monitor_sn + PV-strings 3-6 to create toml

* added options.json for testing

* prepare pytest and coverage for addons

* fix missing values in resulting config.toml
- define mqtt default values
- convert filter configuration

* first running unittest for addons

* add ha_addons

* increase test coverage

* test empty options.json file for HA AddOn

* fix pytest call in terminal

* improve test coverage

* remove uneeded options.json

* move config.py into subdir cnf

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>
2024-12-03 22:22:50 +01:00
Stefan Allius
a5b2b4b7c2 S allius/issue217 (#229)
* move config.py into a sub directory cnf

* adapt unit test

* split config class

- use depency injection to get config

* increase test coverage
2024-12-03 22:02:23 +01:00
Stefan Allius
668c631018 S allius/issue222 (#223)
* github action: use ubuntu 24.04 and sonar-scanner-action 4
2024-12-02 23:41:58 +01:00
Stefan Allius
07c989a305 increase mqtt timeout to 10s 2024-12-02 23:11:30 +01:00
Stefan Allius
28cf875533 migrate paho.mqtt CallbackAPIVersion to VERSION2 (#225) 2024-12-02 22:49:56 +01:00
Stefan Allius
9bae905c08 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.12 2024-11-29 21:38:23 +01:00
metzi
45b57109a8 Add on (#212)
* added service to transfer Add-on config from options.json to config.toml

* added feature to get MQTT config from Homeassistant

current version is MVP. can run as Home Assistant Add-On, config.toml is automatically created from option parameters in the add-on configuration tab.

* fix pylance and flake8 warnings

* prepare building a ha addon

- move build script into root dir
- cp source files in addon build-tree

* ignore proxy source files in addon build tree

* move proxy source files in own directory

* remove duplicates source files from repro

* check for a valis SONAR_TOKEN

* rename add_on path

* prepare for unittests and coverage measurement

* move file cause of the changes pathname

* move the proxy dir to /home/proxy

* build addon with make now

* remove duplicated requirements.txt file from repo

* undo changes

---------

Co-authored-by: Michael Metz <michael.metz@siemens.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2024-11-29 21:02:19 +01:00
Stefan Allius
2c69044bf8 initial test version 2024-11-24 22:26:55 +01:00
Stefan Allius
3bada76516 S allius/pytest (#211)
* - fix pytest setup that can be startet from the rootdir
  - support python venv environment
  - add pytest.ini
  - move common settings from .vscode/settings.json into pytest.ini
  - add missing requirements
  - fix import paths for pytests

* - support python venv environment

* initial version

* - add missing requirements python-dotenv

* fix import paths for pytests

* fix pytest warnings

* initial version

* report 5 slowest test durations

* add more vscode settings for python
2024-11-24 22:07:43 +01:00
Stefan Allius
84231c034c specify more offset of the 0x4110 message 2024-11-23 16:31:44 +01:00
Stefan Allius
d4fd396dcf Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.12 2024-11-20 22:09:53 +01:00
dependabot[bot]
976eaed9ea Bump aiohttp from 3.10.5 to 3.10.11 in /app (#209)
* Bump aiohttp from 3.10.5 to 3.10.11 in /app

Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.5...v3.10.11)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump sonarcloud-github-action to v3.1.0

* prepare version 0.11.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius
2024-11-20 21:08:47 +01:00
Stefan Allius
211a958080 add PROD_COMPL_TYPE to trace 2024-11-20 20:08:20 +01:00
Stefan Allius
5ced5ff06a S allius/issue205 (#207)
* Add SolarmanEmu class

* Forward a device ind to establish the EMU connection

* Move SolarmanEmu class into a dedicated file

* Add cloud connection counter

* Send inverter data in emulator mode

* Improve emulator mode

- parse more values from MQTT register
- differ between inverter and logger serial no

* Add some unit tests for SolarmanEmu class

* Send seconds since last sync in data packets

* Increase test coverage
2024-11-13 22:03:28 +01:00
Stefan Allius
78a35b5513 report alarm and fault bitfield to ha (#204)
* report alarm and fault bitfield to home assistant

* initial verson of message builder for SolarmanV5

- for SolarmaV5 we build he param field for the
  device and inverter message from the internal
  database
- added param description to the info table
  for constant values, which are not parsed and
  stored in internal database

* define constants for often used format strings

* update changelog
2024-11-02 15:09:10 +01:00
Stefan Allius
9b22fe354c clear remote ptr on disc only for client ifcs 2024-10-26 17:30:00 +02:00
Stefan Allius
a6ad3d4f0d fix linter warnings 2024-10-25 23:49:35 +02:00
Stefan Allius
4993676614 remove all eval() calls 2024-10-25 23:41:25 +02:00
Stefan Allius
10a18237c7 replace some eval calls 2024-10-25 21:38:36 +02:00
Stefan Allius
8d67f1745d update SonarSource/sonarcloud-github-action 2024-10-25 20:36:53 +02:00
Stefan Allius
9eb7c7fbe0 increase test coverage 2024-10-19 01:23:16 +02:00
Stefan Allius
6c6109d421 update class diagramms 2024-10-18 23:49:23 +02:00
Stefan Allius
7d0ea41728 reduce code duplications 2024-10-17 23:20:13 +02:00
Stefan Allius
ce5bd6eb0a reduce code duplications 2024-10-17 21:51:26 +02:00
Stefan Allius
6122f40718 fix recv_resp method call 2024-10-16 23:25:18 +02:00
Stefan Allius
c5f184a730 improve setting the node_id in the modbus 2024-10-16 23:20:23 +02:00
Stefan Allius
6da5d2cef6 define __slots__ for class ByteFifo (#202)
* define __slots__ for class ByteFifo

* disable set-timezone action

* set set-timezone to UTC

* try MathRobin/timezone-action@v1.1

* set TZ to "Europe/Berlin"

* define __slots__
2024-10-15 22:16:22 +02:00
Stefan Allius
db06d8c8e6 define __slots__ 2024-10-15 22:11:19 +02:00
Stefan Allius
3863454a84 set TZ to "Europe/Berlin" 2024-10-15 21:59:32 +02:00
Stefan Allius
5775cb1ce3 try MathRobin/timezone-action@v1.1 2024-10-15 21:53:11 +02:00
Stefan Allius
5d61a261b1 set set-timezone to UTC 2024-10-15 21:37:01 +02:00
Stefan Allius
bbda66e455 disable set-timezone action 2024-10-15 21:28:57 +02:00
Stefan Allius
0c7bf7956d define __slots__ for class ByteFifo 2024-10-15 21:25:09 +02:00
Stefan Allius
6b9c13ddfe Merge branch 'dev-0.11' of https://github.com/s-allius/tsun-gen3-proxy into main 2024-10-15 20:30:04 +02:00
Stefan Allius
a6ffcc0949 update version 0.11 2024-10-13 18:24:00 +02:00
Stefan Allius
c956c13d13 Dev 0.11 (#200)
* Code Cleanup (#158)


* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning

* Sonar qube 4 (#169)

* add unit test for inverter.py

* fix SonarQube warning

* Sonar qube 5 (#170)

* fix SonarLints warnings

* use random IP adresses for unit tests

* Docker: The description ist missing (#171)

Fixes #167

* S allius/issue167 (#172)

* cleanup

* Sonar qube 6 (#174)

* test class ModbusConn

* Sonar qube 3 (#178)

* add more unit tests

* GEN3: don't crash on overwritten msg in the receive buffer

* improve test coverage und reduce test delays

* reduce cognitive complexity

* fix merge

* fix merge conflikt

* fix merge conflict

* S allius/issue182 (#183)

* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182

* add support for Controller serial no and MAC

* test hardening

* GEN3: add support for new messages of version 3 firmwares

* bump libraries to latest versions

- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5

* improve test coverage

* reduce cognective complexity

* fix target preview

* remove dubbled fixtures

* increase test coverage

* Update README.md (#185)

update badges

* S allius/issue186 (#187)

* Parse more values in Server Mode
Fixes #186

* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode

* fix unit test

* increase test coverage

* S allius/issue186 (#188)

* increase test coverage

* update changelog

* add dokumentation

* change default config

* Update README.md (#189)

Config file is now foldable

* GEN3: Invalid Contact Info Msg (#192)

Fixes #191

* Refactoring async stream (#194)

* GEN3: Invalid Contact Info Msg
Fixes #191

* introduce ifc with FIFOs

* add object factory

* use AsyncIfc class with FIFO

* declare more methods as classmethods

* - refactoring

- remove _forward_buffer
- make async_write private

* remove _forward_buffer

* refactoring

* avoid mqtt handling for invalid serial numbers

* add two more callbacks

* FIX update_header_cb handling

* split AsyncStream in two classes

* split ConnectionG3(P) in server and client class

* update class diagramm

* refactor server creation

* remove duplicated imports

* reduce code duplication

* move StremPtr instances into Inverter class

* resolution of connection classes

- remove ConnectionG3Client
- remove ConnectionG3Server
- remove ConnectionG3PClient
- remove ConnectionG3PServer

* fix server connections

* fix client loop closing

* don't overwrite self.remote in constructor

* update class diagramm

* fixes

- fixes null pointer accesses
- initalize AsyncStreamClient with proper
  StreamPtr instance

* add close callback

* refactor close handling

* remove connection classes

* move more code into InverterBase class

* remove test_inverter_base.py

* add abstract inverter interface class

* initial commit

* fix sonar qube warnings

* rename class Inverter into Proxy

* fix typo

* move class InverterIfc into a separate file

* add more testcases

* use ProtocolIfc class

* add unit tests for AsyncStream class

* icrease test coverage

* reduce cognitive complexity

* increase test coverage

* increase tes coverage

* simplify heartbeat handler

* remove obsolete tx_get method

* add more unittests

* update changelog

* remove __del__ method for proper gc runs

* check releasing of ModbusConn instances

* call garbage collector to release unreachable objs

* decrease ref counter after the with block

* S allius/issue196 (#198)

* fix healthcheck

- on infrastructure with IPv6 support localhost
  might be resolved to an IPv6 adress. Since the
  proxy only support IPv4 for now, we replace
  localhost by 127.0.0.1, to fix this

* merge from main
2024-10-13 18:12:10 +02:00
Stefan Allius
85fe7261d5 Merge branch 'main' into dev-0.11 2024-10-13 18:07:38 +02:00
Stefan Allius
d4b618742c merge from main 2024-10-13 17:31:55 +02:00
Stefan Allius
719c6f703a S allius/issue196 (#198)
* fix healthcheck

- on infrastructure with IPv6 support localhost
  might be resolved to an IPv6 adress. Since the
  proxy only support IPv4 for now, we replace
  localhost by 127.0.0.1, to fix this
2024-10-13 17:13:07 +02:00
Stefan Allius
62ea2a9e6f Refactoring async stream (#194)
* GEN3: Invalid Contact Info Msg
Fixes #191

* introduce ifc with FIFOs

* add object factory

* use AsyncIfc class with FIFO

* declare more methods as classmethods

* - refactoring

- remove _forward_buffer
- make async_write private

* remove _forward_buffer

* refactoring

* avoid mqtt handling for invalid serial numbers

* add two more callbacks

* FIX update_header_cb handling

* split AsyncStream in two classes

* split ConnectionG3(P) in server and client class

* update class diagramm

* refactor server creation

* remove duplicated imports

* reduce code duplication

* move StremPtr instances into Inverter class

* resolution of connection classes

- remove ConnectionG3Client
- remove ConnectionG3Server
- remove ConnectionG3PClient
- remove ConnectionG3PServer

* fix server connections

* fix client loop closing

* don't overwrite self.remote in constructor

* update class diagramm

* fixes

- fixes null pointer accesses
- initalize AsyncStreamClient with proper
  StreamPtr instance

* add close callback

* refactor close handling

* remove connection classes

* move more code into InverterBase class

* remove test_inverter_base.py

* add abstract inverter interface class

* initial commit

* fix sonar qube warnings

* rename class Inverter into Proxy

* fix typo

* move class InverterIfc into a separate file

* add more testcases

* use ProtocolIfc class

* add unit tests for AsyncStream class

* icrease test coverage

* reduce cognitive complexity

* increase test coverage

* increase tes coverage

* simplify heartbeat handler

* remove obsolete tx_get method

* add more unittests

* update changelog

* remove __del__ method for proper gc runs

* check releasing of ModbusConn instances

* call garbage collector to release unreachable objs

* decrease ref counter after the with block
2024-10-13 16:07:01 +02:00
Stefan Allius
166a856705 GEN3: Invalid Contact Info Msg (#192)
Fixes #191
2024-09-19 19:17:22 +02:00
Stefan Allius
bfea38d9da Dev 0.11 (#190)
* Code Cleanup (#158)


* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning

* Sonar qube 4 (#169)

* add unit test for inverter.py

* fix SonarQube warning

* Sonar qube 5 (#170)

* fix SonarLints warnings

* use random IP adresses for unit tests

* Docker: The description ist missing (#171)

Fixes #167

* S allius/issue167 (#172)

* cleanup

* Sonar qube 6 (#174)

* test class ModbusConn

* Sonar qube 3 (#178)

* add more unit tests

* GEN3: don't crash on overwritten msg in the receive buffer

* improve test coverage und reduce test delays

* reduce cognitive complexity

* fix merge

* fix merge conflikt

* fix merge conflict

* S allius/issue182 (#183)

* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182

* add support for Controller serial no and MAC

* test hardening

* GEN3: add support for new messages of version 3 firmwares

* bump libraries to latest versions

- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5

* improve test coverage

* reduce cognective complexity

* fix target preview

* remove dubbled fixtures

* increase test coverage

* Update README.md (#185)

update badges

* S allius/issue186 (#187)

* Parse more values in Server Mode
Fixes #186

* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode

* fix unit test

* increase test coverage

* S allius/issue186 (#188)

* increase test coverage

* update changelog

* add dokumentation

* change default config

* Update README.md (#189)

Config file is now foldable
2024-09-16 00:45:36 +02:00
Stefan Allius
d5ec47fd1e Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.11 2024-09-16 00:37:39 +02:00
Stefan Allius
828f26cf24 Update README.md (#189)
Config file is now foldable
2024-09-16 00:17:43 +02:00
Stefan Allius
0b3d84ff36 change default config 2024-09-16 00:12:30 +02:00
Stefan Allius
5642c912a8 add dokumentation 2024-09-15 15:17:45 +02:00
Stefan Allius
614acbf32d update changelog 2024-09-15 01:18:36 +02:00
Stefan Allius
57525ca519 S allius/issue186 (#188)
* increase test coverage
2024-09-15 01:02:49 +02:00
Stefan Allius
5ef68280b1 S allius/issue186 (#187)
* Parse more values in Server Mode
Fixes #186

* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode

* fix unit test

* increase test coverage
2024-09-14 19:49:29 +02:00
Stefan Allius
e12c78212f Update README.md (#185)
update badges
2024-09-14 08:40:53 +02:00
Stefan Allius
2ab35a8257 increase test coverage 2024-09-07 18:04:28 +02:00
Stefan Allius
865216b8d9 remove dubbled fixtures 2024-09-07 18:03:50 +02:00
Stefan Allius
5d5d7c218f fix target preview 2024-09-07 13:49:45 +02:00
Stefan Allius
be4c6ac77f S allius/issue182 (#183)
* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182

* add support for Controller serial no and MAC

* test hardening

* GEN3: add support for new messages of version 3 firmwares

* bump libraries to latest versions

- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5

* improve test coverage

* reduce cognective complexity
2024-09-07 11:45:16 +02:00
Stefan Allius
a9dc7e6847 Dev 0.11 (#181)
* Sonar qube 6 (#174)

* test class ModbusConn

* Sonar qube 3 (#178)

* add more unit tests

* GEN3: don't crash on overwritten msg in the receive buffer

* improve test coverage und reduce test delays

* reduce cognitive complexity
2024-09-03 18:58:24 +02:00
Stefan Allius
270732f1d0 fix merge conflict 2024-09-03 18:54:49 +02:00
Stefan Allius
7b4fabdc25 fix merge conflikt 2024-09-03 18:48:21 +02:00
Stefan Allius
2351ec314a fix merge 2024-09-03 18:42:48 +02:00
Stefan Allius
604d30c711 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.11 2024-09-03 18:39:27 +02:00
Stefan Allius
ab5256659b reduce cognitive complexity 2024-09-03 18:32:44 +02:00
Stefan Allius
a76c0ac440 improve test coverage und reduce test delays 2024-09-03 17:23:09 +02:00
Stefan Allius
215dcd98e6 GEN3: don't crash on overwritten msg in the receive buffer 2024-09-03 17:22:34 +02:00
Stefan Allius
627ca97360 Test modbus_tcp (#179)
* add more unit tests
2024-08-30 20:40:53 +02:00
Stefan Allius
d2b88ab838 Sonar qube 3 (#178)
* add more unit tests
2024-08-29 23:47:30 +02:00
Stefan Allius
6d9addc7d5 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.11 2024-08-27 21:41:11 +02:00
Stefan Allius
1bb08fb211 Update README.md (#177) 2024-08-27 15:03:57 +02:00
Stefan Allius
193eea65af Update README.md (#176)
add SonarCloude shields
2024-08-27 00:24:11 +02:00
Stefan Allius
2b8dacb0de Dev 0.11 (#175)
* use random IP adresses for unit tests

* Docker: The description ist missing (#171)

Fixes #167

* S allius/issue167 (#172)

* cleanup

* Sonar qube 6 (#174)

* test class ModbusConn
2024-08-26 23:49:23 +02:00
Stefan Allius
cb0c69944f Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into dev-0.11 2024-08-26 23:45:48 +02:00
Stefan Allius
7f41365815 Sonar qube 6 (#174)
* test class ModbusConn
2024-08-26 23:37:24 +02:00
Stefan Allius
5db3fbf495 Update README.md (#173) 2024-08-26 21:28:44 +02:00
Stefan Allius
d44726c0f3 S allius/issue167 (#172)
* cleanup
2024-08-25 23:28:35 +02:00
Stefan Allius
1985557bce Docker: The description ist missing (#171)
Fixes #167
2024-08-25 23:05:25 +02:00
Stefan Allius
7dc2595d71 use random IP adresses for unit tests 2024-08-25 12:02:27 +02:00
Stefan Allius
6d9a446bfe Sonar qube 5 (#170)
* fix SonarLints warnings
2024-08-24 23:03:02 +02:00
Stefan Allius
f9c1b83ccd Sonar qube 4 (#169)
* add unit test for inverter.py

* fix SonarQube warning
2024-08-24 22:21:55 +02:00
Stefan Allius
58b42f7d7c SonarCloud setup (#168)
* Code Cleanup (#158)

* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning
2024-08-23 21:24:01 +02:00
Stefan Allius
27045cac6e Sonar qube 3 (#166)
* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning
2024-08-23 00:26:01 +02:00
Stefan Allius
54de2aecfe Sonar qube 3 (#165)
* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings
2024-08-16 21:07:08 +02:00
Stefan Allius
5a39370cc3 Sonar qube 3 (#164)
* fix SonarQube warnings
2024-08-13 22:22:45 +02:00
Stefan Allius
7a9b23d068 Sonar qube 3 (#163)
fix SonarQube warnings in modbus.py
2024-08-13 21:11:56 +02:00
Stefan Allius
e34afcb523 cleanup (#160) 2024-08-11 23:22:07 +02:00
Stefan Allius
22df381da5 ignore some rules for tests 2024-08-11 00:48:19 +02:00
Stefan Allius
117e6a7570 exclude *.pyi filese 2024-08-10 23:55:19 +02:00
Stefan Allius
65de946992 fix linter warning 2024-08-10 23:53:35 +02:00
Stefan Allius
33d385db10 optimise versionstring handling (#159)
- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment
2024-08-10 22:53:25 +02:00
Stefan Allius
1e610af1df Code Cleanup (#158)
* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes
2024-08-10 20:41:31 +02:00
Stefan Allius
db1169f61f Update README.md (#156)
add modbus_polling to example config
2024-08-10 16:49:18 +02:00
Stefan Allius
383be10e87 Hotfix v0.10.1: fix displaying the version string at startup and in HA (#155)
* Version 0.10.0 no longer displays the version string (#154)

Fixes #153
2024-08-10 14:18:25 +02:00
Stefan Allius
b364fb3f8e Dev 0.10 (#151)
* S allius/issue117 (#118)

* add shutdown flag

* add more register definitions

* add start commando for client side connections

* add first support for port 8899

* fix shutdown

* add client_mode configuration

* read client_mode config to setup inverter connections

* add client_mode connections over port 8899

* add preview build

* Update README.md

describe the new client-mode over port 8899 for GEN3PLUS

* MODBUS: the last digit of the inverter version is a hexadecimal number (#121)

* S allius/issue117 (#122)

* add shutdown flag

* add more register definitions

* add start commando for client side connections

* add first support for port 8899

* fix shutdown

* add client_mode configuration

* read client_mode config to setup inverter connections

* add client_mode connections over port 8899

* add preview build

* add documentation for client_mode

* catch os error and log thme with DEBUG level

* update changelog

* make the maximum output coefficient configurable (#124)

* S allius/issue120 (#126)

* add config option to disable the modbus polling

* read more modbus regs in polling mode

* extend connection timeouts if polling mode is disabled

* update changelog

* S allius/issue125 (#127)

* fix linter warning

* move sequence diagramm to wiki

* catch asyncio.CancelledError

* S allius/issue128 (#130)

* set Register.NO_INPUTS fix to 4 for GEN3PLUS

* don't set Register.NO_INPUTS per MODBUS

* fix unit tests

* register OUTPUT_COEFFICIENT at HA

* update changelog

* - Home Assistant: improve inverter status value texts

* - GEN3: add inverter status

* on closing send outstanding MQTT data to the broker

* force MQTT publish on every conn open and close

* reset inverter state on close

- workaround which reset the inverter status to
  offline when the inverter has a very low
  output power on connection close

* improve client modified
- reduce the polling cadence to 30s
- set controller statistics for HA

* client mode set controller IP for HA

* S allius/issue131 (#132)

* Make __publish_outstanding_mqtt public

* update proxy counter

- on client mode connection establishment or
  disconnecting update tje counection counter

* Update README.md (#133)

* reset inverter state on close

- workaround which reset the inverter status to
  offline when the inverter has a very low
  output power on connection close

* S allius/issue134 (#135)

* add polling invertval and method ha_remove()

* add client_mode arg to constructors

- add PollingInvervall

* hide some topics in client mode

- we hide topics in HA by sending an empty register
  MQTT topic during HA auto configuration

* add client_mode value

* update class diagram

* fix modbus close handler

- fix empty call and cleanup que
- add unit test

* don't sent an initial 1710 msg in client mode

* change HA icon for inverter status

* increase test coverage

* accelerate timer tests

* bump aiomqtt and schema to latest release (#137)

* MQTT timestamps and protocol improvements (#140)

* add TS_INPUT, TS_GRID and TS_TOTAL

* prepare MQTT timestamps

- add _set_mqtt_timestamp method
- fix hexdump printing

* push dev and debug images to docker.io

* add unix epoche timestamp for MQTT pakets

* set timezone for unit tests

* set name für setting timezone step

* trigger new action

* GEN3 and GEN3PLUS: handle multiple message

- read: iterate over the receive buffer
- forward: append messages to the forward buffer
- _update_header: iterate over the forward buffer

* GEN3: optimize timeout handling

- longer timeout in state init and reveived
- got to state pending only from state up

* update changelog

* cleanup

* print coloured logs

* Create sonarcloud.yml (#143)

* Update sonarcloud.yml

* Update sonarcloud.yml

* Update sonarcloud.yml

* Update sonarcloud.yml

* Update sonarcloud.yml

* build multi arch images with sboms (#146)

* don't send MODBUS request when state is not up (#147)

* adapt timings

* don't send MODBUS request when state is note up

* adapt unit test

* make test code more clean (#148)

* Make test code more clean (#149)

* cleanup

* Code coverage for SonarCloud (#150)


* cleanup code and unit tests

* add test coverage for SonarCloud

* configure SonarCloud

* update changelog

* Do no build on *.yml changes

* prepare release 0.10.0

* disable MODBUS_POLLING for GEN§PLUS in example config

* bump aiohttp to version 3.10.2

* code cleanup

* Fetch all history for all tags and branches
2024-08-09 23:16:47 +02:00
Stefan Allius
a42ba8a8c6 Dev 0.9 (#115)
* make timestamp handling stateless

* adapt tests for stateless timestamp handling

* initial version

* add more type annotations

* add more type annotations

* fix Generator annotation for ha_proxy_confs

* fix names of issue branches

* add more type annotations

* don't use depricated varn anymore

* don't mark all test as async

* fix imports

* fix solarman unit tests

- fake Mqtt class

* print image build time during proxy start

* update changelog

* fix pytest collect warning

* cleanup msg_get_time handler

* addapt unit test

* label debug images with debug

* dump droped packages

* fix warnings

* add systemtest with invalid start byte

* update changelog

* update changelog

* add exposed ports and healthcheck

* add wget for healthcheck

* add aiohttp

* use config validation for healthcheck

* add http server for healthcheck

* calculate msg prossesing time

* add healthy check methods

* fix typo

* log ConfigErr with DEBUG level

* Update async_stream.py

- check if processing time is < 5 sec

* add a close handler to release internal resources

* call modbus close hanlder on a close call

* add exception handling for forward handler

* update changelog

* isolate Modbus fix

* cleanup

* update changelog

* add heaithy handler

* log unrelease references

* add healtcheck

* complete exposed port list

* add wget for healtcheck

* add aiohttp

* use Enum class for State

* calc processing time for healthcheck

* add HTTP server for healthcheck

* cleanup

* Update CHANGELOG.md

* updat changelog

* add docstrings to state enum

* set new state State.received

* add healthy method

* log healthcheck infos with DEBUG level

* update changelog

* S allius/issue100 (#101)

* detect dead connections

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

* update changelog

* fix merge conflict

* fix unittests

* S allius/issue108 (#109)

* add more data types

* adapt unittests

* improve test coverage

* fix linter warning

* update changelog

* S allius/issue102 (#110)

* hotfix: don't send two MODBUS commands together

* fix unit tests

* remove read loop

* optional sleep between msg read and sending rsp

* wait after read 0.5s before sending a response

* add pending state

* fix state definitions

* determine the connection timeout by the conn state

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

* update changelog

* S allius/issue111 (#112)

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

* inital checkin

* remove crontab entry for regular MODBUS cmds

* add timer for regular MODBUS polling

* fix Stop method call for already stopped timer

* optimize MB_START_TIMEOUT value

* cleanup

* update changelog

* fix buildx warnings

* fix timer cleanup

* fix Config.class_init()

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

* add quit flag to docker push

* fix timout calculation

* rename python to debugpy

* add asyncio log

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

* update changelog

* update changelog

* fix exception in MODBUS timeout callback

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

* hotfix: don't send two MODBUS commands together

* Update README.md

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

* Update python-app.yml

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

* S allius/issue104 (#105)

* Update README.md

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

* Update python-app.yml

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

* fix forwarding of MODBUS responses

* fix unit tests

* update changelog

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

* hotfix: don't send two MODBUS commands together

* Update README.md

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

* Update python-app.yml

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

* S allius/issue104 (#105)

* Update README.md

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

* Update python-app.yml

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

* fix forwarding of MODBUS responses

* fix unit tests

* update changelog
2024-06-21 18:12:48 +02:00
Stefan Allius
c34b33ed5f Update python-app.yml
fix name for issues branches
2024-06-08 23:39:28 +02:00
Stefan Allius
0a18918326 Update python-app.yml
run also on pushes to issue branches
2024-06-08 23:23:56 +02:00
163 changed files with 26757 additions and 3606 deletions

3
.cover_ghaction_rc Normal file
View File

@@ -0,0 +1,3 @@
[run]
branch = True
relative_files = True

View File

@@ -1,2 +1,3 @@
[run]
branch = True
branch = True
omit = app/src/web/templates/*.html.j2

14
.env_example Normal file
View File

@@ -0,0 +1,14 @@
# example file for the .env file. The .env set private values
# which are needed for builing containers
# registry for debug an dev container
PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
# registry for official container (preview, rc, rel)
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
PUBLIC_CR_KEY=
# define serial number of GEN3PLUS devices for systemtests
# the serialnumber are coded as 4-byte hex-strings
SOLARMAN_INV_SNR='00000000'
SOLARMAN_DCU_SNR='00000000'

View File

@@ -5,7 +5,7 @@ name: Python application
on:
push:
branches: [ "main", "dev-*" ]
branches: [ "main", "dev-*", "*/issue*" ]
paths-ignore:
- '**.md' # Do no build on *.md changes
- '**.yml' # Do no build on *.yml changes
@@ -18,33 +18,54 @@ on:
- '**.dockerfile' # Do no build on *.dockerfile changes
- '**.sh' # Do no build on *.sh changes
pull_request:
branches: [ "main" ]
branches: [ "main", "dev-*" ]
permissions:
contents: read
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
env:
TZ: "Europe/Berlin"
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- 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
pip install flake8 pytest pytest-asyncio
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --select=E9,F63,F7,F82 --ignore=F821 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
- name: Test with pytest
run: |
python -m pytest app
python -m pytest app --cov=app/src --cov-config=.cover_ghaction_rc --cov-report=xml
coverage report
- name: Analyze with SonarCloud
if: ${{ env.SONAR_TOKEN != 0 }}
uses: SonarSource/sonarqube-scan-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectBaseDir: .
args:
-Dsonar.projectKey=s-allius_tsun-gen3-proxy
-Dsonar.python.coverage.reportPaths=coverage.xml
-Dsonar.python.flake8.reportPaths=output_flake.txt
# -Dsonar.docker.hadolint.reportPaths=

8
.gitignore vendored
View File

@@ -1,11 +1,19 @@
__pycache__
.pytest_cache
.venv/**
bin/**
mosquitto/**
homeassistant/**
ha_addons/ha_addon/rootfs/home/proxy/*
ha_addons/ha_addon/rootfs/requirements.txt
tsun_proxy/**
Doku/**
.DS_Store
.coverage
.env
.venv
coverage.xml
*.pot
*.mo
*.log
*.log.*

2
.hadolint.yaml Normal file
View File

@@ -0,0 +1,2 @@
ignored:
- SC1091

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13.2

View File

@@ -0,0 +1,4 @@
{
"sonarCloudOrganization": "s-allius",
"projectKey": "s-allius_tsun-gen3-proxy"
}

2
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"configurations": [
{
"name": "Python: Aktuelle Datei",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",

27
.vscode/settings.json vendored
View File

@@ -1,15 +1,32 @@
{
"python.analysis.extraPaths": [
"app/src",
"app/tests",
".venv/lib",
],
"python.testing.pytestArgs": [
"-vv",
"app",
"-vvv",
"--cov=app/src",
"--cov-report=xml",
"--cov-report=html",
"app",
"system_tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"flake8.args": [
"--extend-exclude=app/tests/*.py system_tests/*.py"
]
"--extend-exclude=app/tests/*.py,system_tests/*.py"
],
"sonarlint.connectedMode.project": {
"connectionId": "s-allius",
"projectKey": "s-allius_tsun-gen3-proxy"
},
"files.exclude": {
"**/*.pyi": true
},
"python.analysis.typeEvaluation.deprecateTypingAliases": true,
"python.autoComplete.extraPaths": [
".venv/lib"
],
"coverage-gutters.coverageBaseDir": "tsun",
"makefile.configureOnOpen": false
}

View File

@@ -5,7 +5,147 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [unreleased]
- Update ghcr.io/hassio-addons/base Docker tag to v17.2.5
- fix a lot of pytest-asyncio problems in the unit tests
- Cleanup startup code for Quart and the Proxy
- Redirect the hypercorn traces to a separate log-file
- Configure the dashboard trace handler by the logging.ini file
- Dashboard: add Notes page and table for important messages
- Dashboard: add Log-File page
- Dashboard: add Connection page
- add web UI to add-on
- allow `Y00` serial numbers for GEN3PLUS devices
## [0.13.0] - 2025-04-13
- 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
- detect usage of a local DNS resolver [#37](https://github.com/s-allius/tsun-gen3-proxy/issues/37)
- path for logs is now configurable by cli args
- configure the number of keeped logfiles by cli args
- add DOCS.md and CHANGELOG.md for add-ons
- pin library version und update them with renovate
- build config.yaml for add-ons by a jinja2 template
- use gnu make to build proxy and add-on
- make the configuration more flexible, add command line args to control this
- fix the python path so we don't need special import paths for unit tests anymore
- add emulator mode [#205](https://github.com/s-allius/tsun-gen3-proxy/issues/205)
- ignore inverter replays which a older than 1 day [#246](https://github.com/s-allius/tsun-gen3-proxy/issues/246)
- support test coverage in vscode
- upgrade SonarQube action to version 4
- update github action to Ubuntu 24-04
- add initial support for home assistant add-ons from @mime24
- github action: use ubuntu 24.04 and sonar-scanner-action 4 [#222](https://github.com/s-allius/tsun-gen3-proxy/issues/222)
- migrate paho.mqtt CallbackAPIVersion to VERSION2 [#224](https://github.com/s-allius/tsun-gen3-proxy/issues/224)
- add PROD_COMPL_TYPE to trace
- add SolarmanV5 messages builder
- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7)
## [0.11.1] - 2024-11-20
- fix pytest setup that can be startet from the rootdir
- support python venv environment
- add pytest.ini
- move common settings from .vscode/settings.json into pytest.ini
- add missing requirements
- fix import paths for pytests
- Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
## [0.11.0] - 2024-10-13
- fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196)
- refactoring: cleaner architecture, increase test coverage
- Parse more values in Server Mode [#186](https://github.com/s-allius/tsun-gen3-proxy/issues/186)
- GEN3: add support for new messages of version 3 firmwares [#182](https://github.com/s-allius/tsun-gen3-proxy/issues/182)
- add support for controller MAC and serial number
- GEN3: don't crash on overwritten msg in the receive buffer
- Reading the version string from the image updates it even if the image is re-pulled without re-deployment
## [0.10.1] - 2024-08-10
- fix displaying the version string at startup and in HA [#153](https://github.com/s-allius/tsun-gen3-proxy/issues/153)
## [0.10.0] - 2024-08-09
- bump aiohttp to version 3.10.2
- add SonarQube and code coverage support
- don't send MODBUS request when state is note up; adapt timeouts [#141](https://github.com/s-allius/tsun-gen3-proxy/issues/141)
- build multi arch images with sboms [#144](https://github.com/s-allius/tsun-gen3-proxy/issues/144)
- add timestamp to MQTT topics [#138](https://github.com/s-allius/tsun-gen3-proxy/issues/138)
- improve the message handling, to avoid hangs
- GEN3: allow long timeouts until we received first inverter data (not only device data)
- bump aiomqtt to version 2.2.0
- bump schema to version 0.7.7
- Home Assistant: improve inverter status value texts
- GEN3: add inverter status
- fix flapping registers [#128](https://github.com/s-allius/tsun-gen3-proxy/issues/128)
- register OUTPUT_COEFFICIENT at HA
- GEN3: INVERTER_STATUS,
- add config option to disable the MODBUS polling [#120](https://github.com/s-allius/tsun-gen3-proxy/issues/120)
- make the maximum output coefficient configurable [#123](https://github.com/s-allius/tsun-gen3-proxy/issues/123)
- cleanup shutdown
- add preview build
- MODBUS: the last digit of the inverter version is a hexadecimal number [#119](https://github.com/s-allius/tsun-gen3-proxy/issues/119)
- GEN3PLUS: add client_mode connection on port 8899 [#117](https://github.com/s-allius/tsun-gen3-proxy/issues/117)
## [0.9.0] - 2024-07-01
- fix exception in MODBUS timeout callback
## [0.9.0-RC1] - 2024-06-29
- add asyncio log and debug mode
- stop the HTTP server on shutdown gracefully
- Synchronize regular MODBUS commands with the status of the inverter to prevent the inverter from crashing due to
unexpected packets. [#111](https://github.com/s-allius/tsun-gen3-proxy/issues/111)
- GEN3: avoid sending MODBUS commands to the inverter during the inverter's reporting phase
- GEN3: determine the connection timeout based on the connection state
- GEN3: support more data encodings for DSP version V5.0.17 [#108](https://github.com/s-allius/tsun-gen3-proxy/issues/108)
- detect dead connections [#100](https://github.com/s-allius/tsun-gen3-proxy/issues/100)
- improve connection logging wirt a unique connection id
- Add healthcheck, readiness and liveness checks [#91](https://github.com/s-allius/tsun-gen3-proxy/issues/91)
- MODBUS close handler releases internal resource [#93](https://github.com/s-allius/tsun-gen3-proxy/issues/93)
- add exception handling for message forwarding [#94](https://github.com/s-allius/tsun-gen3-proxy/issues/94)
- GEN3: make timestamp handling stateless, to avoid blocking when the TSUN cloud is down [#56](https://github.com/s-allius/tsun-gen3-proxy/issues/56)
- GEN3PLUS: dump invalid packages with wrong start or stop byte
- label debug imagages als `debug`
- print imgae build time during proxy start
- add type annotations
- improve async unit test and fix pytest warnings
- run github tests even for pulls on issue branches
## [0.8.1] - 2024-06-21
- Fix MODBUS responses are dropped and not forwarded to the TSUN cloud [#104](https://github.com/s-allius/tsun-gen3-proxy/issues/104)
- GEN3: Fix connections losts due MODBUS requests [#102](https://github.com/s-allius/tsun-gen3-proxy/issues/102)
## [0.8.0] - 2024-06-07
- improve logging: add protocol or node_id to connection logs
- improve logging: log ignored AT+ or MODBUS commands
@@ -17,7 +157,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- catch all OSError errors in the read loop
- log Modbus traces with different log levels
- add Modbus fifo and timeout handler
- build version string in the same format as TSUN for GEN3 invterts
- build version string in the same format as TSUN for GEN3 inverters
- add graceful shutdown
- parse Modbus values and store them in the database
- add cron task to request the output power every minute

27
Makefile Normal file
View File

@@ -0,0 +1,27 @@
.PHONY: build babel clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel check-docker-compose install
babel:
$(MAKE) -C app $@
build:
$(MAKE) -C ha_addons $@
clean:
$(MAKE) -C app $@
$(MAKE) -C ha_addons $@
debug dev preview rc rel:
$(MAKE) -C app babel
$(MAKE) -C app $@
addon-dev addon-debug addon-rc addon-rel:
$(MAKE) -C app babel
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
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

314
README.md
View File

@@ -1,34 +1,44 @@
<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.0.1-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>
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=alert_status"><img alt="The quality gate status" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=alert_status"></a>
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=bugs"><img alt="No of bugs" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=bugs"></a>
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=code_smells"><img alt="No of code-smells" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=code_smells"></a>
<br>
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=coverage"><img alt="Test coverage in percent" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
</p>
# 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.
Alternatively you can run the TSUN-Proxy as a Home Assistant Add-on. The installation of this add-on is pretty straightforward and not different in comparison to installing any other custom Home Assistant add-on.
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
@@ -40,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
@@ -59,11 +70,20 @@ Here are some screenshots of how the inverter is displayed in the Home Assistant
## Requirements
### 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
### 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 device and the TSUN cloud
# Getting Started
## for Docker Installation
To run the proxy, you first need to create the image. You can do this quite simply as follows:
```sh
@@ -89,15 +109,30 @@ With this information we can customize the `docker run`` statement:
docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
```
## for Home Assistant Add-on Installation
1. Add the repository URL to the Home Assistant add-on store
[![Add repository on my Home Assistant][repository-badge]][repository-url]
2. Reload the add-on store page
3. Click the "Install" button to install the add-on.
# Configuration
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
```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.
```
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.
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 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
@@ -106,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-env#docker-compose-environment-variables)
## Proxy Configuration
@@ -115,26 +150,63 @@ The proxy can be configured via the file 'config.toml'. When the proxy is starte
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>
<details>
<summary>Here is an example of a <b>config.toml</b> file</summary>
```toml
# configuration for tsun cloud for 'GEN3' inverters
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
tsun.host = 'logger.talent-monitoring.com'
tsun.port = 5005
# configuration for solarman cloud for 'GEN3 PLUS' inverters
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
solarman.host = 'iot.talent-monitoring.com'
solarman.port = 10000
##########################################################################################
###
### T S U N - G E N 3 - P R O X Y
###
### from Stefan Allius
###
##########################################################################################
###
### The readme will give you an overview of the project:
### https://s-allius.github.io/tsun-gen3-proxy/
###
### The proxy supports different operation modes. Select the proper mode
### which depends on your inverter type and you inverter firmware.
### Please read:
### 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-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
###
##########################################################################################
# mqtt broker configuration
##########################################################################################
##
## MQTT broker configuration
##
## In this block, you must configure the connection to your MQTT broker and specify the
## 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-toml#mqtt-broker-account
##
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883
mqtt.user = ''
mqtt.passwd = ''
# home-assistant
##########################################################################################
##
## HOME ASSISTANT
##
## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
## 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-toml#home-assistant
##
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
@@ -142,34 +214,142 @@ ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_i
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
# microinverters
inverters.allow_all = false # True: allow inverters, even if we have no inverter mapping
##########################################################################################
##
## GEN3 Proxy Mode Configuration
##
## In this block, you can configure an optional connection to the TSUN cloud for GEN3
## 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-toml#tsun-cloud-for-gen3-inverter-only
##
# inverter mapping, maps a `serial_no* to a `node_id` and defines an optional `suggested_area` for `home-assistant`
#
# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
tsun.host = 'logger.talent-monitoring.com'
tsun.port = 5005
##########################################################################################
##
## GEN3PLUS Proxy Mode Configuration
##
## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
## 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-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'
solarman.port = 10000
##########################################################################################
###
### Inverter Definitions
###
### The proxy supports the simultaneous operation of several inverters, even of different
### types. A configuration block must be defined for each inverter, in which all necessary
### parameters must be specified. These depend on the operation mode used and also differ
### slightly depending on the inverter type.
###
### In addition, the PV modules can be defined at the individual inputs for documentation
### purposes, whereby these are displayed in Home Assistant.
###
### The proxy only accepts connections from known inverters. This can be switched off for
### test purposes and unknown serial numbers are also accepted.
###
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
## to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
## in the configuration block
##
## The serial numbers of all GEN3 inverters start with `R17`!
##
[inverters."R17xxxxxxxxxxxx1"]
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
suggested_area = 'roof' # Optional, suggested installation area for home-assistant
node_id = 'inv_1' # MQTT replacement for inverters serial number
suggested_area = 'roof' # suggested installation place for home-assistant
modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
[inverters."R17xxxxxxxxxxxx2"]
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
##
## 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
## to this inverter. Further inverter-specific parameters (e.g. polling mode, client mode)
## can be set in the configuration block
##
## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
## with the inverter.
##
[inverters."Y17xxxxxxxxxxxx1"] # This block is also for inverters with a Y47 serial no
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
node_id = 'inv_3' # MQTT replacement for inverters serial number
suggested_area = 'garage' # suggested installation place for home-assistant
monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
node_id = 'inv_2' # MQTT replacement for inverters 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
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
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
### this connection or parameters (e.g. network credentials) can be queried. Filters can
### then be configured for the AT+ commands from the TSUN Cloud so that only certain
### accesses are permitted.
###
### An overview of all known AT+ commands can be found here:
### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
###
[gen3plus.at_acl]
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'] # allow this for TSUN access
tsun.block = []
@@ -178,27 +358,45 @@ mqtt.block = []
```
## Inverter Configuration
</details>
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.
## Inverter and Batterie Configuration
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.
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.
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`.
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 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 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.
```
If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters.
## Client Mode (GEN3PLUS only)
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 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
@@ -214,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!
@@ -226,22 +424,25 @@ In the following table you will find an overview of which inverter model has bee
A combination with a red question mark should work, but I have not checked it in detail.
<table align="center">
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 4.0.10</th></tr>
<tr><td>GEN3 micro inverters (single MPPT):<br>MS300, MS350, MS400<br>MS400-D</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 micro inverters (dual MPPT):<br>MS600, MS700, MS800<br>MS600-D, MS800-D</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td></tr>
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td></tr>
<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></tr>
<tr><th align="center">Micro Inverter Model</th><th align="center">Fw. 1.00.06</th><th align="center">Fw. 1.00.17</th><th align="center">Fw. 1.00.20</th><th align="center">Fw. 4.0.10</th><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 (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 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)
@@ -265,3 +466,6 @@ We're very happy to receive contributions to this project! You can get started b
## Changelog
The changelog lives in [CHANGELOG.md](https://github.com/s-allius/tsun-gen3-proxy/blob/main/CHANGELOG.md). It follows the principles of [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[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

View File

@@ -2,4 +2,6 @@ tests/
**/__pycache__
*.pyc
.DS_Store
build.sh
build.sh
*.pot
*.po

1
app/.version Normal file
View File

@@ -0,0 +1 @@
0.14.0

View File

@@ -4,37 +4,35 @@ ARG GID=1000
#
# first stage for our base image
FROM python:3.12-alpine AS base
USER root
FROM python:3.13-alpine AS base
COPY --chmod=0700 ./hardening_base.sh .
COPY --chmod=0700 ./hardening_base.sh /
RUN apk upgrade --no-cache && \
apk add --no-cache su-exec && \
./hardening_base.sh && \
rm ./hardening_base.sh
apk add --no-cache su-exec=0.2-r3 && \
/hardening_base.sh && \
rm /hardening_base.sh
#
# second stage for building wheels packages
FROM base as builder
FROM base AS builder
# copy the dependencies file to the root dir and install requirements
COPY ./requirements.txt /root/
RUN apk add --no-cache build-base && \
python -m pip install --no-cache-dir -U pip wheel && \
RUN apk add --no-cache build-base=0.5-r3 && \
python -m pip install --no-cache-dir pip==24.3.1 wheel==0.45.1 && \
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
#
# third stage for our runtime image
FROM base as runtime
FROM base AS runtime
ARG SERVICE_NAME
ARG VERSION
ARG UID
ARG GID
ARG LOG_LVL
ARG LOG_LVL=INFO
ARG environment
ENV VERSION=$VERSION
ENV SERVICE_NAME=$SERVICE_NAME
ENV UID=$UID
ENV GID=$GID
@@ -51,9 +49,9 @@ VOLUME ["/home/$SERVICE_NAME/log", "/home/$SERVICE_NAME/config"]
# and unistall python packages and alpine package manger to reduce attack surface
COPY --from=builder /root/wheels /root/wheels
COPY --chmod=0700 ./hardening_final.sh .
RUN python -m pip install --no-cache --no-index /root/wheels/* && \
RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && \
rm -rf /root/wheels && \
python -m pip uninstall --yes setuptools wheel pip && \
python -m pip uninstall --yes wheel pip && \
apk --purge del apk-tools && \
./hardening_final.sh && \
rm ./hardening_final.sh
@@ -61,19 +59,12 @@ RUN python -m pip install --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 .
EXPOSE 5005
COPY translations ./translations
RUN echo ${VERSION} > /proxy-version.txt \
&& date > /build-date.txt
EXPOSE 5005 8127 10000
# command to run on container start
ENTRYPOINT ["/root/entrypoint.sh"]
CMD [ "python3", "./server.py" ]
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
LABEL org.opencontainers.image.authors="Stefan Allius"
LABEL org.opencontainers.image.source https://github.com/s-allius/tsun-gen3-proxy
LABEL org.opencontainers.image.description 'This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
LABEL org.opencontainers.image.vendor="Stefan Allius"

63
app/Makefile Normal file
View File

@@ -0,0 +1,63 @@
#!make
include ../.env
SHELL = /bin/sh
IMAGE = tsun-gen3-proxy
# Folders
APP=.
SRC=$(APP)/src
# Folders for Babel translation
BABEL_INPUT_JINJA=$(SRC)/web/templates
BABEL_INPUT= $(foreach dir,$(BABEL_INPUT_JINJA),$(wildcard $(dir)/*.html.j2)) \
BABEL_TRANSLATIONS=$(APP)/translations
export BUILD_DATE := ${shell date -Iminutes}
VERSION := $(shell cat $(APP)/.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/)
clean:
rm -f $(BABEL_TRANSLATIONS)/*.pot
dev debug:
@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:
@[ "${RC}" ] || ( echo ">> RC is not set"; exit 1 )
@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)-$@$(RC) && \
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
docker buildx bake -f docker-bake.hcl $@
preview rel:
@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 $@
babel: $(BABEL_TRANSLATIONS)/de/LC_MESSAGES/messages.mo $(BABEL_TRANSLATIONS)/de/LC_MESSAGES/messages.po $(BABEL_TRANSLATIONS)/messages.pot
$(BABEL_TRANSLATIONS)/%.pot : $(SRC)/.babel.cfg $(BABEL_INPUT)
@mkdir -p $(@D)
@pybabel extract -F $< --project=$(IMAGE) --version=$(VERSION) -o $@ $(SRC)
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po : $(BABEL_TRANSLATIONS)/messages.pot
@mkdir -p $(@D)
@pybabel update --init-missing -i $< -d $(BABEL_TRANSLATIONS) -l $*
$(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.mo : $(BABEL_TRANSLATIONS)/%/LC_MESSAGES/messages.po
@pybabel compile -d $(BABEL_TRANSLATIONS) -l $*
.PHONY: babel clean debug dev preview rc rel

View File

@@ -1,55 +0,0 @@
#!/bin/bash
# Usage: ./build.sh [dev|rc|rel]
# dev: development build
# rc: release candidate build
# rel: release build and push to ghcr.io
# Note: for release build, you need to set GHCR_TOKEN
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .profile
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
set -e
BUILD_DATE=$(date -Iminutes)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
VERSION=$(git describe --tags --abbrev=0)
VERSION="${VERSION:1}"
arr=(${VERSION//./ })
MAJOR=${arr[0]}
IMAGE=tsun-gen3-proxy
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
IMAGE=docker.io/sallius/${IMAGE}
VERSION=${VERSION}-$1
elif [[ $1 == rc ]] || [[ $1 == rel ]];then
IMAGE=ghcr.io/s-allius/${IMAGE}
else
echo argument missing!
echo try: $0 '[debug|dev|rc|rel]'
exit 1
fi
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
if [[ $1 == debug ]];then
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
elif [[ $1 == dev ]];then
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
elif [[ $1 == rc ]];then
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
echo 'login to ghcr.io'
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
docker push ghcr.io/s-allius/tsun-gen3-proxy:rc
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
elif [[ $1 == rel ]];then
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
echo 'login to ghcr.io'
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
docker push ghcr.io/s-allius/tsun-gen3-proxy:latest
docker push ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
docker push ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
fi
echo 'check docker-compose.yaml file'
docker-compose config -q

View File

@@ -1,56 +0,0 @@
# configuration to reach tsun cloud
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
tsun.host = 'logger.talent-monitoring.com'
tsun.port = 5005
# configuration to reach the new tsun cloud for G3 Plus inverters
solarman.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
solarman.host = 'iot.talent-monitoring.com'
solarman.port = 10000
# mqtt broker configuration
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883
mqtt.user = ''
mqtt.passwd = ''
# home-assistant
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
# microinverters
inverters.allow_all = true # allow inverters, even if we have no inverter mapping
# inverter mapping, maps a `serial_no* to a `mqtt_id` and defines an optional `suggested_place` for `home-assistant`
#
# for each inverter add a block starting with [inverters."<16-digit serial numbeer>"]
[inverters."R170000000000001"]
#node_id = '' # Optional, MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation area for home-assistant
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
#[inverters."R17xxxxxxxxxxxx2"]
#node_id = '' # Optional, MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation area for home-assistant
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
[inverters."Y170000000000001"]
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
#node_id = '' # Optional, MQTT replacement for inverters serial number
#suggested_area = '' # Optional, suggested installation place for home-assistant
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
[gen3plus.at_acl]
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
tsun.block = []
mqtt.allow = ['AT+']
mqtt.block = []

93
app/docker-bake.hcl Normal file
View File

@@ -0,0 +1,93 @@
variable "IMAGE" {
default = "tsun-gen3-proxy"
}
variable "VERSION" {
default = "0.0.0"
}
variable "MAJOR" {
default = "0"
}
variable "BUILD_DATE" {
default = "dev"
}
variable "BRANCH" {
default = ""
}
variable "DESCRIPTION" {
default = "This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations."
}
target "_common" {
context = "."
dockerfile = "Dockerfile"
args = {
VERSION = "${VERSION}"
environment = "production"
}
attest = [
"type =provenance,mode=max",
"type =sbom,generator=docker/scout-sbom-indexer:latest"
]
annotations = [
"index:org.opencontainers.image.title=TSUN Gen3 Proxy",
"index:org.opencontainers.image.authors=Stefan Allius",
"index:org.opencontainers.image.created=${BUILD_DATE}",
"index:org.opencontainers.image.version=${VERSION}",
"index:org.opencontainers.image.revision=${BRANCH}",
"index:org.opencontainers.image.description=${DESCRIPTION}",
"index:org.opencontainers.image.licenses=BSD-3-Clause",
"index:org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy"
]
labels = {
"org.opencontainers.image.title" = "TSUN Gen3 Proxy"
"org.opencontainers.image.authors" = "Stefan Allius"
"org.opencontainers.image.created" = "${BUILD_DATE}"
"org.opencontainers.image.version" = "${VERSION}"
"org.opencontainers.image.revision" = "${BRANCH}"
"org.opencontainers.image.description" = "${DESCRIPTION}"
"org.opencontainers.image.licenses" = "BSD-3-Clause"
"org.opencontainers.image.source" = "https://github.com/s-allius/tsun-gen3-proxy"
}
output = [
"type=image,push=true"
]
no-cache = false
platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
}
target "_debug" {
args = {
LOG_LVL = "DEBUG"
environment = "dev"
}
}
target "_prod" {
args = {
}
}
target "debug" {
inherits = ["_common", "_debug"]
tags = ["${IMAGE}:debug"]
}
target "dev" {
inherits = ["_common"]
tags = ["${IMAGE}:dev"]
}
target "preview" {
inherits = ["_common", "_prod"]
tags = ["${IMAGE}:preview", "${IMAGE}:${VERSION}"]
}
target "rc" {
inherits = ["_common", "_prod"]
tags = ["${IMAGE}:rc", "${IMAGE}:${VERSION}"]
}
target "rel" {
inherits = ["_common", "_prod"]
tags = ["${IMAGE}:latest", "${IMAGE}:${MAJOR}", "${IMAGE}:${VERSION}"]
no-cache = true
}

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

36
app/docu/proxy.yuml Normal file
View File

@@ -0,0 +1,36 @@
// {type:class}
// {direction:topDown}
// {generate:true}
[note: You can stick notes on diagrams too!{bg:cornsilk}]
[<<AbstractIterMeta>>||__iter__()]
[Mqtt;<<Singleton>>|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
[Proxy|<cls>db_stat;<cls>entity_prfx;<cls>discovery_prfx;<cls>proxy_node_id;<cls>proxy_unique_id;<cls>mqtt:Mqtt;;__ha_restarts|class_init();class_close();;<async>_cb_mqtt_is_up();<async>_register_proxy_stat_home_assistant();<async>_async_publ_mqtt_proxy_stat(key)]
[<<InverterIfc>>||healthy()->bool;<async>disc(shutdown_started=False);<async>create_remote();]
[<<AbstractIterMeta>>]^-.-[<<InverterIfc>>]
[InverterBase|_registry;__ha_restarts;;addr;config_id:str;prot_class:MessageProt;remote:StreamPtr;local:StreamPtr;|healthy()->bool;<async>disc(shutdown_started=False);<async>create_remote();<async>async_publ_mqtt()]
[StreamPtr||stream:ProtocolIfc;ifc:AsyncIfc]
[<<InverterIfc>>]^-.-[InverterBase]
[InverterG3]-[note: Creates an GEN3 inverter instance with prot_class:Talent{bg:cornsilk}]
[InverterG3P]-[note: Creates an GEN3PLUS inverter instance with prot_class:SolarmanV5{bg:cornsilk}]
[InverterBase]^[InverterG3]
[InverterBase]^[InverterG3P]
[Proxy]^[InverterBase]
[InverterBase]-2>[StreamPtr]
[Proxy]++->[Mqtt;<<Singleton>>]
[<<AsyncIfc>>]
[StreamPtr]-1>[<<ProtocolIfc>>]
[StreamPtr]-1>[<<AsyncIfc>>]
[<<ProtocolIfc>>]use-.->[<<AsyncIfc>>]
[ModbusConn|host;port;addr;stream:InverterG3P;|]has-1>[InverterG3P]
[ModbusTcp||<async>modbus_loop()]creates-*>[ModbusConn]

383
app/docu/proxy_2.svg Normal file
View File

@@ -0,0 +1,383 @@
<?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="548pt" height="2000pt"
viewBox="0.00 0.00 548.12 2000.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1996)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1996 544.1155,-1996 544.1155,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="239.7476,-1972 141.4834,-1972 141.4834,-1928 245.7476,-1928 245.7476,-1966 239.7476,-1972"/>
<polyline fill="none" stroke="#000000" points="239.7476,-1972 239.7476,-1966 "/>
<polyline fill="none" stroke="#000000" points="245.7476,-1966 239.7476,-1966 "/>
<text text-anchor="middle" x="193.6155" y="-1959" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
<text text-anchor="middle" x="193.6155" y="-1947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
<text text-anchor="middle" x="193.6155" y="-1935" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3 inverter!</text>
</g>
<!-- A1 -->
<g id="node2" class="node">
<title>A1</title>
<polygon fill="none" stroke="#000000" points="263.6155,-1960 263.6155,-1992 379.6155,-1992 379.6155,-1960 263.6155,-1960"/>
<text text-anchor="start" x="273.2645" y="-1973" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AbstractIterMeta&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="263.6155,-1940 263.6155,-1960 379.6155,-1960 379.6155,-1940 263.6155,-1940"/>
<polygon fill="none" stroke="#000000" points="263.6155,-1908 263.6155,-1940 379.6155,-1940 379.6155,-1908 263.6155,-1908"/>
<text text-anchor="start" x="300.2255" y="-1921" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
</g>
<!-- A15 -->
<g id="node16" class="node">
<title>A15</title>
<polygon fill="none" stroke="#000000" points="276.6155,-1748 276.6155,-1780 366.6155,-1780 366.6155,-1748 276.6155,-1748"/>
<text text-anchor="start" x="286.322" y="-1761" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;ProtocolIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="276.6155,-1716 276.6155,-1748 366.6155,-1748 366.6155,-1716 276.6155,-1716"/>
<text text-anchor="start" x="302.449" y="-1729" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
<polygon fill="none" stroke="#000000" points="276.6155,-1684 276.6155,-1716 366.6155,-1716 366.6155,-1684 276.6155,-1684"/>
<text text-anchor="start" x="306.618" y="-1697" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A1&#45;&gt;A15 -->
<g id="edge15" class="edge">
<title>A1&#45;&gt;A15</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M321.6155,-1897.756C321.6155,-1862.0883 321.6155,-1815.1755 321.6155,-1780.3644"/>
<polygon fill="none" stroke="#000000" points="318.1156,-1897.9674 321.6155,-1907.9674 325.1156,-1897.9674 318.1156,-1897.9674"/>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="77.6155,-662 77.6155,-694 175.6155,-694 175.6155,-662 77.6155,-662"/>
<text text-anchor="start" x="98.2755" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterBase</text>
<polygon fill="none" stroke="#000000" points="77.6155,-606 77.6155,-662 175.6155,-662 175.6155,-606 77.6155,-606"/>
<text text-anchor="start" x="116.6125" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="87.1685" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="92.4475" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="77.6155,-550 77.6155,-606 175.6155,-606 175.6155,-550 77.6155,-550"/>
<text text-anchor="start" x="91.0575" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
<text text-anchor="start" x="111.618" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="75.3469,-320 -.1159,-320 -.1159,-284 75.3469,-284 75.3469,-320"/>
<text text-anchor="middle" x="37.6155" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
</g>
<!-- A2&#45;&gt;A3 -->
<g id="edge1" class="edge">
<title>A2&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M103.8,-539.9668C83.1352,-465.6664 54.2132,-361.677 42.6665,-320.1609"/>
<polygon fill="none" stroke="#000000" points="100.4796,-541.0903 106.5312,-549.7868 107.2236,-539.2146 100.4796,-541.0903"/>
</g>
<!-- A4 -->
<g id="node5" class="node">
<title>A4</title>
<polygon fill="none" stroke="#000000" points="189.9521,-320 93.2789,-320 93.2789,-284 189.9521,-284 189.9521,-320"/>
<text text-anchor="middle" x="141.6155" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A4 -->
<g id="edge2" class="edge">
<title>A2&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M130.5679,-537.6831C133.7849,-469.0527 138.1335,-376.283 140.2896,-330.2853"/>
<polygon fill="#000000" stroke="#000000" points="130.5625,-537.7999 134.2771,-543.9807 130.0005,-549.7868 126.2859,-543.606 130.5625,-537.7999"/>
<polygon fill="#000000" stroke="#000000" points="140.7642,-320.1609 144.7909,-330.3606 140.53,-325.1554 140.2959,-330.1499 140.2959,-330.1499 140.2959,-330.1499 140.53,-325.1554 135.8008,-329.9391 140.7642,-320.1609 140.7642,-320.1609"/>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="315.0096,-320 208.2214,-320 208.2214,-284 315.0096,-284 315.0096,-320"/>
<text text-anchor="middle" x="261.6155" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A5 -->
<g id="edge3" class="edge">
<title>A2&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M157.861,-538.7126C166.3035,-516.9056 175.6035,-493.4873 184.6155,-472 205.944,-421.1467 232.9474,-362.7699 248.6524,-329.3512"/>
<polygon fill="#000000" stroke="#000000" points="157.8454,-538.7533 159.4203,-545.7903 153.5304,-549.9506 151.9554,-542.9136 157.8454,-538.7533"/>
<polygon fill="#000000" stroke="#000000" points="252.9567,-320.2155 252.7653,-331.1797 250.8256,-324.7387 248.6945,-329.2618 248.6945,-329.2618 248.6945,-329.2618 250.8256,-324.7387 244.6237,-327.3438 252.9567,-320.2155 252.9567,-320.2155"/>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="128.6155,-100 128.6155,-132 306.6155,-132 306.6155,-100 128.6155,-100"/>
<text text-anchor="start" x="173.167" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
<polygon fill="none" stroke="#000000" points="128.6155,-68 128.6155,-100 306.6155,-100 306.6155,-68 128.6155,-68"/>
<text text-anchor="start" x="185.3865" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
<polygon fill="none" stroke="#000000" points="128.6155,0 128.6155,-68 306.6155,-68 306.6155,0 128.6155,0"/>
<text text-anchor="start" x="169.273" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="160.104" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward()</text>
<text text-anchor="start" x="138.4245" 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="202.618" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A4&#45;&gt;A9 -->
<g id="edge9" class="edge">
<title>A4&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M151.2355,-272.1274C161.7441,-239.4955 178.9835,-185.9626 193.26,-141.6303"/>
<polygon fill="#000000" stroke="#000000" points="151.1313,-272.451 153.0996,-279.3883 147.4529,-283.8733 145.4847,-276.936 151.1313,-272.451"/>
<polygon fill="#000000" stroke="#000000" points="196.3509,-132.0321 197.5689,-142.9302 194.8182,-136.7914 193.2855,-141.5507 193.2855,-141.5507 193.2855,-141.5507 194.8182,-136.7914 189.0022,-140.1713 196.3509,-132.0321 196.3509,-132.0321"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="none" stroke="#000000" points="362.6155,-82 362.6155,-114 500.6155,-114 500.6155,-82 362.6155,-82"/>
<text text-anchor="start" x="389.1125" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
<polygon fill="none" stroke="#000000" points="362.6155,-62 362.6155,-82 500.6155,-82 500.6155,-62 362.6155,-62"/>
<polygon fill="none" stroke="#000000" points="362.6155,-18 362.6155,-62 500.6155,-62 500.6155,-18 362.6155,-18"/>
<text text-anchor="start" x="385.4935" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="372.4395" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward())</text>
</g>
<!-- A5&#45;&gt;A10 -->
<g id="edge11" class="edge">
<title>A5&#45;&gt;A10</title>
<path fill="none" stroke="#000000" d="M269.2148,-283.6405C279.6962,-259.3121 299.996,-215.5582 323.6155,-182 338.3046,-161.1299 356.5265,-140.1557 373.793,-121.8925"/>
<polygon fill="#000000" stroke="#000000" points="381.1214,-114.2395 377.4553,-124.5745 377.6632,-117.8508 374.2051,-121.4621 374.2051,-121.4621 374.2051,-121.4621 377.6632,-117.8508 370.9549,-118.3498 381.1214,-114.2395 381.1214,-114.2395"/>
<text text-anchor="middle" x="268.7308" y="-260.6464" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="396.6155,-1114 396.6155,-1146 513.6155,-1146 513.6155,-1114 396.6155,-1114"/>
<text text-anchor="start" x="424.5445" y="-1127" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AsyncIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="396.6155,-1094 396.6155,-1114 513.6155,-1114 513.6155,-1094 396.6155,-1094"/>
<polygon fill="none" stroke="#000000" points="396.6155,-822 396.6155,-1094 513.6155,-1094 513.6155,-822 396.6155,-822"/>
<text text-anchor="start" x="424.5515" y="-1075" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
<text text-anchor="start" x="422.8815" y="-1063" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
<text text-anchor="start" x="436.779" y="-1039" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
<text text-anchor="start" x="434.5595" y="-1027" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
<text text-anchor="start" x="438.169" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
<text text-anchor="start" x="434.279" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
<text text-anchor="start" x="438.449" y="-991" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
<text text-anchor="start" x="434.2845" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
<text text-anchor="start" x="438.449" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
<text text-anchor="start" x="432.89" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
<text text-anchor="start" x="434.56" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
<text text-anchor="start" x="437.894" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
<text text-anchor="start" x="434.004" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
<text text-anchor="start" x="438.174" y="-895" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
<text text-anchor="start" x="434.0095" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
<text text-anchor="start" x="438.174" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
<text text-anchor="start" x="430.1145" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
<text text-anchor="start" x="406.495" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="447.6155,-652 447.6155,-684 540.6155,-684 540.6155,-652 447.6155,-652"/>
<text text-anchor="start" x="465.7795" y="-665" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
<polygon fill="none" stroke="#000000" points="447.6155,-560 447.6155,-652 540.6155,-652 540.6155,-560 447.6155,-560"/>
<text text-anchor="start" x="457.1635" y="-633" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
<text text-anchor="start" x="461.0525" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
<text text-anchor="start" x="460.7775" y="-609" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
<text text-anchor="start" x="460.2115" y="-597" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
<text text-anchor="start" x="476.329" y="-585" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="469.665" y="-573" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
</g>
<!-- A6&#45;&gt;A7 -->
<g id="edge4" class="edge">
<title>A6&#45;&gt;A7</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M473.1735,-811.7434C478.1009,-766.0069 483.1088,-719.5241 486.9345,-684.013"/>
<polygon fill="none" stroke="#000000" points="469.682,-811.4771 472.0907,-821.7945 476.6418,-812.227 469.682,-811.4771"/>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="none" stroke="#000000" points="418.6155,-390 418.6155,-422 520.6155,-422 520.6155,-390 418.6155,-390"/>
<text text-anchor="start" x="439.8895" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
<polygon fill="none" stroke="#000000" points="418.6155,-310 418.6155,-390 520.6155,-390 520.6155,-310 418.6155,-310"/>
<text text-anchor="start" x="455.1685" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
<text text-anchor="start" x="457.3985" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
<text text-anchor="start" x="459.6125" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="455.1685" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
<text text-anchor="start" x="455.7235" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
<polygon fill="none" stroke="#000000" points="418.6155,-182 418.6155,-310 520.6155,-310 520.6155,-182 418.6155,-182"/>
<text text-anchor="start" x="441.2695" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="457.3975" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
<text text-anchor="start" x="454.618" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="450.1695" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="434.886" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
<text text-anchor="start" x="434.3365" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
<text text-anchor="start" x="428.2225" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge5" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M488.1838,-549.5774C485.3646,-511.9877 481.8463,-465.0771 478.6327,-422.2295"/>
<polygon fill="none" stroke="#000000" points="484.7214,-550.2112 488.9596,-559.9214 491.7018,-549.6876 484.7214,-550.2112"/>
</g>
<!-- A8&#45;&gt;A9 -->
<g id="edge6" class="edge">
<title>A8&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M412.0877,-185.8777C410.9473,-184.56 409.7899,-183.2666 408.6155,-182 380.1271,-151.2753 341.6819,-125.829 306.7513,-106.6759"/>
<polygon fill="none" stroke="#000000" points="409.4058,-188.1271 418.4338,-193.672 414.834,-183.7074 409.4058,-188.1271"/>
</g>
<!-- A8&#45;&gt;A10 -->
<g id="edge7" class="edge">
<title>A8&#45;&gt;A10</title>
<path fill="none" stroke="#000000" d="M448.6523,-171.8077C445.3431,-151.2556 442.1142,-131.2022 439.3729,-114.1772"/>
<polygon fill="none" stroke="#000000" points="445.2363,-172.6095 450.2815,-181.9259 452.1472,-171.4966 445.2363,-172.6095"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="193.6155,-740 193.6155,-772 307.6155,-772 307.6155,-740 193.6155,-740"/>
<text text-anchor="start" x="236.7235" y="-753" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
<polygon fill="none" stroke="#000000" points="193.6155,-600 193.6155,-740 307.6155,-740 307.6155,-600 193.6155,-600"/>
<text text-anchor="start" x="231.4385" y="-721" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
<text text-anchor="start" x="240.6125" y="-709" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="203.3785" y="-685" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
<text text-anchor="start" x="238.393" y="-673" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
<text text-anchor="start" x="219.2155" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
<text text-anchor="start" x="222.5555" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
<text text-anchor="start" x="226.16" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
<text text-anchor="start" x="224.4995" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
<text text-anchor="start" x="236.7275" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="193.6155,-472 193.6155,-600 307.6155,-600 307.6155,-472 193.6155,-472"/>
<text text-anchor="start" x="208.108" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
<text text-anchor="start" x="210.048" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
<text text-anchor="start" x="215.892" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
<text text-anchor="start" x="203.944" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
<text text-anchor="start" x="205.889" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
<text text-anchor="start" x="215.056" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="231.1695" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="235.618" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A11&#45;&gt;A4 -->
<g id="edge8" class="edge">
<title>A11&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M196.2254,-462.3225C179.1579,-412.2162 162.0761,-362.0677 151.6753,-331.5332"/>
<polygon fill="#000000" stroke="#000000" points="199.4666,-471.8382 191.9826,-463.8233 197.8544,-467.1053 196.2422,-462.3723 196.2422,-462.3723 196.2422,-462.3723 197.8544,-467.1053 200.5019,-460.9213 199.4666,-471.8382 199.4666,-471.8382"/>
<polygon fill="#000000" stroke="#000000" points="151.6435,-331.4398 145.9225,-327.05 147.7742,-320.0807 153.4952,-324.4705 151.6435,-331.4398"/>
</g>
<!-- A11&#45;&gt;A5 -->
<g id="edge10" class="edge">
<title>A11&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M256.1287,-461.6172C258.0803,-404.8425 260.0297,-348.132 260.994,-320.0807"/>
<polygon fill="#000000" stroke="#000000" points="255.7773,-471.8382 251.6236,-461.6895 255.9491,-466.8412 256.121,-461.8441 256.121,-461.8441 256.121,-461.8441 255.9491,-466.8412 260.6183,-461.9988 255.7773,-471.8382 255.7773,-471.8382"/>
<text text-anchor="middle" x="268.8186" y="-335.4866" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A13 -->
<g id="node14" class="node">
<title>A13</title>
<polygon fill="none" stroke="#000000" points="333.6155,-318 333.6155,-350 400.6155,-350 400.6155,-318 333.6155,-318"/>
<text text-anchor="start" x="349.6085" y="-331" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
<polygon fill="none" stroke="#000000" points="333.6155,-298 333.6155,-318 400.6155,-318 400.6155,-298 333.6155,-298"/>
<polygon fill="none" stroke="#000000" points="333.6155,-254 333.6155,-298 400.6155,-298 400.6155,-254 333.6155,-254"/>
<text text-anchor="start" x="343.4995" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="351.2835" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
</g>
<!-- A11&#45;&gt;A13 -->
<g id="edge13" class="edge">
<title>A11&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M305.5203,-471.8728C311.6394,-455.0532 317.7699,-438.1631 323.6155,-422 330.9569,-401.7009 338.9463,-379.4498 346.0242,-359.681"/>
<polygon fill="#000000" stroke="#000000" points="349.4187,-350.1951 350.2862,-361.1266 347.734,-354.9028 346.0494,-359.6104 346.0494,-359.6104 346.0494,-359.6104 347.734,-354.9028 341.8125,-358.0942 349.4187,-350.1951 349.4187,-350.1951"/>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="326.6155,-710 326.6155,-742 429.6155,-742 429.6155,-710 326.6155,-710"/>
<text text-anchor="start" x="367.2775" y="-723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
<polygon fill="none" stroke="#000000" points="326.6155,-654 326.6155,-710 429.6155,-710 429.6155,-654 326.6155,-654"/>
<text text-anchor="start" x="370.057" y="-691" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="345.6015" y="-679" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="359.219" y="-667" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="326.6155,-502 326.6155,-654 429.6155,-654 429.6155,-502 326.6155,-502"/>
<text text-anchor="start" x="353.951" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="352" y="-623" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="348.946" y="-611" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="347.276" y="-599" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="345.3255" y="-587" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
<text text-anchor="start" x="360.3285" y="-575" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
<text text-anchor="start" x="353.1095" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
<text text-anchor="start" x="354.49" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
<text text-anchor="start" x="338.6525" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
<text text-anchor="start" x="348.101" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
<text text-anchor="start" x="336.438" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
</g>
<!-- A12&#45;&gt;A13 -->
<g id="edge12" class="edge">
<title>A12&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M373.1357,-491.6786C371.4196,-441.7544 369.5661,-387.8351 368.2756,-350.293"/>
<polygon fill="none" stroke="#000000" points="369.6466,-492.0596 373.4882,-501.9334 376.6425,-491.819 369.6466,-492.0596"/>
</g>
<!-- A14 -->
<g id="node15" class="node">
<title>A14</title>
<polygon fill="none" stroke="#000000" points="297.6155,-1524 297.6155,-1556 446.6155,-1556 446.6155,-1524 297.6155,-1524"/>
<text text-anchor="start" x="351.833" y="-1537" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
<polygon fill="none" stroke="#000000" points="297.6155,-1300 297.6155,-1524 446.6155,-1524 446.6155,-1300 297.6155,-1300"/>
<text text-anchor="start" x="335.442" y="-1505" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
<text text-anchor="start" x="345.9995" y="-1493" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
<text text-anchor="start" x="346.834" y="-1481" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
<text text-anchor="start" x="354.329" y="-1469" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="332.6585" y="-1457" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
<text text-anchor="start" x="347.1055" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
<text text-anchor="start" x="352.9395" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
<text text-anchor="start" x="350.44" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
<text text-anchor="start" x="344.3305" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
<text text-anchor="start" x="341.2715" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
<text text-anchor="start" x="348.2155" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
<text text-anchor="start" x="321.82" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
<text text-anchor="start" x="341" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
<text text-anchor="start" x="337.1225" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
<text text-anchor="start" x="346.0005" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
<text text-anchor="start" x="335.168" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
<text text-anchor="start" x="326.2695" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
<polygon fill="none" stroke="#000000" points="297.6155,-1196 297.6155,-1300 446.6155,-1300 446.6155,-1196 297.6155,-1196"/>
<text text-anchor="start" x="321" y="-1281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
<text text-anchor="start" x="349.6155" y="-1269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
<text text-anchor="start" x="322.383" y="-1257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
<text text-anchor="start" x="307.375" y="-1245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt; end_modbus_cmd()</text>
<text text-anchor="start" x="357.118" y="-1233" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="342.946" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="341.276" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
</g>
<!-- A14&#45;&gt;A6 -->
<g id="edge14" class="edge">
<title>A14&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M409.7752,-1195.7758C412.5746,-1182.5547 415.3943,-1169.2373 418.1831,-1156.0662"/>
<polygon fill="#000000" stroke="#000000" points="420.3088,-1146.0268 422.6397,-1156.7421 419.2731,-1150.9183 418.2373,-1155.8099 418.2373,-1155.8099 418.2373,-1155.8099 419.2731,-1150.9183 413.8349,-1154.8777 420.3088,-1146.0268 420.3088,-1146.0268"/>
<text text-anchor="middle" x="405.2609" y="-1173.292" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
</g>
<!-- A14&#45;&gt;A11 -->
<g id="edge17" class="edge">
<title>A14&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M341.1152,-1185.9405C320.5475,-1057.7747 293.7865,-891.0162 274.7116,-772.1524"/>
<polygon fill="none" stroke="#000000" points="337.6695,-1186.5583 342.7099,-1195.8774 344.5811,-1185.4491 337.6695,-1186.5583"/>
</g>
<!-- A15&#45;&gt;A14 -->
<g id="edge16" class="edge">
<title>A15&#45;&gt;A14</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M329.7896,-1673.8004C334.3532,-1641.3079 340.3126,-1598.8764 346.2965,-1556.2713"/>
<polygon fill="none" stroke="#000000" points="326.2837,-1673.5986 328.3587,-1683.9883 333.2156,-1674.5723 326.2837,-1673.5986"/>
</g>
<!-- A16 -->
<g id="node17" class="node">
<title>A16</title>
<polygon fill="none" stroke="#000000" points="385.6155,-1826 385.6155,-1858 460.6155,-1858 460.6155,-1826 385.6155,-1826"/>
<text text-anchor="start" x="405.333" y="-1839" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
<polygon fill="none" stroke="#000000" points="385.6155,-1674 385.6155,-1826 460.6155,-1826 460.6155,-1674 385.6155,-1674"/>
<text text-anchor="start" x="414.777" y="-1807" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
<text text-anchor="start" x="395.6055" y="-1783" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
<text text-anchor="start" x="396.7205" y="-1771" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
<text text-anchor="start" x="406.724" y="-1759" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
<text text-anchor="start" x="397.005" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
<text text-anchor="start" x="405.0575" y="-1735" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
<text text-anchor="start" x="417.007" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
<text text-anchor="start" x="403.669" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
<text text-anchor="start" x="401.9945" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
<text text-anchor="start" x="416.452" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
<polygon fill="none" stroke="#000000" points="385.6155,-1606 385.6155,-1674 460.6155,-1674 460.6155,-1606 385.6155,-1606"/>
<text text-anchor="start" x="397.0055" y="-1655" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
<text text-anchor="start" x="400.3395" y="-1643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
<text text-anchor="start" x="397.8395" y="-1631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
<text text-anchor="start" x="408.118" y="-1619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A16&#45;&gt;A14 -->
<g id="edge18" class="edge">
<title>A16&#45;&gt;A14</title>
<path fill="none" stroke="#000000" d="M403.1382,-1596.041C401.2623,-1582.9463 399.3403,-1569.5297 397.4159,-1556.0971"/>
<polygon fill="#000000" stroke="#000000" points="404.563,-1605.9867 398.6903,-1596.726 403.8539,-1601.0373 403.1448,-1596.0878 403.1448,-1596.0878 403.1448,-1596.0878 403.8539,-1601.0373 407.5994,-1595.4496 404.563,-1605.9867 404.563,-1605.9867"/>
<text text-anchor="middle" x="408.3534" y="-1569.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
<text text-anchor="middle" x="393.6256" y="-1586.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 34 KiB

43
app/docu/proxy_2.yuml Normal file
View File

@@ -0,0 +1,43 @@
// {type:class}
// {direction:topDown}
// {generate:true}
[note: Example of instantiation for a GEN3 inverter!{bg:cornsilk}]
[<<AbstractIterMeta>>||__iter__()]
[InverterBase|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
[InverterBase]^[InverterG3]
[InverterBase]++->[local:StreamPtr]
[InverterBase]++->[remote:StreamPtr]
[<<AsyncIfc>>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()]
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
[AsyncStream|reader;writer;addr;r_addr;l_addr|;<async>loop;disc();close();healthy();;__async_read();__async_write();__async_forward()]
[AsyncStreamServer|create_remote|<async>server_loop();<async>_async_forward();<async>publish_outstanding_mqtt();close()]
[AsyncStreamClient||<async>client_loop();<async>_async_forward())]
[<<AsyncIfc>>]^-.-[AsyncIfcImpl]
[AsyncIfcImpl]^[AsyncStream]
[AsyncStream]^[AsyncStreamServer]
[AsyncStream]^[AsyncStreamClient]
[Talent|conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()]
[Talent]<-++[local:StreamPtr]
[local:StreamPtr]++->[AsyncStreamServer]
[Talent]<-0..1[remote:StreamPtr]
[remote:StreamPtr]0..1->[AsyncStreamClient]
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
[Infos]^[InfosG3||ha_confs();parse()]
[Talent]->[InfosG3]
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
[Message]use->[<<AsyncIfc>>]
[<<ProtocolIfc>>|_registry|close()]
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
[<<ProtocolIfc>>]^-.-[Message]
[Message]^[Talent]
[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
[Modbus]<0..1-has[Message]

382
app/docu/proxy_3.svg Normal file
View File

@@ -0,0 +1,382 @@
<?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="597pt" height="1940pt"
viewBox="0.00 0.00 597.00 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 593,-1936 593,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="287.2332,-1912 172.7668,-1912 172.7668,-1868 293.2332,-1868 293.2332,-1906 287.2332,-1912"/>
<polyline fill="none" stroke="#000000" points="287.2332,-1912 287.2332,-1906 "/>
<polyline fill="none" stroke="#000000" points="293.2332,-1906 287.2332,-1906 "/>
<text text-anchor="middle" x="233" y="-1899" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Example of</text>
<text text-anchor="middle" x="233" y="-1887" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">instantiation for a</text>
<text text-anchor="middle" x="233" y="-1875" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">GEN3PLUS inverter!</text>
</g>
<!-- A1 -->
<g id="node2" class="node">
<title>A1</title>
<polygon fill="none" stroke="#000000" points="311,-1900 311,-1932 427,-1932 427,-1900 311,-1900"/>
<text text-anchor="start" x="320.649" y="-1913" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AbstractIterMeta&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="311,-1880 311,-1900 427,-1900 427,-1880 311,-1880"/>
<polygon fill="none" stroke="#000000" points="311,-1848 311,-1880 427,-1880 427,-1848 311,-1848"/>
<text text-anchor="start" x="347.61" y="-1861" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__()</text>
</g>
<!-- A15 -->
<g id="node16" class="node">
<title>A15</title>
<polygon fill="none" stroke="#000000" points="324,-1688 324,-1720 414,-1720 414,-1688 324,-1688"/>
<text text-anchor="start" x="333.7065" y="-1701" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;ProtocolIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="324,-1656 324,-1688 414,-1688 414,-1656 324,-1656"/>
<text text-anchor="start" x="349.8335" y="-1669" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_registry</text>
<polygon fill="none" stroke="#000000" points="324,-1624 324,-1656 414,-1656 414,-1624 324,-1624"/>
<text text-anchor="start" x="354.0025" y="-1637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A1&#45;&gt;A15 -->
<g id="edge15" class="edge">
<title>A1&#45;&gt;A15</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M369,-1837.756C369,-1802.0883 369,-1755.1755 369,-1720.3644"/>
<polygon fill="none" stroke="#000000" points="365.5001,-1837.9674 369,-1847.9674 372.5001,-1837.9674 365.5001,-1837.9674"/>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="128,-632 128,-664 226,-664 226,-632 128,-632"/>
<text text-anchor="start" x="148.66" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterBase</text>
<polygon fill="none" stroke="#000000" points="128,-576 128,-632 226,-632 226,-576 128,-576"/>
<text text-anchor="start" x="166.997" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="137.553" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
<text text-anchor="start" x="142.832" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
<polygon fill="none" stroke="#000000" points="128,-520 128,-576 226,-576 226,-520 128,-520"/>
<text text-anchor="start" x="141.442" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote()</text>
<text text-anchor="start" x="162.0025" 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="0,-302 0,-334 116,-334 116,-302 0,-302"/>
<text text-anchor="start" x="31.05" y="-315" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
<polygon fill="none" stroke="#000000" points="0,-270 0,-302 116,-302 116,-270 0,-270"/>
<text text-anchor="start" x="9.6585" y="-283" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">forward_at_cmd_resp</text>
</g>
<!-- A2&#45;&gt;A3 -->
<g id="edge1" class="edge">
<title>A2&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M143.4931,-510.3444C119.4997,-451.8732 88.4875,-376.2972 71.177,-334.1121"/>
<polygon fill="none" stroke="#000000" points="140.3971,-512.0193 147.4314,-519.942 146.873,-509.3619 140.3971,-512.0193"/>
</g>
<!-- A4 -->
<g id="node5" class="node">
<title>A4</title>
<polygon fill="none" stroke="#000000" points="230.3366,-320 133.6634,-320 133.6634,-284 230.3366,-284 230.3366,-320"/>
<text text-anchor="middle" x="182" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">local:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A4 -->
<g id="edge2" class="edge">
<title>A2&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M178.4553,-507.5905C179.4883,-447.68 180.8113,-370.9429 181.5127,-330.266"/>
<polygon fill="#000000" stroke="#000000" points="178.4493,-507.9438 182.3453,-514.0119 178.2424,-519.942 174.3465,-513.8739 178.4493,-507.9438"/>
<polygon fill="#000000" stroke="#000000" points="181.6851,-320.2627 186.012,-330.3388 181.5989,-325.2619 181.5126,-330.2612 181.5126,-330.2612 181.5126,-330.2612 181.5989,-325.2619 177.0133,-330.1836 181.6851,-320.2627 181.6851,-320.2627"/>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="355.3941,-320 248.6059,-320 248.6059,-284 355.3941,-284 355.3941,-320"/>
<text text-anchor="middle" x="302" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote:StreamPtr</text>
</g>
<!-- A2&#45;&gt;A5 -->
<g id="edge3" class="edge">
<title>A2&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M212.8581,-508.8093C238.9076,-448.3743 272.5536,-370.3156 290.1233,-329.5539"/>
<polygon fill="#000000" stroke="#000000" points="212.8095,-508.9221 214.1078,-516.0154 208.0595,-519.942 206.7612,-512.8487 212.8095,-508.9221"/>
<polygon fill="#000000" stroke="#000000" points="294.1282,-320.2627 294.3023,-331.2272 292.149,-324.8543 290.1698,-329.4459 290.1698,-329.4459 290.1698,-329.4459 292.149,-324.8543 286.0373,-327.6647 294.1282,-320.2627 294.1282,-320.2627"/>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="183,-100 183,-132 361,-132 361,-100 183,-100"/>
<text text-anchor="start" x="227.5515" y="-113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamServer</text>
<polygon fill="none" stroke="#000000" points="183,-68 183,-100 361,-100 361,-68 183,-68"/>
<text text-anchor="start" x="239.771" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create_remote</text>
<polygon fill="none" stroke="#000000" points="183,0 183,-68 361,-68 361,0 183,0"/>
<text text-anchor="start" x="223.6575" y="-49" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="214.4885" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward()</text>
<text text-anchor="start" x="192.809" 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="257.0025" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A4&#45;&gt;A9 -->
<g id="edge9" class="edge">
<title>A4&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M193.2169,-272.5869C205.6662,-239.9419 226.2449,-185.9801 243.2484,-141.3932"/>
<polygon fill="#000000" stroke="#000000" points="193.1887,-272.661 194.7881,-279.6925 188.9127,-283.8733 187.3132,-276.8418 193.1887,-272.661"/>
<polygon fill="#000000" stroke="#000000" points="246.8183,-132.0321 247.4596,-142.9792 245.0366,-136.7039 243.2549,-141.3757 243.2549,-141.3757 243.2549,-141.3757 245.0366,-136.7039 239.0503,-139.7723 246.8183,-132.0321 246.8183,-132.0321"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="none" stroke="#000000" points="424,-82 424,-114 562,-114 562,-82 424,-82"/>
<text text-anchor="start" x="450.497" y="-95" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStreamClient</text>
<polygon fill="none" stroke="#000000" points="424,-62 424,-82 562,-82 562,-62 424,-62"/>
<polygon fill="none" stroke="#000000" points="424,-18 424,-62 562,-62 562,-18 424,-18"/>
<text text-anchor="start" x="446.878" y="-43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="433.824" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;_async_forward())</text>
</g>
<!-- A5&#45;&gt;A10 -->
<g id="edge11" class="edge">
<title>A5&#45;&gt;A10</title>
<path fill="none" stroke="#000000" d="M308.989,-283.7374C318.9281,-259.1334 338.7724,-214.6635 364,-182 380.8963,-160.1235 402.2571,-139.0239 422.71,-120.9559"/>
<polygon fill="#000000" stroke="#000000" points="430.4931,-114.1842 425.9026,-124.143 426.721,-117.4662 422.9488,-120.7481 422.9488,-120.7481 422.9488,-120.7481 426.721,-117.4662 419.9951,-117.3532 430.4931,-114.1842 430.4931,-114.1842"/>
<text text-anchor="middle" x="308.0806" y="-260.758" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="443,-1054 443,-1086 560,-1086 560,-1054 443,-1054"/>
<text text-anchor="start" x="470.929" y="-1067" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;AsyncIfc&gt;&gt;</text>
<polygon fill="none" stroke="#000000" points="443,-1034 443,-1054 560,-1054 560,-1034 443,-1034"/>
<polygon fill="none" stroke="#000000" points="443,-762 443,-1034 560,-1034 560,-762 443,-762"/>
<text text-anchor="start" x="470.936" y="-1015" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_node_id()</text>
<text text-anchor="start" x="469.266" y="-1003" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_conn_no()</text>
<text text-anchor="start" x="483.1635" y="-979" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_add()</text>
<text text-anchor="start" x="480.944" y="-967" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_flush()</text>
<text text-anchor="start" x="484.5535" y="-955" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_get()</text>
<text text-anchor="start" x="480.6635" y="-943" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_peek()</text>
<text text-anchor="start" x="484.8335" y="-931" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_log()</text>
<text text-anchor="start" x="480.669" y="-919" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_clear()</text>
<text text-anchor="start" x="484.8335" y="-907" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_len()</text>
<text text-anchor="start" x="479.2745" y="-883" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_add()</text>
<text text-anchor="start" x="480.9445" y="-871" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_log()</text>
<text text-anchor="start" x="484.2785" y="-859" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_get()</text>
<text text-anchor="start" x="480.3885" y="-847" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_peek()</text>
<text text-anchor="start" x="484.5585" y="-835" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_log()</text>
<text text-anchor="start" x="480.394" y="-823" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_clear()</text>
<text text-anchor="start" x="484.5585" y="-811" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_len()</text>
<text text-anchor="start" x="476.499" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_set_cb()</text>
<text text-anchor="start" x="452.8795" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">prot_set_timeout_cb()</text>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="494,-622 494,-654 587,-654 587,-622 494,-622"/>
<text text-anchor="start" x="512.164" y="-635" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncIfcImpl</text>
<polygon fill="none" stroke="#000000" points="494,-530 494,-622 587,-622 587,-530 494,-530"/>
<text text-anchor="start" x="503.548" y="-603" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">fwd_fifo:ByteFifo</text>
<text text-anchor="start" x="507.437" y="-591" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tx_fifo:ByteFifo</text>
<text text-anchor="start" x="507.162" y="-579" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rx_fifo:ByteFifo</text>
<text text-anchor="start" x="506.596" y="-567" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no:Count</text>
<text text-anchor="start" x="522.7135" y="-555" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="516.0495" y="-543" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout_cb</text>
</g>
<!-- A6&#45;&gt;A7 -->
<g id="edge4" class="edge">
<title>A6&#45;&gt;A7</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M521.2574,-751.5524C525.3561,-716.6607 529.4102,-682.1494 532.6937,-654.1971"/>
<polygon fill="none" stroke="#000000" points="517.7337,-751.5502 520.043,-761.8903 524.6859,-752.367 517.7337,-751.5502"/>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="none" stroke="#000000" points="487,-390 487,-422 589,-422 589,-390 487,-390"/>
<text text-anchor="start" x="508.274" y="-403" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
<polygon fill="none" stroke="#000000" points="487,-310 487,-390 589,-390 589,-310 487,-310"/>
<text text-anchor="start" x="523.553" y="-371" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
<text text-anchor="start" x="525.783" y="-359" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
<text text-anchor="start" x="527.997" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="523.553" y="-335" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
<text text-anchor="start" x="524.108" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
<polygon fill="none" stroke="#000000" points="487,-182 487,-310 589,-310 589,-182 487,-182"/>
<text text-anchor="start" x="509.654" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="525.782" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
<text text-anchor="start" x="523.0025" y="-255" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="518.554" y="-243" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="503.2705" y="-219" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
<text text-anchor="start" x="502.721" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_write()</text>
<text text-anchor="start" x="496.607" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge5" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M539.5006,-519.5861C539.2971,-490.0737 539.0562,-455.1552 538.8278,-422.0295"/>
<polygon fill="none" stroke="#000000" points="536.002,-519.8121 539.5709,-529.7877 543.0018,-519.7638 536.002,-519.8121"/>
</g>
<!-- A8&#45;&gt;A9 -->
<g id="edge6" class="edge">
<title>A8&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M480.5271,-185.826C479.3695,-184.5245 478.1938,-183.2483 477,-182 444.7093,-148.2346 400.4099,-121.5033 361.252,-102.2528"/>
<polygon fill="none" stroke="#000000" points="477.867,-188.1011 486.9604,-193.5382 483.2424,-183.6171 477.867,-188.1011"/>
</g>
<!-- A8&#45;&gt;A10 -->
<g id="edge7" class="edge">
<title>A8&#45;&gt;A10</title>
<path fill="none" stroke="#000000" d="M513.2286,-172.088C509.2911,-151.438 505.4474,-131.2796 502.1863,-114.1772"/>
<polygon fill="none" stroke="#000000" points="509.7933,-172.7584 515.1045,-181.9259 516.6695,-171.4473 509.7933,-172.7584"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="244,-668 244,-700 354,-700 354,-668 244,-668"/>
<text text-anchor="start" x="271.495" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
<polygon fill="none" stroke="#000000" points="244,-552 244,-668 354,-668 354,-552 244,-552"/>
<text text-anchor="start" x="279.823" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">conn_no</text>
<text text-anchor="start" x="288.997" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="253.994" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inverter:InverterG3P</text>
<text text-anchor="start" x="283.998" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
<text text-anchor="start" x="287.0575" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
<text text-anchor="start" x="292.056" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
<text text-anchor="start" x="271.21" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
<text text-anchor="start" x="285.112" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="244,-484 244,-552 354,-552 354,-484 244,-484"/>
<text text-anchor="start" x="263.4405" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="279.554" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
<text text-anchor="start" x="284.0025" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A11&#45;&gt;A4 -->
<g id="edge8" class="edge">
<title>A11&#45;&gt;A4</title>
<path fill="none" stroke="#000000" d="M251.4932,-474.2481C230.4181,-422.0107 207.3684,-364.879 193.8227,-331.3042"/>
<polygon fill="#000000" stroke="#000000" points="255.2671,-483.6023 247.3524,-476.0123 253.3964,-478.9655 251.5256,-474.3286 251.5256,-474.3286 251.5256,-474.3286 253.3964,-478.9655 255.6988,-472.6449 255.2671,-483.6023 255.2671,-483.6023"/>
<polygon fill="#000000" stroke="#000000" points="193.7733,-331.1815 187.8189,-327.1139 189.2835,-320.053 195.2379,-324.1207 193.7733,-331.1815"/>
</g>
<!-- A11&#45;&gt;A5 -->
<g id="edge10" class="edge">
<title>A11&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M300.2256,-473.5237C300.8304,-415.0627 301.4968,-350.6465 301.8132,-320.053"/>
<polygon fill="#000000" stroke="#000000" points="300.1214,-483.6023 295.7251,-473.5562 300.1731,-478.6026 300.2249,-473.6028 300.2249,-473.6028 300.2249,-473.6028 300.1731,-478.6026 304.7247,-473.6494 300.1214,-483.6023 300.1214,-483.6023"/>
<text text-anchor="middle" x="310.0777" y="-335.2657" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
<!-- A13 -->
<g id="node14" class="node">
<title>A13</title>
<polygon fill="none" stroke="#000000" points="374,-336 374,-368 469,-368 469,-336 374,-336"/>
<text text-anchor="start" x="400.6585" y="-349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
<polygon fill="none" stroke="#000000" points="374,-304 374,-336 469,-336 469,-304 374,-304"/>
<text text-anchor="start" x="383.7125" y="-317" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">client_mode:bool</text>
<polygon fill="none" stroke="#000000" points="374,-236 374,-304 469,-304 469,-236 374,-236"/>
<text text-anchor="start" x="397.884" y="-285" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="405.668" y="-273" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
<text text-anchor="start" x="409.282" y="-261" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">calc()</text>
<text text-anchor="start" x="407.6135" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build()</text>
</g>
<!-- A11&#45;&gt;A13 -->
<g id="edge13" class="edge">
<title>A11&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M344.6018,-483.6023C359.4473,-448.3138 375.5948,-409.9305 389.2039,-377.5809"/>
<polygon fill="#000000" stroke="#000000" points="393.1562,-368.1861 393.4263,-379.1487 391.2173,-372.7949 389.2784,-377.4036 389.2784,-377.4036 389.2784,-377.4036 391.2173,-372.7949 385.1305,-375.6586 393.1562,-368.1861 393.1562,-368.1861"/>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="373,-680 373,-712 476,-712 476,-680 373,-680"/>
<text text-anchor="start" x="413.662" y="-693" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
<polygon fill="none" stroke="#000000" points="373,-624 373,-680 476,-680 476,-624 373,-624"/>
<text text-anchor="start" x="416.4415" y="-661" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="391.986" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="405.6035" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="373,-472 373,-624 476,-624 476,-472 373,-472"/>
<text text-anchor="start" x="400.3355" y="-605" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="398.3845" y="-593" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="395.3305" y="-581" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="393.6605" y="-569" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="391.71" y="-557" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
<text text-anchor="start" x="406.713" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
<text text-anchor="start" x="399.494" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_remove</text>
<text text-anchor="start" x="400.8745" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
<text text-anchor="start" x="385.037" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
<text text-anchor="start" x="394.4855" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
<text text-anchor="start" x="382.8225" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
</g>
<!-- A12&#45;&gt;A13 -->
<g id="edge12" class="edge">
<title>A12&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M422.6543,-461.9134C422.3183,-429.4373 421.9719,-395.9527 421.6835,-368.0691"/>
<polygon fill="none" stroke="#000000" points="419.1548,-461.9893 422.7581,-471.9525 426.1544,-461.9168 419.1548,-461.9893"/>
</g>
<!-- A14 -->
<g id="node15" class="node">
<title>A14</title>
<polygon fill="none" stroke="#000000" points="345,-1464 345,-1496 494,-1496 494,-1464 345,-1464"/>
<text text-anchor="start" x="399.2175" y="-1477" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
<polygon fill="none" stroke="#000000" points="345,-1240 345,-1464 494,-1464 494,-1240 345,-1240"/>
<text text-anchor="start" x="382.8265" y="-1445" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
<text text-anchor="start" x="393.384" y="-1433" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
<text text-anchor="start" x="394.2185" y="-1421" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ifc:AsyncIfc</text>
<text text-anchor="start" x="401.7135" y="-1409" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="380.043" y="-1397" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
<text text-anchor="start" x="394.49" y="-1385" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len</text>
<text text-anchor="start" x="400.324" y="-1373" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len</text>
<text text-anchor="start" x="397.8245" y="-1361" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
<text text-anchor="start" x="391.715" y="-1349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area:str</text>
<text text-anchor="start" x="388.656" y="-1337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:dict</text>
<text text-anchor="start" x="395.6" y="-1325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state:State</text>
<text text-anchor="start" x="369.2045" y="-1313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">shutdown_started:bool</text>
<text text-anchor="start" x="388.3845" y="-1301" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_elms</text>
<text text-anchor="start" x="384.507" y="-1289" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timer:Timer</text>
<text text-anchor="start" x="393.385" y="-1277" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_timeout</text>
<text text-anchor="start" x="382.5525" y="-1265" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb_first_timeout</text>
<text text-anchor="start" x="373.654" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">modbus_polling:bool</text>
<polygon fill="none" stroke="#000000" points="345,-1136 345,-1240 494,-1240 494,-1136 345,-1136"/>
<text text-anchor="start" x="368.3845" y="-1221" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_set_mqtt_timestamp()</text>
<text text-anchor="start" x="397" y="-1209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_timeout()</text>
<text text-anchor="start" x="369.7675" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_modbus_cmd()</text>
<text text-anchor="start" x="354.7595" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt; end_modbus_cmd()</text>
<text text-anchor="start" x="404.5025" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="390.3305" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="388.6605" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
</g>
<!-- A14&#45;&gt;A6 -->
<g id="edge14" class="edge">
<title>A14&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M456.7,-1135.7758C459.4656,-1122.5547 462.2514,-1109.2373 465.0066,-1096.0662"/>
<polygon fill="#000000" stroke="#000000" points="467.1066,-1086.0268 469.4637,-1096.7363 466.0828,-1090.9209 465.059,-1095.8149 465.059,-1095.8149 465.059,-1095.8149 466.0828,-1090.9209 460.6544,-1094.8935 467.1066,-1086.0268 467.1066,-1086.0268"/>
<text text-anchor="middle" x="452.138" y="-1113.3031" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">use</text>
</g>
<!-- A14&#45;&gt;A11 -->
<g id="edge17" class="edge">
<title>A14&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M387.4309,-1125.5329C364.9447,-989.8666 335.5291,-812.3923 316.9437,-700.2604"/>
<polygon fill="none" stroke="#000000" points="384.0176,-1126.3448 389.1057,-1135.6378 390.9234,-1125.2001 384.0176,-1126.3448"/>
</g>
<!-- A15&#45;&gt;A14 -->
<g id="edge16" class="edge">
<title>A15&#45;&gt;A14</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M377.1741,-1613.8004C381.7377,-1581.3079 387.6971,-1538.8764 393.681,-1496.2713"/>
<polygon fill="none" stroke="#000000" points="373.6682,-1613.5986 375.7432,-1623.9883 380.6001,-1614.5723 373.6682,-1613.5986"/>
</g>
<!-- A16 -->
<g id="node17" class="node">
<title>A16</title>
<polygon fill="none" stroke="#000000" points="433,-1766 433,-1798 508,-1798 508,-1766 433,-1766"/>
<text text-anchor="start" x="452.7175" y="-1779" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
<polygon fill="none" stroke="#000000" points="433,-1614 433,-1766 508,-1766 508,-1614 433,-1614"/>
<text text-anchor="start" x="462.1615" y="-1747" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
<text text-anchor="start" x="442.99" y="-1723" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
<text text-anchor="start" x="444.105" y="-1711" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
<text text-anchor="start" x="454.1085" y="-1699" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout</text>
<text text-anchor="start" x="444.3895" y="-1687" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">max_retires</text>
<text text-anchor="start" x="452.442" y="-1675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
<text text-anchor="start" x="464.3915" y="-1663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
<text text-anchor="start" x="451.0535" y="-1651" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
<text text-anchor="start" x="449.379" y="-1639" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
<text text-anchor="start" x="463.8365" y="-1627" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
<polygon fill="none" stroke="#000000" points="433,-1546 433,-1614 508,-1614 508,-1546 433,-1546"/>
<text text-anchor="start" x="444.39" y="-1595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
<text text-anchor="start" x="447.724" y="-1583" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
<text text-anchor="start" x="445.224" y="-1571" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
<text text-anchor="start" x="455.5025" y="-1559" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A16&#45;&gt;A14 -->
<g id="edge18" class="edge">
<title>A16&#45;&gt;A14</title>
<path fill="none" stroke="#000000" d="M450.5227,-1536.041C448.6468,-1522.9463 446.7248,-1509.5297 444.8004,-1496.0971"/>
<polygon fill="#000000" stroke="#000000" points="451.9475,-1545.9867 446.0748,-1536.726 451.2384,-1541.0373 450.5293,-1536.0878 450.5293,-1536.0878 450.5293,-1536.0878 451.2384,-1541.0373 454.9839,-1535.4496 451.9475,-1545.9867 451.9475,-1545.9867"/>
<text text-anchor="middle" x="455.7379" y="-1509.8414" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
<text text-anchor="middle" x="441.0101" y="-1526.2424" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

43
app/docu/proxy_3.yuml Normal file
View File

@@ -0,0 +1,43 @@
// {type:class}
// {direction:topDown}
// {generate:true}
[note: Example of instantiation for a GEN3PLUS inverter!{bg:cornsilk}]
[<<AbstractIterMeta>>||__iter__()]
[InverterBase|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()]
[InverterBase]^[InverterG3P|forward_at_cmd_resp;]
[InverterBase]++->[local:StreamPtr]
[InverterBase]++->[remote:StreamPtr]
[<<AsyncIfc>>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()]
[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb]
[AsyncStream|reader;writer;addr;r_addr;l_addr|;<async>loop;disc();close();healthy();;__async_read();__async_write();__async_forward()]
[AsyncStreamServer|create_remote|<async>server_loop();<async>_async_forward();<async>publish_outstanding_mqtt();close()]
[AsyncStreamClient||<async>client_loop();<async>_async_forward())]
[<<AsyncIfc>>]^-.-[AsyncIfcImpl]
[AsyncIfcImpl]^[AsyncStream]
[AsyncStream]^[AsyncStreamServer]
[AsyncStream]^[AsyncStreamClient]
[SolarmanV5|conn_no;addr;inverter:InverterG3P;control;serial;snr;db:InfosG3P;switch|msg_unknown();;healthy();close()]
[SolarmanV5]<-++[local:StreamPtr]
[local:StreamPtr]++->[AsyncStreamServer]
[SolarmanV5]<-0..1[remote:StreamPtr]
[remote:StreamPtr]0..1->[AsyncStreamClient]
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]
[Infos]^[InfosG3P|client_mode:bool|ha_confs();parse();calc();build()]
[SolarmanV5]->[InfosG3P]
[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd();<async> end_modbus_cmd();close();inc_counter();dec_counter()]
[Message]use->[<<AsyncIfc>>]
[<<ProtocolIfc>>|_registry|close()]
[<<AbstractIterMeta>>]^-.-[<<ProtocolIfc>>]
[<<ProtocolIfc>>]^-.-[Message]
[Message]^[SolarmanV5]
[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
[Modbus]<0..1-has[Message]

View File

@@ -2,9 +2,12 @@
set -e
user="$(id -u)"
export VERSION=$(cat /proxy-version.txt)
echo "######################################################"
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
echo "# for running with UserID:$UID, GroupID:$GID"
echo "# Image built: $(cat /build-date.txt) "
echo "#"
if [ "$user" = '0' ]; then
@@ -20,7 +23,7 @@ if [ "$user" = '0' ]; then
echo "######################################################"
echo "#"
exec su-exec $SERVICE_NAME "$@"
exec su-exec $SERVICE_NAME "$@" -tr './translations/'
else
exec "$@"
fi

View File

@@ -17,6 +17,5 @@ if [ "$environment" = "production" ] ; then \
-name od -o \
-name strings -o \
-name su -o \
-name wget -o \
\) -delete \
; fi

View File

@@ -1,384 +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="691pt" height="1312pt"
viewBox="0.00 0.00 691.35 1312.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1308)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1308 687.348,-1308 687.348,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="108.5444,-1208 .1516,-1208 .1516,-1172 114.5444,-1172 114.5444,-1202 108.5444,-1208"/>
<polyline fill="none" stroke="#000000" points="108.5444,-1208 108.5444,-1202 "/>
<polyline fill="none" stroke="#000000" points="114.5444,-1202 108.5444,-1202 "/>
<text text-anchor="middle" x="57.348" y="-1193" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">You can stick notes</text>
<text text-anchor="middle" x="57.348" y="-1181" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">on diagrams too!</text>
</g>
<!-- A1 -->
<g id="node2" class="node">
<title>A1</title>
<polygon fill="none" stroke="#000000" points="657.0297,-906 587.6663,-906 587.6663,-870 657.0297,-870 657.0297,-906"/>
<text text-anchor="middle" x="622.348" y="-885" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Singleton</text>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="561.348,-608 561.348,-640 683.348,-640 683.348,-608 561.348,-608"/>
<text text-anchor="start" x="612.625" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Mqtt</text>
<polygon fill="none" stroke="#000000" points="561.348,-552 561.348,-608 683.348,-608 683.348,-552 561.348,-552"/>
<text text-anchor="start" x="579.8355" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;ha_restarts</text>
<text text-anchor="start" x="587.6145" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__client</text>
<text text-anchor="start" x="571.2215" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;static&gt;__cb_MqttIsUp</text>
<polygon fill="none" stroke="#000000" points="561.348,-508 561.348,-552 683.348,-552 683.348,-508 561.348,-508"/>
<text text-anchor="start" x="584.284" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;publish()</text>
<text text-anchor="start" x="588.4525" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;close()</text>
</g>
<!-- A1&#45;&gt;A2 -->
<g id="edge1" class="edge">
<title>A1&#45;&gt;A2</title>
<path fill="none" stroke="#000000" d="M622.348,-859.5395C622.348,-810.311 622.348,-708.0351 622.348,-640.2069"/>
<polygon fill="none" stroke="#000000" points="618.8481,-859.7608 622.348,-869.7608 625.8481,-859.7608 618.8481,-859.7608"/>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="none" stroke="#000000" points="568.348,-324 568.348,-356 676.348,-356 676.348,-324 568.348,-324"/>
<text text-anchor="start" x="605.4015" y="-337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Inverter</text>
<polygon fill="none" stroke="#000000" points="568.348,-232 568.348,-324 676.348,-324 676.348,-232 568.348,-232"/>
<text text-anchor="start" x="598.452" y="-305" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.db_stat</text>
<text text-anchor="start" x="591.7885" y="-293" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.entity_prfx</text>
<text text-anchor="start" x="582.6235" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.discovery_prfx</text>
<text text-anchor="start" x="582.0595" y="-269" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_node_id</text>
<text text-anchor="start" x="578.1705" y="-257" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.proxy_unique_id</text>
<text text-anchor="start" x="594.0135" y="-245" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">cls.mqtt:Mqtt</text>
<polygon fill="none" stroke="#000000" points="568.348,-212 568.348,-232 676.348,-232 676.348,-212 568.348,-212"/>
</g>
<!-- A2&#45;&gt;A11 -->
<g id="edge13" class="edge">
<title>A2&#45;&gt;A11</title>
<path fill="none" stroke="#000000" d="M622.348,-507.8316C622.348,-462.6124 622.348,-402.6972 622.348,-356.2361"/>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="257.348,-366 257.348,-398 364.348,-398 364.348,-366 257.348,-366"/>
<text text-anchor="start" x="293.0655" y="-379" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Modbus</text>
<polygon fill="none" stroke="#000000" points="257.348,-226 257.348,-366 364.348,-366 364.348,-226 257.348,-226"/>
<text text-anchor="start" x="302.5095" y="-347" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">que</text>
<text text-anchor="start" x="283.338" y="-323" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snd_handler</text>
<text text-anchor="start" x="284.453" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">rsp_handler</text>
<text text-anchor="start" x="266.9565" y="-299" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">timeout:max_retires</text>
<text text-anchor="start" x="292.79" y="-287" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">last_xxx</text>
<text text-anchor="start" x="304.7395" y="-275" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">err</text>
<text text-anchor="start" x="291.4015" y="-263" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">retry_cnt</text>
<text text-anchor="start" x="289.727" y="-251" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">req_pend</text>
<text text-anchor="start" x="304.1845" y="-239" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">tim</text>
<polygon fill="none" stroke="#000000" points="257.348,-170 257.348,-226 364.348,-226 364.348,-170 257.348,-170"/>
<text text-anchor="start" x="284.738" y="-207" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">build_msg()</text>
<text text-anchor="start" x="288.072" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_req()</text>
<text text-anchor="start" x="285.572" y="-183" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">recv_resp()</text>
</g>
<!-- A4 -->
<g id="node5" class="node">
<title>A4</title>
<polygon fill="none" stroke="#000000" points="263.348,-1200 263.348,-1232 334.348,-1232 334.348,-1200 263.348,-1200"/>
<text text-anchor="start" x="273.293" y="-1213" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">IterRegistry</text>
<polygon fill="none" stroke="#000000" points="263.348,-1180 263.348,-1200 334.348,-1200 334.348,-1180 263.348,-1180"/>
<polygon fill="none" stroke="#000000" points="263.348,-1148 263.348,-1180 334.348,-1180 334.348,-1148 263.348,-1148"/>
<text text-anchor="start" x="280.787" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__iter__</text>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="231.348,-994 231.348,-1026 365.348,-1026 365.348,-994 231.348,-994"/>
<text text-anchor="start" x="278.0655" y="-1007" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Message</text>
<polygon fill="none" stroke="#000000" points="231.348,-818 231.348,-994 365.348,-994 365.348,-818 231.348,-818"/>
<text text-anchor="start" x="261.6745" y="-975" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">server_side:bool</text>
<text text-anchor="start" x="258.891" y="-963" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_valid:bool</text>
<text text-anchor="start" x="251.662" y="-951" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">header_len:unsigned</text>
<text text-anchor="start" x="257.496" y="-939" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">data_len:unsigned</text>
<text text-anchor="start" x="276.6725" y="-927" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">unique_id</text>
<text text-anchor="start" x="280.5615" y="-915" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">node_id</text>
<text text-anchor="start" x="277.5065" y="-903" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">sug_area</text>
<text text-anchor="start" x="248.337" y="-891" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_recv_buffer:bytearray</text>
<text text-anchor="start" x="246.9425" y="-879" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_send_buffer:bytearray</text>
<text text-anchor="start" x="241.1145" y="-867" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_forward_buffer:bytearray</text>
<text text-anchor="start" x="280.5615" y="-855" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:Infos</text>
<text text-anchor="start" x="269.174" y="-843" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_data:list</text>
<text text-anchor="start" x="287.51" y="-831" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">state</text>
<polygon fill="none" stroke="#000000" points="231.348,-750 231.348,-818 365.348,-818 365.348,-750 231.348,-750"/>
<text text-anchor="start" x="248.0575" y="-799" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">_read():void&lt;abstract&gt;</text>
<text text-anchor="start" x="272.7925" y="-787" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close():void</text>
<text text-anchor="start" x="258.6205" y="-775" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter():void</text>
<text text-anchor="start" x="256.9505" y="-763" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter():void</text>
</g>
<!-- A4&#45;&gt;A5 -->
<g id="edge2" class="edge">
<title>A4&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M298.348,-1137.5879C298.348,-1106.6429 298.348,-1065.8843 298.348,-1026.2983"/>
<polygon fill="none" stroke="#000000" points="294.8481,-1137.6902 298.348,-1147.6902 301.8481,-1137.6902 294.8481,-1137.6902"/>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="370.348,-668 370.348,-700 484.348,-700 484.348,-668 370.348,-668"/>
<text text-anchor="start" x="413.456" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Talent</text>
<polygon fill="none" stroke="#000000" points="370.348,-564 370.348,-668 484.348,-668 484.348,-564 370.348,-564"/>
<text text-anchor="start" x="380.111" y="-649" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">await_conn_resp_cnt</text>
<text text-anchor="start" x="415.1255" y="-637" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">id_str</text>
<text text-anchor="start" x="395.948" y="-625" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_name</text>
<text text-anchor="start" x="399.288" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">contact_mail</text>
<text text-anchor="start" x="402.8925" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3</text>
<text text-anchor="start" x="401.232" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
<text text-anchor="start" x="413.46" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="370.348,-448 370.348,-564 484.348,-564 484.348,-448 370.348,-448"/>
<text text-anchor="start" x="384.8405" y="-545" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_contact_info()</text>
<text text-anchor="start" x="386.7805" y="-533" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_ota_update()</text>
<text text-anchor="start" x="392.6245" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_get_time()</text>
<text text-anchor="start" x="380.6765" y="-509" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_collector_data()</text>
<text text-anchor="start" x="382.6215" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_inverter_data()</text>
<text text-anchor="start" x="391.7885" y="-485" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="412.3505" y="-461" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A5&#45;&gt;A6 -->
<g id="edge3" class="edge">
<title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M358.9294,-740.5383C364.4479,-727.1056 370.0049,-713.5794 375.4378,-700.355"/>
<polygon fill="none" stroke="#000000" points="355.6797,-739.2382 355.117,-749.8181 362.1546,-741.8983 355.6797,-739.2382"/>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="127.348,-632 127.348,-664 218.348,-664 218.348,-632 127.348,-632"/>
<text text-anchor="start" x="145.343" y="-645" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">SolarmanV5</text>
<polygon fill="none" stroke="#000000" points="127.348,-540 127.348,-632 218.348,-632 218.348,-540 127.348,-540"/>
<text text-anchor="start" x="157.846" y="-613" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">control</text>
<text text-anchor="start" x="160.9055" y="-601" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">serial</text>
<text text-anchor="start" x="165.904" y="-589" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">snr</text>
<text text-anchor="start" x="145.058" y="-577" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">db:InfosG3P</text>
<text text-anchor="start" x="146.732" y="-565" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">mb:Modbus</text>
<text text-anchor="start" x="158.96" y="-553" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">switch</text>
<polygon fill="none" stroke="#000000" points="127.348,-484 127.348,-540 218.348,-540 218.348,-484 127.348,-484"/>
<text text-anchor="start" x="137.2885" y="-521" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">msg_unknown()</text>
<text text-anchor="start" x="157.8505" y="-497" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A5&#45;&gt;A7 -->
<g id="edge4" class="edge">
<title>A5&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M239.076,-740.2903C228.6761,-714.3733 218.1403,-688.1174 208.6075,-664.3611"/>
<polygon fill="none" stroke="#000000" points="235.9268,-741.8409 242.8992,-749.8181 242.4233,-739.2339 235.9268,-741.8409"/>
</g>
<!-- A6&#45;&gt;A3 -->
<g id="edge6" class="edge">
<title>A6&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M376.3705,-447.6454C371.0187,-434.3805 365.5816,-420.9039 360.2423,-407.6696"/>
<polygon fill="#000000" stroke="#000000" points="356.3743,-398.0824 364.289,-405.6724 358.2451,-402.7192 360.1158,-407.3561 360.1158,-407.3561 360.1158,-407.3561 358.2451,-402.7192 355.9427,-409.0397 356.3743,-398.0824 356.3743,-398.0824"/>
<text text-anchor="middle" x="370.9946" y="-408.7296" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
<text text-anchor="middle" x="361.7502" y="-430.9982" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="none" stroke="#000000" points="382.348,-300 382.348,-332 532.348,-332 532.348,-300 382.348,-300"/>
<text text-anchor="start" x="425.3935" y="-313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
<polygon fill="none" stroke="#000000" points="382.348,-268 382.348,-300 532.348,-300 532.348,-268 382.348,-268"/>
<text text-anchor="start" x="392.335" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
<polygon fill="none" stroke="#000000" points="382.348,-236 382.348,-268 532.348,-268 532.348,-236 382.348,-236"/>
<text text-anchor="start" x="442.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A6&#45;&gt;A8 -->
<g id="edge5" class="edge">
<title>A6&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M441.464,-437.5454C445.3714,-399.7739 449.3591,-361.2265 452.3615,-332.203"/>
<polygon fill="none" stroke="#000000" points="437.9668,-437.3383 440.4192,-447.6454 444.9297,-438.0587 437.9668,-437.3383"/>
</g>
<!-- A7&#45;&gt;A3 -->
<g id="edge8" class="edge">
<title>A7&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M210.935,-483.8952C216.3404,-471.7801 221.9084,-459.553 227.348,-448 235.1472,-431.4354 243.6196,-414.0579 252.0717,-397.0641"/>
<polygon fill="#000000" stroke="#000000" points="256.7608,-387.6701 256.3209,-398.6272 254.5277,-392.1437 252.2946,-396.6174 252.2946,-396.6174 252.2946,-396.6174 254.5277,-392.1437 248.2683,-394.6076 256.7608,-387.6701 256.7608,-387.6701"/>
<text text-anchor="middle" x="256.228" y="-404.663" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">1</text>
<text text-anchor="middle" x="210.6174" y="-460.8977" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="64.348,-300 64.348,-332 220.348,-332 220.348,-300 64.348,-300"/>
<text text-anchor="start" x="107.059" y="-313" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
<polygon fill="none" stroke="#000000" points="64.348,-268 64.348,-300 220.348,-300 220.348,-268 64.348,-268"/>
<text text-anchor="start" x="74.0005" y="-281" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
<polygon fill="none" stroke="#000000" points="64.348,-236 64.348,-268 220.348,-268 220.348,-236 64.348,-236"/>
<text text-anchor="start" x="127.3505" y="-249" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A7&#45;&gt;A9 -->
<g id="edge7" class="edge">
<title>A7&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M161.9757,-473.7349C157.0236,-425.8645 151.3255,-370.7828 147.349,-332.3431"/>
<polygon fill="none" stroke="#000000" points="158.5098,-474.2451 163.0203,-483.8319 165.4726,-473.5248 158.5098,-474.2451"/>
</g>
<!-- A8&#45;&gt;A8 -->
<g id="edge15" class="edge">
<title>A8&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M532.5164,-321.6908C543.1874,-315.5948 550.348,-303.0313 550.348,-284 550.348,-270.3213 546.6488,-259.9838 540.6058,-252.9875"/>
<polygon fill="#000000" stroke="#000000" points="532.5164,-246.3092 543.0929,-249.2054 536.3722,-249.4924 540.228,-252.6756 540.228,-252.6756 540.228,-252.6756 536.3722,-249.4924 537.3632,-256.1459 532.5164,-246.3092 532.5164,-246.3092"/>
<text text-anchor="middle" x="551.8757" y="-248.3308" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
<text text-anchor="middle" x="543.0584" y="-301.6947" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A12 -->
<g id="node13" class="node">
<title>A12</title>
<polygon fill="none" stroke="#000000" points="478.348,-88 478.348,-120 600.348,-120 600.348,-88 478.348,-88"/>
<text text-anchor="start" x="515.7325" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3</text>
<polygon fill="none" stroke="#000000" points="478.348,-56 478.348,-88 600.348,-88 600.348,-56 478.348,-56"/>
<text text-anchor="start" x="508.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<polygon fill="none" stroke="#000000" points="478.348,0 478.348,-56 600.348,-56 600.348,0 478.348,0"/>
<text text-anchor="start" x="487.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
<text text-anchor="start" x="524.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A8&#45;&gt;A12 -->
<g id="edge14" class="edge">
<title>A8&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M478.5265,-226.1465C490.409,-193.6871 505.2165,-153.2373 517.2458,-120.3767"/>
<polygon fill="none" stroke="#000000" points="475.09,-225.3526 474.9391,-235.9464 481.6634,-227.759 475.09,-225.3526"/>
</g>
<!-- A9&#45;&gt;A9 -->
<g id="edge17" class="edge">
<title>A9&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M220.6951,-321.2601C231.2923,-315.0138 238.348,-302.5938 238.348,-284 238.348,-270.6357 234.703,-260.4608 228.7179,-253.4753"/>
<polygon fill="#000000" stroke="#000000" points="220.6951,-246.7399 231.2473,-249.7232 224.5245,-249.9548 228.3539,-253.1697 228.3539,-253.1697 228.3539,-253.1697 224.5245,-249.9548 225.4605,-256.6162 220.6951,-246.7399 220.6951,-246.7399"/>
<text text-anchor="middle" x="240.0123" y="-248.9211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">0..1</text>
<text text-anchor="middle" x="231.039" y="-301.1428" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">has</text>
</g>
<!-- A13 -->
<g id="node14" class="node">
<title>A13</title>
<polygon fill="none" stroke="#000000" points="251.348,-88 251.348,-120 373.348,-120 373.348,-88 251.348,-88"/>
<text text-anchor="start" x="285.398" y="-101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InverterG3P</text>
<polygon fill="none" stroke="#000000" points="251.348,-56 251.348,-88 373.348,-88 373.348,-56 251.348,-56"/>
<text text-anchor="start" x="281.7835" y="-69" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__ha_restarts</text>
<polygon fill="none" stroke="#000000" points="251.348,0 251.348,-56 373.348,-56 373.348,0 251.348,0"/>
<text text-anchor="start" x="260.9515" y="-37" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_create_remote()</text>
<text text-anchor="start" x="297.3505" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
</g>
<!-- A9&#45;&gt;A13 -->
<g id="edge16" class="edge">
<title>A9&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M184.9859,-227.8183C209.8288,-195.0842 241.1576,-153.8039 266.5264,-120.3767"/>
<polygon fill="none" stroke="#000000" points="182.0747,-225.8647 178.8173,-235.9464 187.6507,-230.0965 182.0747,-225.8647"/>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="none" stroke="#000000" points="236.348,-662 236.348,-694 352.348,-694 352.348,-662 236.348,-662"/>
<text text-anchor="start" x="264.622" y="-675" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">AsyncStream</text>
<polygon fill="none" stroke="#000000" points="236.348,-582 236.348,-662 352.348,-662 352.348,-582 236.348,-582"/>
<text text-anchor="start" x="279.901" y="-643" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">reader</text>
<text text-anchor="start" x="282.131" y="-631" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">writer</text>
<text text-anchor="start" x="284.345" y="-619" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">addr</text>
<text text-anchor="start" x="279.901" y="-607" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">r_addr</text>
<text text-anchor="start" x="280.456" y="-595" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">l_addr</text>
<polygon fill="none" stroke="#000000" points="236.348,-454 236.348,-582 352.348,-582 352.348,-454 236.348,-454"/>
<text text-anchor="start" x="246.0055" y="-563" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;server_loop()</text>
<text text-anchor="start" x="248.226" y="-551" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;client_loop()</text>
<text text-anchor="start" x="266.002" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;async&gt;loop</text>
<text text-anchor="start" x="282.13" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">disc()</text>
<text text-anchor="start" x="279.3505" y="-515" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
<text text-anchor="start" x="259.6185" y="-491" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_read()</text>
<text text-anchor="start" x="264.628" y="-479" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">async_write()</text>
<text text-anchor="start" x="252.955" y="-467" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">__async_forward()</text>
</g>
<!-- A10&#45;&gt;A8 -->
<g id="edge9" class="edge">
<title>A10&#45;&gt;A8</title>
<path fill="none" stroke="#000000" d="M357.3002,-455.2837C358.6569,-452.8318 360.0073,-450.4016 361.348,-448 383.2991,-408.6787 409.1348,-364.6637 428.4716,-332.1398"/>
<polygon fill="none" stroke="#000000" points="354.1241,-453.7956 352.3659,-464.2436 360.2557,-457.1724 354.1241,-453.7956"/>
</g>
<!-- A10&#45;&gt;A9 -->
<g id="edge10" class="edge">
<title>A10&#45;&gt;A9</title>
<path fill="none" stroke="#000000" d="M231.478,-454.0506C209.0706,-411.2997 185.0929,-365.5527 167.6574,-332.2876"/>
<polygon fill="none" stroke="#000000" points="228.4903,-455.8898 236.2327,-463.1221 234.6903,-452.6401 228.4903,-455.8898"/>
</g>
<!-- A11&#45;&gt;A12 -->
<g id="edge11" class="edge">
<title>A11&#45;&gt;A12</title>
<path fill="none" stroke="#000000" d="M592.1173,-202.4136C582.0634,-175.28 571.0546,-145.5697 561.7056,-120.3387"/>
<polygon fill="none" stroke="#000000" points="588.8729,-203.7311 595.6294,-211.892 595.4368,-201.2989 588.8729,-203.7311"/>
</g>
<!-- A11&#45;&gt;A13 -->
<g id="edge12" class="edge">
<title>A11&#45;&gt;A13</title>
<path fill="none" stroke="#000000" d="M586.2753,-202.8933C578.5712,-190.8884 569.6045,-179.4114 559.348,-170 530.8998,-143.8959 437.024,-105.9199 373.5518,-82.1078"/>
<polygon fill="none" stroke="#000000" points="583.4606,-204.9994 591.6624,-211.7061 589.4331,-201.3485 583.4606,-204.9994"/>
</g>
<!-- A14 -->
<g id="node15" class="node">
<title>A14</title>
<polygon fill="none" stroke="#000000" points="133.348,-1272 133.348,-1304 236.348,-1304 236.348,-1272 133.348,-1272"/>
<text text-anchor="start" x="174.01" y="-1285" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Infos</text>
<polygon fill="none" stroke="#000000" points="133.348,-1216 133.348,-1272 236.348,-1272 236.348,-1216 133.348,-1216"/>
<text text-anchor="start" x="176.7895" y="-1253" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">stat</text>
<text text-anchor="start" x="152.334" y="-1241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">new_stat_data</text>
<text text-anchor="start" x="165.9515" y="-1229" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">info_dev</text>
<polygon fill="none" stroke="#000000" points="133.348,-1076 133.348,-1216 236.348,-1216 236.348,-1076 133.348,-1076"/>
<text text-anchor="start" x="160.6835" y="-1197" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">static_init()</text>
<text text-anchor="start" x="158.7325" y="-1185" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dev_value()</text>
<text text-anchor="start" x="155.6785" y="-1173" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">inc_counter()</text>
<text text-anchor="start" x="154.0085" y="-1161" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">dec_counter()</text>
<text text-anchor="start" x="152.058" y="-1149" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_proxy_conf</text>
<text text-anchor="start" x="167.061" y="-1137" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_conf</text>
<text text-anchor="start" x="161.2225" y="-1125" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">update_db</text>
<text text-anchor="start" x="145.385" y="-1113" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">set_db_def_value</text>
<text text-anchor="start" x="154.8335" y="-1101" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">get_db_value</text>
<text text-anchor="start" x="143.1705" y="-1089" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ignore_this_device</text>
</g>
<!-- A15 -->
<g id="node16" class="node">
<title>A15</title>
<polygon fill="none" stroke="#000000" points="386.348,-904 386.348,-936 453.348,-936 453.348,-904 386.348,-904"/>
<text text-anchor="start" x="402.341" y="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3</text>
<polygon fill="none" stroke="#000000" points="386.348,-884 386.348,-904 453.348,-904 453.348,-884 386.348,-884"/>
<polygon fill="none" stroke="#000000" points="386.348,-840 386.348,-884 453.348,-884 453.348,-840 386.348,-840"/>
<text text-anchor="start" x="396.232" y="-865" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="404.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
</g>
<!-- A14&#45;&gt;A15 -->
<g id="edge18" class="edge">
<title>A14&#45;&gt;A15</title>
<path fill="none" stroke="#000000" d="M242.8857,-1086.9876C246.5464,-1083.0913 250.3682,-1079.4032 254.348,-1076 298.2601,-1038.4501 335.1504,-1068.4478 374.348,-1026 397.0004,-1001.4693 408.2589,-965.3633 413.8498,-936.2357"/>
<polygon fill="none" stroke="#000000" points="240.0515,-1084.9088 236.0452,-1094.717 245.2936,-1089.548 240.0515,-1084.9088"/>
</g>
<!-- A16 -->
<g id="node17" class="node">
<title>A16</title>
<polygon fill="none" stroke="#000000" points="142.348,-904 142.348,-936 209.348,-936 209.348,-904 142.348,-904"/>
<text text-anchor="start" x="155.0065" y="-917" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">InfosG3P</text>
<polygon fill="none" stroke="#000000" points="142.348,-884 142.348,-904 209.348,-904 209.348,-884 142.348,-884"/>
<polygon fill="none" stroke="#000000" points="142.348,-840 142.348,-884 209.348,-884 209.348,-840 142.348,-840"/>
<text text-anchor="start" x="152.232" y="-865" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ha_confs()</text>
<text text-anchor="start" x="160.016" y="-853" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">parse()</text>
</g>
<!-- A14&#45;&gt;A16 -->
<g id="edge19" class="edge">
<title>A14&#45;&gt;A16</title>
<path fill="none" stroke="#000000" d="M180.6399,-1065.5724C179.2846,-1020.0932 177.8303,-971.2935 176.7899,-936.3828"/>
<polygon fill="none" stroke="#000000" points="177.1491,-1065.9355 180.9455,-1075.8267 184.146,-1065.7269 177.1491,-1065.9355"/>
</g>
<!-- A15&#45;&gt;A6 -->
<g id="edge21" class="edge">
<title>A15&#45;&gt;A6</title>
<path fill="none" stroke="#000000" d="M420.5717,-839.9684C421.4566,-805.2366 422.6992,-756.4655 423.879,-710.1572"/>
<polygon fill="#000000" stroke="#000000" points="424.1376,-700.0098 428.3813,-710.1212 424.0102,-705.0082 423.8828,-710.0066 423.8828,-710.0066 423.8828,-710.0066 424.0102,-705.0082 419.3842,-709.8919 424.1376,-700.0098 424.1376,-700.0098"/>
</g>
<!-- A16&#45;&gt;A7 -->
<g id="edge20" class="edge">
<title>A16&#45;&gt;A7</title>
<path fill="none" stroke="#000000" d="M174.8891,-839.9684C174.4696,-796.0581 173.8357,-729.7079 173.3059,-674.2644"/>
<polygon fill="#000000" stroke="#000000" points="173.2083,-664.0467 177.8037,-674.0032 173.2561,-669.0465 173.304,-674.0463 173.304,-674.0463 173.304,-674.0463 173.2561,-669.0465 168.8042,-674.0893 173.2083,-664.0467 173.2083,-664.0467"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,28 +0,0 @@
// {type:class}
// {direction:topDown}
// {generate:true}
[note: You can stick notes on diagrams too!{bg:cornsilk}]
[Singleton]^[Mqtt|<static>ha_restarts;<static>__client;<static>__cb_MqttIsUp|<async>publish();<async>close()]
[Modbus|que;;snd_handler;rsp_handler;timeout:max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp()]
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list;state|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
[Talent]has-1>[Modbus]
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
[SolarmanV5]has-1>[Modbus]
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
[AsyncStream]^[ConnectionG3P]
[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
[Mqtt]-[Inverter]
[ConnectionG3]^[InverterG3]
[ConnectionG3]has-0..1>[ConnectionG3]
[ConnectionG3P]^[InverterG3P]
[ConnectionG3P]has-0..1>[ConnectionG3P]
[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;update_db;set_db_def_value;get_db_value;ignore_this_device]^[InfosG3||ha_confs();parse()]
[Infos]^[InfosG3P||ha_confs();parse()]
[InfosG3P]->[SolarmanV5]
[InfosG3]->[Talent]

View File

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

View File

@@ -1,3 +1,5 @@
aiomqtt==2.0.1
schema==0.7.5
aiocron==1.8
aiomqtt==2.4.0
schema==0.7.7
aiocron==2.1
quart==0.20
quart-babel==1.0.7

3
app/src/.babel.cfg Normal file
View File

@@ -0,0 +1,3 @@
[python: **.py]
[jinja2: web/templates/**.html]
[jinja2: web/templates/**.html.j2]

104
app/src/async_ifc.py Normal file
View File

@@ -0,0 +1,104 @@
from abc import ABC, abstractmethod
class AsyncIfc(ABC):
@abstractmethod
def get_conn_no(self):
pass # pragma: no cover
@abstractmethod
def set_node_id(self, value: str):
pass # pragma: no cover
#
# TX - QUEUE
#
@abstractmethod
def tx_add(self, data: bytearray):
''' add data to transmit queue'''
pass # pragma: no cover
@abstractmethod
def tx_flush(self):
''' send transmit queue and clears it'''
pass # pragma: no cover
@abstractmethod
def tx_peek(self, size: int = None) -> bytearray:
'''returns size numbers of byte without removing them'''
pass # pragma: no cover
@abstractmethod
def tx_log(self, level, info):
''' log the transmit queue'''
pass # pragma: no cover
@abstractmethod
def tx_clear(self):
''' clear transmit queue'''
pass # pragma: no cover
@abstractmethod
def tx_len(self):
''' get numner of bytes in the transmit queue'''
pass # pragma: no cover
#
# FORWARD - QUEUE
#
@abstractmethod
def fwd_add(self, data: bytearray):
''' add data to forward queue'''
pass # pragma: no cover
@abstractmethod
def fwd_log(self, level, info):
''' log the forward queue'''
pass # pragma: no cover
#
# RX - QUEUE
#
@abstractmethod
def rx_get(self, size: int = None) -> bytearray:
'''removes size numbers of bytes and return them'''
pass # pragma: no cover
@abstractmethod
def rx_peek(self, size: int = None) -> bytearray:
'''returns size numbers of byte without removing them'''
pass # pragma: no cover
@abstractmethod
def rx_log(self, level, info):
''' logs the receive queue'''
pass # pragma: no cover
@abstractmethod
def rx_clear(self):
''' clear receive queue'''
pass # pragma: no cover
@abstractmethod
def rx_len(self):
''' get numner of bytes in the receive queue'''
pass # pragma: no cover
@abstractmethod
def rx_set_cb(self, callback):
pass # pragma: no cover
#
# Protocol Callbacks
#
@abstractmethod
def prot_set_timeout_cb(self, callback):
pass # pragma: no cover
@abstractmethod
def prot_set_init_new_client_conn_cb(self, callback):
pass # pragma: no cover
@abstractmethod
def prot_set_update_header_cb(self, callback):
pass # pragma: no cover

View File

@@ -1,141 +1,409 @@
import asyncio
import logging
import traceback
from messages import hex_dump_memory
import time
from asyncio import StreamReader, StreamWriter
from typing import Self
from itertools import count
from proxy import Proxy
from byte_fifo import ByteFifo
from async_ifc import AsyncIfc
from infos import Infos
import gc
logger = logging.getLogger('conn')
class AsyncStream():
class AsyncIfcImpl(AsyncIfc):
_ids = count(0)
def __init__(self) -> None:
logger.debug('AsyncIfcImpl.__init__')
self.fwd_fifo = ByteFifo()
self.tx_fifo = ByteFifo()
self.rx_fifo = ByteFifo()
self.conn_no = next(self._ids)
self.node_id = ''
self.timeout_cb = None
self.init_new_client_conn_cb = None
self.update_header_cb = None
def close(self):
self.timeout_cb = None
self.fwd_fifo.reg_trigger(None)
self.tx_fifo.reg_trigger(None)
self.rx_fifo.reg_trigger(None)
def set_node_id(self, value: str):
self.node_id = value
def get_conn_no(self):
return self.conn_no
def tx_add(self, data: bytearray):
''' add data to transmit queue'''
self.tx_fifo += data
def tx_flush(self):
''' send transmit queue and clears it'''
self.tx_fifo()
def tx_peek(self, size: int = None) -> bytearray:
'''returns size numbers of byte without removing them'''
return self.tx_fifo.peek(size)
def tx_log(self, level, info):
''' log the transmit queue'''
self.tx_fifo.logging(level, info)
def tx_clear(self):
''' clear transmit queue'''
self.tx_fifo.clear()
def tx_len(self):
''' get numner of bytes in the transmit queue'''
return len(self.tx_fifo)
def fwd_add(self, data: bytearray):
''' add data to forward queue'''
self.fwd_fifo += data
def fwd_log(self, level, info):
''' log the forward queue'''
self.fwd_fifo.logging(level, info)
def rx_get(self, size: int = None) -> bytearray:
'''removes size numbers of bytes and return them'''
return self.rx_fifo.get(size)
def rx_peek(self, size: int = None) -> bytearray:
'''returns size numbers of byte without removing them'''
return self.rx_fifo.peek(size)
def rx_log(self, level, info):
''' logs the receive queue'''
self.rx_fifo.logging(level, info)
def rx_clear(self):
''' clear receive queue'''
self.rx_fifo.clear()
def rx_len(self):
''' get numner of bytes in the receive queue'''
return len(self.rx_fifo)
def rx_set_cb(self, callback):
self.rx_fifo.reg_trigger(callback)
def prot_set_timeout_cb(self, callback):
self.timeout_cb = callback
def prot_set_init_new_client_conn_cb(self, callback):
self.init_new_client_conn_cb = callback
def prot_set_update_header_cb(self, callback):
self.update_header_cb = callback
class StreamPtr():
'''Descr StreamPtr'''
def __init__(self, _stream, _ifc=None):
self.stream = _stream
self.ifc = _ifc
@property
def ifc(self):
return self._ifc
@ifc.setter
def ifc(self, value):
self._ifc = value
@property
def stream(self):
return self._stream
@stream.setter
def stream(self, value):
self._stream = value
class AsyncStream(AsyncIfcImpl):
MAX_PROC_TIME = 2
'''maximum processing time for a received msg in sec'''
MAX_START_TIME = 400
'''maximum time without a received msg in sec'''
MAX_INV_IDLE_TIME = 120
'''maximum time without a received msg from the inverter in sec'''
MAX_DEF_IDLE_TIME = 360
'''maximum default time without a received msg in sec'''
def __init__(self, reader: StreamReader, writer: StreamWriter,
rstream: "StreamPtr") -> None:
AsyncIfcImpl.__init__(self)
def __init__(self, reader, writer, addr) -> None:
logger.debug('AsyncStream.__init__')
self.reader = reader
self.writer = writer
self.addr = addr
self.r_addr = ''
self.l_addr = ''
async def server_loop(self, addr):
'''Loop for receiving messages from the inverter (server-side)'''
logging.info(f'[{self.node_id}] Accept connection from {addr}')
self.inc_counter('Inverter_Cnt')
await self.loop()
self.dec_counter('Inverter_Cnt')
logging.info(f'[{self.node_id}] Server loop stopped for'
f' r{self.r_addr}')
self.remote = rstream
self.tx_fifo.reg_trigger(self.__write_cb)
self._reader = reader
self._writer = writer
self.r_addr = writer.get_extra_info('peername')
self.l_addr = writer.get_extra_info('sockname')
self.proc_start = None # start processing start timestamp
self.proc_max = 0
self.async_publ_mqtt = None # will be set AsyncStreamServer only
# if the server connection closes, we also have to disconnect
# the connection to te TSUN cloud
if self.remoteStream:
logging.debug("disconnect client connection")
await self.remoteStream.disc()
try:
await self._async_publ_mqtt_proxy_stat('proxy')
except Exception:
pass
def __write_cb(self):
self._writer.write(self.tx_fifo.get())
async def client_loop(self, addr):
'''Loop for receiving messages from the TSUN cloud (client-side)'''
clientStream = await self.remoteStream.loop()
logging.info(f'[{self.node_id}] Client loop stopped for'
f' l{clientStream.l_addr}')
# if the client connection closes, we don't touch the server
# connection. Instead we erase the client connection stream,
# thus on the next received packet from the inverter, we can
# establish a new connection to the TSUN cloud
# erase backlink to inverter
clientStream.remoteStream = None
if self.remoteStream == clientStream:
# logging.debug(f'Client l{clientStream.l_addr} refs:'
# f' {gc.get_referrers(clientStream)}')
# than erase client connection
self.remoteStream = None
async def loop(self):
self.r_addr = self.writer.get_extra_info('peername')
self.l_addr = self.writer.get_extra_info('sockname')
def __timeout(self) -> int:
if self.timeout_cb:
return self.timeout_cb()
return 360
async def loop(self) -> Self:
"""Async loop handler for precessing all received messages"""
self.proc_start = time.time()
while True:
try:
await self.__async_read()
self.__calc_proc_time()
dead_conn_to = self.__timeout()
await asyncio.wait_for(self.__async_read(),
dead_conn_to)
if self.unique_id:
await self.async_write()
await self.__async_forward()
await self.__async_write()
await self.__async_forward()
if self.async_publ_mqtt:
await self.async_publ_mqtt()
except asyncio.TimeoutError:
logger.warning(f'[{self.node_id}:{self.conn_no}] Dead '
f'connection timeout ({dead_conn_to}s) '
f'for {self.l_addr}')
await self.disc()
return self
except OSError as error:
logger.error(f'[{self.node_id}] {error} for l{self.l_addr} | '
logger.error(f'[{self.node_id}:{self.conn_no}] '
f'{error} for l{self.l_addr} | '
f'r{self.r_addr}')
await self.disc()
self.close()
return self
except RuntimeError as error:
logger.info(f"[{self.node_id}] {error} for {self.l_addr}")
logger.info(f'[{self.node_id}:{self.conn_no}] '
f'{error} for {self.l_addr}')
await self.disc()
self.close()
return self
except Exception:
self.inc_counter('SW_Exception')
Infos.inc_counter('SW_Exception')
logger.error(
f"Exception for {self.addr}:\n"
f"Exception for {self.r_addr}:\n"
f"{traceback.format_exc()}")
await asyncio.sleep(0) # be cooperative to other task
def __calc_proc_time(self):
if self.proc_start:
proc = time.time() - self.proc_start
if proc > self.proc_max:
self.proc_max = proc
self.proc_start = None
async def disc(self) -> None:
if self.writer.is_closing():
"""Async disc handler for graceful disconnect"""
if self._writer.is_closing():
return
logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}')
self.writer.close()
await self.writer.wait_closed()
self._writer.close()
await self._writer.wait_closed()
def close(self):
if self.writer.is_closing():
def close(self) -> None:
logging.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
"""close handler for a no waiting disconnect
hint: must be called before releasing the connection instance
"""
super().close()
self._reader.feed_eof() # abort awaited read
if self._writer.is_closing():
return
logger.debug(f'AsyncStream.close() l{self.l_addr} | r{self.r_addr}')
self.writer.close()
self._writer.close()
def healthy(self) -> bool:
elapsed = 0
if self.proc_start is not None:
elapsed = time.time() - self.proc_start
if elapsed > self.MAX_PROC_TIME:
logging.debug(f'[{self.node_id}:{self.conn_no}:'
f'{type(self).__name__}]'
f' act:{round(1000*elapsed)}ms'
f' max:{round(1000*self.proc_max)}ms')
logging.debug(f'Healthy()) refs: {gc.get_referrers(self)}')
return elapsed < 5
'''
Our private methods
'''
async def __async_read(self) -> None:
data = await self.reader.read(4096)
"""Async read handler to read received data from TCP stream"""
data = await self._reader.read(4096)
if data:
self._recv_buffer += data
self.read() # call read in parent class
self.proc_start = time.time()
self.rx_fifo += data
wait = self.rx_fifo() # call read in parent class
if wait and wait > 0:
await asyncio.sleep(wait)
else:
raise RuntimeError("Peer closed.")
async def async_write(self, headline='Transmit to ') -> None:
if self._send_buffer:
hex_dump_memory(logging.INFO, f'{headline}{self.addr}:',
self._send_buffer, len(self._send_buffer))
self.writer.write(self._send_buffer)
await self.writer.drain()
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
async def __async_write(self, headline: str = 'Transmit to ') -> None:
"""Async write handler to transmit the send_buffer"""
if len(self.tx_fifo) > 0:
self.tx_fifo.logging(logging.INFO, f'{headline}{self.r_addr}:')
self._writer.write(self.tx_fifo.get())
await self._writer.drain()
async def __async_forward(self) -> None:
if self._forward_buffer:
if not self.remoteStream:
await self.async_create_remote()
if self.remoteStream:
if self.remoteStream._init_new_client_conn():
await self.remoteStream.async_write()
"""forward handler transmits data over the remote connection"""
if len(self.fwd_fifo) == 0:
return
try:
await self._async_forward()
if self.remoteStream:
self.remoteStream._update_header(self._forward_buffer)
hex_dump_memory(logging.INFO,
f'Forward to {self.remoteStream.addr}:',
self._forward_buffer,
len(self._forward_buffer))
self.remoteStream.writer.write(self._forward_buffer)
await self.remoteStream.writer.drain()
self._forward_buffer = bytearray(0)
except OSError as error:
if self.remote.stream:
rmt = self.remote
logger.error(f'[{rmt.stream.node_id}:{rmt.stream.conn_no}] '
f'Fwd: {error} for '
f'l{rmt.ifc.l_addr} | r{rmt.ifc.r_addr}')
await rmt.ifc.disc()
if rmt.ifc.close_cb:
rmt.ifc.close_cb()
def __del__(self):
logger.debug(
f"AsyncStream.__del__ l{self.l_addr} | r{self.r_addr}")
except RuntimeError as error:
if self.remote.stream:
rmt = self.remote
logger.info(f'[{rmt.stream.node_id}:{rmt.stream.conn_no}] '
f'Fwd: {error} for {rmt.ifc.l_addr}')
await rmt.ifc.disc()
if rmt.ifc.close_cb:
rmt.ifc.close_cb()
except Exception:
Infos.inc_counter('SW_Exception')
logger.error(
f"Fwd Exception for {self.r_addr}:\n"
f"{traceback.format_exc()}")
async def publish_outstanding_mqtt(self):
'''Publish all outstanding MQTT topics'''
try:
await self.async_publ_mqtt()
await Proxy._async_publ_mqtt_proxy_stat('proxy')
except Exception:
pass
class AsyncStreamServer(AsyncStream):
def __init__(self, reader: StreamReader, writer: StreamWriter,
async_publ_mqtt, create_remote,
rstream: "StreamPtr") -> None:
AsyncStream.__init__(self, reader, writer, rstream)
self.create_remote = create_remote
self.async_publ_mqtt = async_publ_mqtt
def close(self) -> None:
logging.debug('AsyncStreamServer.close()')
self.create_remote = None
self.async_publ_mqtt = None
super().close()
async def server_loop(self) -> None:
'''Loop for receiving messages from the inverter (server-side)'''
logger.info(f'[{self.node_id}:{self.conn_no}] '
f'Accept connection from {self.r_addr}')
Infos.inc_counter('Inverter_Cnt')
Infos.inc_counter('ServerMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
Infos.dec_counter('ServerMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
await self.publish_outstanding_mqtt()
logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for'
f' r{self.r_addr}')
# if the server connection closes, we also have to disconnect
# the connection to te TSUN cloud
if self.remote and self.remote.stream:
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
f'connection: [{self.remote.ifc.node_id}:'
f'{self.remote.ifc.conn_no}]')
await self.remote.ifc.disc()
async def _async_forward(self) -> None:
"""forward handler transmits data over the remote connection"""
if not self.remote.stream:
await self.create_remote()
if self.remote.stream and \
self.remote.ifc.init_new_client_conn_cb():
await self.remote.ifc._AsyncStream__async_write()
if self.remote.stream:
self.remote.ifc.update_header_cb(self.fwd_fifo.peek())
self.fwd_fifo.logging(logging.INFO, 'Forward to '
f'{self.remote.ifc.r_addr}:')
self.remote.ifc._writer.write(self.fwd_fifo.get())
await self.remote.ifc._writer.drain()
class AsyncStreamClient(AsyncStream):
def __init__(self, reader: StreamReader, writer: StreamWriter,
rstream: "StreamPtr", close_cb,
use_emu: bool = False) -> None:
AsyncStream.__init__(self, reader, writer, rstream)
self.close_cb = close_cb
self.emu_mode = use_emu
async def disc(self) -> None:
logging.debug('AsyncStreamClient.disc()')
self.remote = None
await super().disc()
def close(self) -> None:
logging.debug('AsyncStreamClient.close()')
self.close_cb = None
super().close()
async def client_loop(self, _: str) -> None:
'''Loop for receiving messages from the TSUN cloud (client-side)'''
Infos.inc_counter('Cloud_Conn_Cnt')
if self.emu_mode:
Infos.inc_counter('EmuMode_Cnt')
else:
Infos.inc_counter('ProxyMode_Cnt')
await self.publish_outstanding_mqtt()
await self.loop()
if self.emu_mode:
Infos.dec_counter('EmuMode_Cnt')
else:
Infos.dec_counter('ProxyMode_Cnt')
Infos.dec_counter('Cloud_Conn_Cnt')
await self.publish_outstanding_mqtt()
logger.info(f'[{self.node_id}:{self.conn_no}] '
'Client loop stopped for'
f' l{self.l_addr}')
if self.close_cb:
self.close_cb()
async def _async_forward(self) -> None:
"""forward handler transmits data over the remote connection"""
if self.remote.stream:
self.remote.ifc.update_header_cb(self.fwd_fifo.peek())
self.fwd_fifo.logging(logging.INFO, 'Forward to '
f'{self.remote.ifc.r_addr}:')
self.remote.ifc._writer.write(self.fwd_fifo.get())
await self.remote.ifc._writer.drain()

52
app/src/byte_fifo.py Normal file
View File

@@ -0,0 +1,52 @@
from messages import hex_dump_str, hex_dump_memory
class ByteFifo:
""" a byte FIFO buffer with trigger callback """
__slots__ = ('__buf', '__trigger_cb')
def __init__(self):
self.__buf = bytearray()
self.__trigger_cb = None
def reg_trigger(self, cb) -> None:
self.__trigger_cb = cb
def __iadd__(self, data):
self.__buf.extend(data)
return self
def __call__(self):
'''triggers the observer'''
if callable(self.__trigger_cb):
return self.__trigger_cb()
return None
def get(self, size: int = None) -> bytearray:
'''removes size numbers of byte and return them'''
if not size:
data = self.__buf
self.clear()
else:
data = self.__buf[:size]
# The fast delete syntax
self.__buf[:size] = b''
return data
def peek(self, size: int = None) -> bytearray:
'''returns size numbers of byte without removing them'''
if not size:
return self.__buf
return self.__buf[:size]
def clear(self):
self.__buf = bytearray()
def __len__(self) -> int:
return len(self.__buf)
def __str__(self) -> str:
return hex_dump_str(self.__buf, self.__len__())
def logging(self, level, info):
hex_dump_memory(level, info, self.__buf, self.__len__())

254
app/src/cnf/config.py Normal file
View File

@@ -0,0 +1,254 @@
'''Config module handles the proxy configuration'''
import shutil
import logging
from abc import ABC, abstractmethod
from schema import Schema, And, Or, Use, Optional
class ConfigIfc(ABC):
'''Abstract basis class for config readers'''
def __init__(self):
Config.add(self)
@abstractmethod
def get_config(self) -> dict: # pragma: no cover
'''get the unverified config from the reader'''
pass
@abstractmethod
def descr(self) -> str: # pragma: no cover
'''return a descriction of the source, e.g. the file name'''
pass
def _extend_key(self, conf, key, val):
'''split a dotted dict key into a hierarchical dict tree '''
lst = key.split('.')
d = conf
for i, idx in enumerate(lst, 1): # pragma: no branch
if i == len(lst):
d[idx] = val
break
if idx not in d:
d[idx] = {}
d = d[idx]
class Config():
'''Static class Config build and sanitize the internal config dictenary.
Using config readers, a partial configuration is added to config.
Config readers are a derivation of the abstract ConfigIfc reader.
When a config reader is instantiated, theits `get_config` method is
called automatically and afterwards the config will be merged.
'''
conf_schema = Schema({
'tsun': {
'enabled': Use(bool),
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
},
'solarman': {
'enabled': Use(bool),
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
},
'mqtt': {
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
'user': Or(None, And(Use(str),
Use(lambda s: s if len(s) > 0 else None))),
'passwd': Or(None, And(Use(str),
Use(lambda s: s if len(s) > 0 else None)))
},
'ha': {
'auto_conf_prefix': Use(str),
'discovery_prefix': Use(str),
'entity_prefix': Use(str),
'proxy_node_id': Use(str),
'proxy_unique_id': Use(str)
},
'gen3plus': {
'at_acl': {
Or('mqtt', 'tsun'): {
'allow': [str],
Optional('block', default=[]): [str]
}
}
},
'inverters': {
'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 + '/'
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),
},
Optional('pv3'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv4'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv5'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv6'): {
Optional('type'): Use(str),
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
)
@classmethod
def init(cls, def_reader: ConfigIfc, log_path: str = '') -> None | str:
'''Initialise the Proxy-Config
Copy the internal default config file into the config directory
and initialise the Config with the default configuration '''
cls.err = None
cls.log_path = log_path
cls.def_config = {}
try:
# make the default config transparaent by copying it
# in the config.example file
logging.debug('Copy Default Config to config.example.toml')
shutil.copy2("default_config.toml",
"config/config.example.toml")
except Exception:
pass
# read example config file as default configuration
try:
def_config = def_reader.get_config()
cls.def_config = cls.conf_schema.validate(def_config)
logging.info(f'Read from {def_reader.descr()} => ok')
except Exception as error:
cls.err = f'Config.read: {error}'
logging.error(
f"Can't read from {def_reader.descr()} => error\n {error}")
cls.act_config = cls.def_config.copy()
@classmethod
def add(cls, reader: ConfigIfc):
'''Merge the config from the Config Reader into the config
Checks if a default config exists. If no default configuration exists,
the Config.init method has not yet been called.This is normal for the very
first Config Reader which creates the default config and must be ignored
here. The default config reader is handled in the Config.init method'''
if hasattr(cls, 'def_config'):
cls.__parse(reader)
@classmethod
def get_error(cls) -> None | str:
'''return the last error as a string or None if there is no error'''
return cls.err
@classmethod
def __parse(cls, reader) -> None | str:
'''Read config from the reader, merge it with the default config
and sanitize the result'''
res = 'ok'
try:
rd_config = reader.get_config()
config = cls.act_config.copy()
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
'gen3plus', 'batteries']:
if key in rd_config:
config[key] = config[key] | rd_config[key]
cls.act_config = cls.conf_schema.validate(config)
except FileNotFoundError:
res = 'n/a'
except Exception as error:
cls.err = f'error: {error}'
logging.error(
f"Can't read from {reader.descr()} => error\n {error}")
return cls.err
logging.info(f'Read from {reader.descr()} => {res}')
return cls.err
@classmethod
def get(cls, member: str = None):
'''Get a named attribute from the proxy config. If member ==
None it returns the complete config dict'''
if member:
return cls.act_config.get(member, {})
else:
return cls.act_config
@classmethod
def is_default(cls, member: str) -> bool:
'''Check if the member is the default value'''
return cls.act_config.get(member) == cls.def_config.get(member)
@classmethod
def get_log_path(cls) -> str:
return cls.log_path

View File

@@ -0,0 +1,25 @@
'''Config Reader module which handles config values from the environment'''
import os
from cnf.config import ConfigIfc
class ConfigReadEnv(ConfigIfc):
'''Reader for environment values of the configuration'''
def get_config(self) -> dict:
conf = {}
data = [
('mqtt.host', 'MQTT_HOST'),
('mqtt.port', 'MQTT_PORT'),
('mqtt.user', 'MQTT_USER'),
('mqtt.passwd', 'MQTT_PASSWORD'),
]
for key, env_var in data:
val = os.getenv(env_var)
if val:
self._extend_key(conf, key, val)
return conf
def descr(self):
return "environment"

View File

@@ -0,0 +1,47 @@
'''Config Reader module which handles *.json config files'''
import json
from cnf.config import ConfigIfc
class ConfigReadJson(ConfigIfc):
'''Reader for json config files'''
def __init__(self, cnf_file='/data/options.json'):
'''Read a json file and add the settings to the config'''
if not isinstance(cnf_file, str):
return
self.cnf_file = cnf_file
super().__init__()
def convert_inv(self, conf, inv):
if 'serial' in inv:
snr = inv['serial']
del inv['serial']
conf[snr] = {}
for key, val in inv.items():
self._extend_key(conf[snr], key, val)
def convert_inv_arr(self, conf, key, val: list):
if key not in conf:
conf[key] = {}
for elm in val:
self.convert_inv(conf[key], elm)
def convert_to_obj(self, data):
conf = {}
for key, val in data.items():
if (key == 'inverters' or key == 'batteries') and \
isinstance(val, list):
self.convert_inv_arr(conf, key, val)
else:
self._extend_key(conf, key, val)
return conf
def get_config(self) -> dict:
with open(self.cnf_file) as f:
data = json.load(f)
return self.convert_to_obj(data)
def descr(self):
return self.cnf_file

View File

@@ -0,0 +1,21 @@
'''Config Reader module which handles *.toml config files'''
import tomllib
from cnf.config import ConfigIfc
class ConfigReadToml(ConfigIfc):
'''Reader for toml config files'''
def __init__(self, cnf_file):
'''Read a toml file and add the settings to the config'''
if not isinstance(cnf_file, str):
return
self.cnf_file = cnf_file
super().__init__()
def get_config(self) -> dict:
with open(self.cnf_file, "rb") as f:
return tomllib.load(f)
def descr(self):
return self.cnf_file

View File

@@ -0,0 +1,204 @@
##########################################################################################
###
### T S U N - G E N 3 - P R O X Y
###
### from Stefan Allius
###
##########################################################################################
###
### The readme will give you an overview of the project:
### https://s-allius.github.io/tsun-gen3-proxy/
###
### The proxy supports different operation modes. Select the proper mode
### which depends on your inverter type and you inverter firmware.
### Please read:
### 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-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
###
##########################################################################################
##########################################################################################
##
## MQTT broker configuration
##
## In this block, you must configure the connection to your MQTT broker and specify the
## 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-toml#mqtt-broker-account
##
mqtt.host = 'mqtt' # URL or IP address of the mqtt broker
mqtt.port = 1883
mqtt.user = ''
mqtt.passwd = ''
##########################################################################################
##
## HOME ASSISTANT
##
## The proxy supports the MQTT autoconfiguration of Home Assistant (HA). The default
## 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-toml#home-assistant
##
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
##########################################################################################
##
## GEN3 Proxy Mode Configuration
##
## In this block, you can configure an optional connection to the TSUN cloud for GEN3
## 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-toml#tsun-cloud-for-gen3-inverter-only
##
tsun.enabled = true # false: disables connecting to the tsun cloud, and avoids updates
tsun.host = 'logger.talent-monitoring.com'
tsun.port = 5005
##########################################################################################
##
## GEN3PLUS Proxy Mode Configuration
##
## In this block, you can configure an optional connection to the TSUN cloud for GEN3PLUS
## 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-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'
solarman.port = 10000
##########################################################################################
###
### Inverter Definitions
###
### The proxy supports the simultaneous operation of several inverters, even of different
### types. A configuration block must be defined for each inverter, in which all necessary
### parameters must be specified. These depend on the operation mode used and also differ
### slightly depending on the inverter type.
###
### In addition, the PV modules can be defined at the individual inputs for documentation
### purposes, whereby these are displayed in Home Assistant.
###
### The proxy only accepts connections from known inverters. This can be switched off for
### test purposes and unknown serial numbers are also accepted.
###
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
## `[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
##
## The serial numbers of all GEN3 inverters start with `R17`!
##
[inverters."R170000000000001"]
node_id = '' # MQTT replacement for inverters serial number
suggested_area = '' # suggested installation area for home-assistant
modbus_polling = false # Disable optional MODBUS polling
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
##########################################################################################
##
## 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
## `[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
##
## The serial numbers of all GEN3PLUS inverters start with `Y17` or Y47! Each GEN3PLUS
## inverter is supplied with a “Monitoring SN:”. This can be found on a sticker enclosed
## with the inverter.
##
[inverters."Y170000000000001"]
monitor_sn = 2000000000 # The GEN3PLUS "Monitoring SN:"
node_id = '' # MQTT replacement for inverters 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
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
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
### this connection or parameters (e.g. network credentials) can be queried. Filters can
### then be configured for the AT+ commands from the TSUN Cloud so that only certain
### accesses are permitted.
###
### An overview of all known AT+ commands can be found here:
### https://github.com/s-allius/tsun-gen3-proxy/wiki/AT--commands
###
[gen3plus.at_acl]
# filter for received commands from the internet
tsun.allow = ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']
tsun.block = []
# filter for received commands from the MQTT broker
mqtt.allow = ['AT+']
mqtt.block = []

View File

@@ -1,170 +0,0 @@
'''Config module handles the proxy configuration in the config.toml file'''
import shutil
import tomllib
import logging
from schema import Schema, And, Or, Use, Optional
class Config():
'''Static class Config is reads and sanitize the config.
Read config.toml file and sanitize it with read().
Get named parts of the config with get()'''
config = {}
def_config = {}
conf_schema = Schema({
'tsun': {
'enabled': Use(bool),
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
},
'solarman': {
'enabled': Use(bool),
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
},
'mqtt': {
'host': Use(str),
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
},
'ha': {
'auto_conf_prefix': Use(str),
'discovery_prefix': Use(str),
'entity_prefix': Use(str),
'proxy_node_id': Use(str),
'proxy_unique_id': Use(str)
},
'gen3plus': {
'at_acl': {
Or('mqtt', 'tsun'): {
'allow': [str],
Optional('block', default=[]): [str]
}
}
},
'inverters': {
'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 + '/'
if len(s) > 0 and
s[-1] != '/' else s)),
Optional('suggested_area', default=""): Use(str),
Optional('pv1'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv2'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv3'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv4'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv5'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
},
Optional('pv6'): {
Optional('type'): Use(str),
Optional('manufacturer'): Use(str),
}
}}
}, ignore_extra_keys=True
)
@classmethod
def class_init(cls): # pragma: no cover
try:
# make the default config transparaent by copying it
# in the config.example file
logging.debug('Copy Default Config to config.example.toml')
shutil.copy2("default_config.toml",
"config/config.example.toml")
except Exception:
pass
cls.read()
@classmethod
def _read_config_file(cls) -> dict: # pragma: no cover
usr_config = {}
try:
with open("config/config.toml", "rb") as f:
usr_config = tomllib.load(f)
except Exception as error:
err = f'Config.read: {error}'
logging.error(err)
logging.info(
'\n To create the missing config.toml file, '
'you can rename the template config.example.toml\n'
' and customize it for your scenario.\n')
return usr_config
@classmethod
def read(cls, path='') -> None | str:
'''Read config file, merge it with the default config
and sanitize the result'''
err = None
config = {}
logger = logging.getLogger('data')
try:
# read example config file as default configuration
cls.def_config = {}
with open(f"{path}default_config.toml", "rb") as f:
def_config = tomllib.load(f)
cls.def_config = cls.conf_schema.validate(def_config)
# overwrite the default values, with values from
# the config.toml file
usr_config = cls._read_config_file()
# merge the default and the user config
config = def_config.copy()
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
'gen3plus']:
if key in usr_config:
config[key] |= usr_config[key]
try:
cls.config = cls.conf_schema.validate(config)
except Exception as error:
err = f'Config.read: {error}'
logging.error(err)
# logging.debug(f'Readed config: "{cls.config}" ')
except Exception as error:
err = f'Config.read: {error}'
logger.error(err)
cls.config = {}
return err
@classmethod
def get(cls, member: str = None):
'''Get a named attribute from the proxy config. If member ==
None it returns the complete config dict'''
if member:
return cls.config.get(member, {})
else:
return cls.config
@classmethod
def is_default(cls, member: str) -> bool:
'''Check if the member is the default value'''
return cls.config.get(member) == cls.def_config.get(member)

View File

@@ -1,36 +0,0 @@
import logging
# import gc
from async_stream import AsyncStream
from gen3.talent import Talent
logger = logging.getLogger('conn')
class ConnectionG3(AsyncStream, Talent):
def __init__(self, reader, writer, addr, remote_stream, server_side: bool,
id_str=b'') -> None:
AsyncStream.__init__(self, reader, writer, addr)
Talent.__init__(self, server_side, id_str)
self.remoteStream = remote_stream
'''
Our puplic methods
'''
def close(self):
AsyncStream.close(self)
Talent.close(self)
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
async def async_create_remote(self) -> None:
pass
async def async_publ_mqtt(self) -> None:
pass
'''
Our private methods
'''
def __del__(self):
super().__del__()

View File

@@ -2,91 +2,181 @@
import struct
import logging
from typing import Generator
from itertools import chain
if __name__ == "app.src.gen3.infos_g3":
from app.src.infos import Infos, Register
else: # pragma: no cover
from infos import Infos, Register
from infos import Infos, Register
class RegisterMap:
__slots__ = ()
map = {
0x00092ba8: Register.COLLECTOR_FW_VERSION,
0x000927c0: Register.CHIP_TYPE,
0x00092f90: Register.CHIP_MODEL,
0x00095a88: Register.TRACE_URL,
0x00095aec: Register.LOGGER_URL,
0x0000000a: Register.PRODUCT_NAME,
0x00000014: Register.MANUFACTURER,
0x0000001e: Register.VERSION,
0x00000028: Register.SERIAL_NUMBER,
0x00000032: Register.EQUIPMENT_MODEL,
0x00013880: Register.NO_INPUTS,
0xffffff00: Register.INVERTER_CNT,
0xffffff01: Register.UNKNOWN_SNR,
0xffffff02: Register.UNKNOWN_MSG,
0xffffff03: Register.INVALID_DATA_TYPE,
0xffffff04: Register.INTERNAL_ERROR,
0xffffff05: Register.UNKNOWN_CTRL,
0xffffff06: Register.OTA_START_MSG,
0xffffff07: Register.SW_EXCEPTION,
0xffffff08: Register.MAX_DESIGNED_POWER,
0xfffffffe: Register.TEST_REG1,
0xffffffff: Register.TEST_REG2,
0x00000640: Register.OUTPUT_POWER,
0x000005dc: Register.RATED_POWER,
0x00000514: Register.INVERTER_TEMP,
0x000006a4: Register.PV1_VOLTAGE,
0x00000708: Register.PV1_CURRENT,
0x0000076c: Register.PV1_POWER,
0x000007d0: Register.PV2_VOLTAGE,
0x00000834: Register.PV2_CURRENT,
0x00000898: Register.PV2_POWER,
0x000008fc: Register.PV3_VOLTAGE,
0x00000960: Register.PV3_CURRENT,
0x000009c4: Register.PV3_POWER,
0x00000a28: Register.PV4_VOLTAGE,
0x00000a8c: Register.PV4_CURRENT,
0x00000af0: Register.PV4_POWER,
0x00000c1c: Register.PV1_DAILY_GENERATION,
0x00000c80: Register.PV1_TOTAL_GENERATION,
0x00000ce4: Register.PV2_DAILY_GENERATION,
0x00000d48: Register.PV2_TOTAL_GENERATION,
0x00000dac: Register.PV3_DAILY_GENERATION,
0x00000e10: Register.PV3_TOTAL_GENERATION,
0x00000e74: Register.PV4_DAILY_GENERATION,
0x00000ed8: Register.PV4_TOTAL_GENERATION,
0x00000b54: Register.DAILY_GENERATION,
0x00000bb8: Register.TOTAL_GENERATION,
0x000003e8: Register.GRID_VOLTAGE,
0x0000044c: Register.GRID_CURRENT,
0x000004b0: Register.GRID_FREQUENCY,
0x000cfc38: Register.CONNECT_COUNT,
0x000c3500: Register.SIGNAL_STRENGTH,
0x000c96a8: Register.POWER_ON_TIME,
0x000d0020: Register.COLLECT_INTERVAL,
0x000cf850: Register.DATA_UP_INTERVAL,
0x000c7f38: Register.COMMUNICATION_TYPE,
0x00000191: Register.EVENT_401,
0x00000192: Register.EVENT_402,
0x00000193: Register.EVENT_403,
0x00000194: Register.EVENT_404,
0x00000195: Register.EVENT_405,
0x00000196: Register.EVENT_406,
0x00000197: Register.EVENT_407,
0x00000198: Register.EVENT_408,
0x00000199: Register.EVENT_409,
0x0000019a: Register.EVENT_410,
0x0000019b: Register.EVENT_411,
0x0000019c: Register.EVENT_412,
0x0000019d: Register.EVENT_413,
0x0000019e: Register.EVENT_414,
0x0000019f: Register.EVENT_415,
0x000001a0: Register.EVENT_416,
0xffffff00: {'reg': Register.INVERTER_CNT},
0xffffff01: {'reg': Register.UNKNOWN_SNR},
0xffffff02: {'reg': Register.UNKNOWN_MSG},
0xffffff03: {'reg': Register.INVALID_DATA_TYPE},
0xffffff04: {'reg': Register.INTERNAL_ERROR},
0xffffff05: {'reg': Register.UNKNOWN_CTRL},
0xffffff06: {'reg': Register.OTA_START_MSG},
0xffffff07: {'reg': Register.SW_EXCEPTION},
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},
0x000006a4: {'reg': Register.PV1_VOLTAGE},
0x00000708: {'reg': Register.PV1_CURRENT},
0x0000076c: {'reg': Register.PV1_POWER},
0x000007d0: {'reg': Register.PV2_VOLTAGE},
0x00000834: {'reg': Register.PV2_CURRENT},
0x00000898: {'reg': Register.PV2_POWER},
0x000008fc: {'reg': Register.PV3_VOLTAGE},
0x00000960: {'reg': Register.PV3_CURRENT},
0x000009c4: {'reg': Register.PV3_POWER},
0x00000a28: {'reg': Register.PV4_VOLTAGE},
0x00000a8c: {'reg': Register.PV4_CURRENT},
0x00000af0: {'reg': Register.PV4_POWER},
0x00000c1c: {'reg': Register.PV1_DAILY_GENERATION},
0x00000c80: {'reg': Register.PV1_TOTAL_GENERATION},
0x00000ce4: {'reg': Register.PV2_DAILY_GENERATION},
0x00000d48: {'reg': Register.PV2_TOTAL_GENERATION},
0x00000dac: {'reg': Register.PV3_DAILY_GENERATION},
0x00000e10: {'reg': Register.PV3_TOTAL_GENERATION},
0x00000e74: {'reg': Register.PV4_DAILY_GENERATION},
0x00000ed8: {'reg': Register.PV4_TOTAL_GENERATION},
0x00000b54: {'reg': Register.DAILY_GENERATION},
0x00000bb8: {'reg': Register.TOTAL_GENERATION},
0x000003e8: {'reg': Register.GRID_VOLTAGE},
0x0000044c: {'reg': Register.GRID_CURRENT},
0x000004b0: {'reg': Register.GRID_FREQUENCY},
0x00000190: {'reg': Register.EVENT_ALARM},
0x000001f4: {'reg': Register.EVENT_FAULT},
0x00000258: {'reg': Register.EVENT_BF1},
0x000002bc: {'reg': Register.EVENT_BF2},
0x00000064: {'reg': Register.INVERTER_STATUS},
0x00000fa0: {'reg': Register.BOOT_STATUS},
0x00001004: {'reg': Register.DSP_STATUS},
0x000010cc: {'reg': Register.WORK_MODE},
0x000011f8: {'reg': Register.OUTPUT_SHUTDOWN},
0x0000125c: {'reg': Register.MAX_DESIGNED_POWER},
0x000012c0: {'reg': Register.RATED_LEVEL},
0x00001324: {'reg': Register.INPUT_COEFFICIENT, 'ratio': 100/1024},
0x00001388: {'reg': Register.GRID_VOLT_CAL_COEF},
0x00002710: {'reg': Register.PROD_COMPL_TYPE},
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
}
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__ = ()
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
sug_area: str = '') \
@@ -100,17 +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 reg 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
@@ -118,10 +218,12 @@ 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:
info_id = RegisterMap.map[addr]
row = reg_map[addr]
info_id = row['reg']
data_type = result[1]
ind += 5
@@ -132,11 +234,23 @@ class InfosG3(Infos):
errors='replace')
ind += str_len+1
elif data_type == 0x00: # 'Nul' -> end
i = elms # abort the loop
elif data_type == 0x41: # 'A' -> Nop ??
ind += 0
i += 1
continue
elif data_type == 0x42: # 'B' -> byte, int8
result = struct.unpack_from('!B', buf, ind)[0]
ind += 1
elif data_type == 0x49: # 'I' -> int32
result = struct.unpack_from('!l', buf, ind)[0]
ind += 4
elif data_type == 0x53: # 'S' -> short
elif data_type == 0x53: # 'S' -> short, int16
result = struct.unpack_from('!h', buf, ind)[0]
ind += 2
@@ -144,27 +258,38 @@ class InfosG3(Infos):
result = round(struct.unpack_from('!f', buf, ind)[0], 2)
ind += 4
elif data_type == 0x4c: # 'L' -> int64
elif data_type == 0x4c: # 'L' -> long, int64
result = struct.unpack_from('!q', buf, ind)[0]
ind += 8
else:
self.inc_counter('Invalid_Data_Type')
logging.error(f"Infos.parse: data_type: {data_type}"
f" @0x{addr:04x} No:{i}"
" not supported")
return
keys, level, unit, must_incr = self._key_obj(info_id)
if keys:
name, update = self.update_db(keys, must_incr, result)
yield keys[0], update
else:
update = False
name = str(f'info-id.0x{addr:x}')
if update:
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
f' {result}{unit}')
result = self.__modify_val(row, result)
yield from self.__store_result(addr, result, info_id, node_id)
i += 1
def __modify_val(self, row, result):
if row and 'ratio' in row:
result = round(result * row['ratio'], 2)
return result
def __store_result(self, addr, result, info_id, node_id):
keys, level, unit, must_incr = self._key_obj(info_id)
if keys:
name, update = self.update_db(keys, must_incr, result)
yield keys[0], update
else:
update = False
name = str(f'info-id.0x{addr:x}')
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

@@ -1,128 +1,9 @@
import asyncio
import logging
import traceback
import json
from config import Config
from inverter import Inverter
from gen3.connection_g3 import ConnectionG3
from aiomqtt import MqttCodeError
from infos import Infos
from asyncio import StreamReader, StreamWriter
# import gc
# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
from inverter_base import InverterBase
from gen3.talent import Talent
class InverterG3(Inverter, ConnectionG3):
'''class Inverter is a derivation of an Async_Stream
The class has some class method for managing common resources like a
connection to the MQTT broker or proxy error counter which are common
for all inverter connection
Instances of the class are connections to an inverter and can have an
optional link to an remote connection to the TSUN cloud. A remote
connection dies with the inverter connection.
class methods:
class_init(): initialize the common resources of the proxy (MQTT
broker, Proxy DB, etc). Must be called before the
first inverter instance can be created
class_close(): release the common resources of the proxy. Should not
be called before any instances of the class are
destroyed
methods:
server_loop(addr): Async loop method for receiving messages from the
inverter (server-side)
client_loop(addr): Async loop method for receiving messages from the
TSUN cloud (client-side)
async_create_remote(): Establish a client connection to the TSUN cloud
async_publ_mqtt(): Publish data to MQTT broker
close(): Release method which must be called before a instance can be
destroyed
'''
def __init__(self, reader, writer, addr):
super().__init__(reader, writer, addr, None, True)
self.__ha_restarts = -1
async def async_create_remote(self) -> None:
'''Establish a client connection to the TSUN cloud'''
tsun = Config.get('tsun')
host = tsun['host']
port = tsun['port']
addr = (host, port)
try:
logging.info(f'[{self.node_id}] Connected to {addr}')
connect = asyncio.open_connection(host, port)
reader, writer = await connect
self.remoteStream = ConnectionG3(reader, writer, addr, self,
False, self.id_str)
asyncio.create_task(self.client_loop(addr))
except (ConnectionRefusedError, TimeoutError) as error:
logging.info(f'{error}')
except Exception:
self.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception for {addr}:\n"
f"{traceback.format_exc()}")
async def async_publ_mqtt(self) -> None:
'''publish data to MQTT broker'''
# check if new inverter or collector infos are available or when the
# home assistant has changed the status back to online
try:
if (('inverter' in self.new_data and self.new_data['inverter'])
or ('collector' in self.new_data and
self.new_data['collector'])
or self.mqtt.ha_restarts != self.__ha_restarts):
await self._register_proxy_stat_home_assistant()
await self.__register_home_assistant()
self.__ha_restarts = self.mqtt.ha_restarts
for key in self.new_data:
await self.__async_publ_mqtt_packet(key)
for key in Infos.new_stat_data:
await self._async_publ_mqtt_proxy_stat(key)
except MqttCodeError as error:
logging.error(f'Mqtt except: {error}')
except Exception:
self.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception:\n"
f"{traceback.format_exc()}")
async def __async_publ_mqtt_packet(self, key):
db = self.db.db
if key in db and self.new_data[key]:
data_json = json.dumps(db[key])
node_id = self.node_id
logger_mqtt.debug(f'{key}: {data_json}')
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
self.new_data[key] = False
async def __register_home_assistant(self) -> None:
'''register all our topics at home assistant'''
for data_json, component, node_id, id in self.db.ha_confs(
self.entity_prfx, self.node_id, self.unique_id,
self.sug_area):
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
f" node_id:'{node_id}' {data_json}")
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
f"/{node_id}{id}/config", data_json)
self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}')
def close(self) -> None:
logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}')
super().close() # call close handler in the parent class
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
def __del__(self):
logging.debug("InverterG3.__del__")
super().__del__()
class InverterG3(InverterBase):
def __init__(self, reader: StreamReader, writer: StreamWriter):
super().__init__(reader, writer, 'tsun', Talent)

View File

@@ -1,18 +1,15 @@
import struct
import logging
import time
from zoneinfo import ZoneInfo
from datetime import datetime
from tzlocal import get_localzone
if __name__ == "app.src.gen3.talent":
from app.src.messages import hex_dump_memory, Message
from app.src.modbus import Modbus
from app.src.config import Config
from app.src.gen3.infos_g3 import InfosG3
else: # pragma: no cover
from messages import hex_dump_memory, Message
from modbus import Modbus
from config import Config
from gen3.infos_g3 import InfosG3
from async_ifc import AsyncIfc
from messages import Message, State
from modbus import Modbus
from cnf.config import Config
from gen3.infos_g3 import InfosG3
from infos import Register
logger = logging.getLogger('msg')
@@ -35,48 +32,62 @@ class Control:
class Talent(Message):
def __init__(self, server_side: bool, id_str=b''):
super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
def __init__(self, inverter, addr, ifc: "AsyncIfc", server_side: bool,
client_mode: bool = False, id_str=b''):
super().__init__('G3', ifc, server_side, self.send_modbus_cb,
mb_timeout=15)
_ = inverter
ifc.rx_set_cb(self.read)
ifc.prot_set_timeout_cb(self._timeout)
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
ifc.prot_set_update_header_cb(self._update_header)
self.addr = addr
self.conn_no = ifc.get_conn_no()
self.await_conn_resp_cnt = 0
self.id_str = id_str
self.contact_name = b''
self.contact_mail = b''
self.ts_offset = 0 # time offset between tsun cloud and local
self.db = InfosG3()
self.switch = {
0x00: self.msg_contact_info,
0x13: self.msg_ota_update,
0x22: self.msg_get_time,
0x99: self.msg_heartbeat,
0x71: self.msg_collector_data,
# 0x76:
0x77: self.msg_modbus,
# 0x78:
0x87: self.msg_modbus2,
0x04: self.msg_inverter_data,
}
self.log_lvl = {
0x00: logging.INFO,
0x13: logging.INFO,
0x22: logging.INFO,
0x99: logging.INFO,
0x71: logging.INFO,
# 0x76:
0x77: self.get_modbus_log_lvl,
# 0x78:
0x87: self.get_modbus_log_lvl,
0x04: logging.INFO,
}
self.modbus_elms = 0 # for unit tests
self.node_id = 'G3' # will be overwritten in __set_serial_no
# self.forwarding = Config.get('tsun')['enabled']
self.sensor_list = 0
'''
Our puplic methods
'''
def close(self) -> None:
logging.debug('Talent.close()')
# we have refernces to methods of this class in self.switch
# we have references to methods of this class in self.switch
# so we have to erase self.switch, otherwise this instance can't be
# deallocated by the garbage collector ==> we get a memory leak
self.switch.clear()
self.log_lvl.clear()
self.state = self.STATE_CLOSED
super().close()
def __set_serial_no(self, serial_no: str):
@@ -89,10 +100,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']
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
self._set_config_parms(inv, serial_no)
self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
else:
self.node_id = ''
self.sug_area = ''
@@ -104,64 +114,76 @@ class Talent(Message):
logger.debug(f'SerialNo {serial_no} not known but accepted!')
self.unique_id = serial_no
self.db.set_db_def_value(Register.COLLECTOR_SNR, serial_no)
def read(self) -> None:
def read(self) -> float:
'''process all received messages in the _recv_buffer'''
self._read()
while True:
if not self.header_valid:
self.__parse_header(self.ifc.rx_peek(), self.ifc.rx_len())
if not self.header_valid:
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
if self.header_valid and \
self.ifc.rx_len() >= (self.header_len + self.data_len):
if self.state == State.init:
self.state = State.received # received 1st package
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
self.data_len):
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
if callable(log_lvl):
log_lvl = log_lvl()
log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
if callable(log_lvl):
log_lvl = log_lvl()
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
self._recv_buffer, self.header_len+self.data_len)
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:'
f' BufLen: {self.ifc.rx_len()}'
f' HdrLen: {self.header_len}'
f' DtaLen: {self.data_len}')
self.__set_serial_no(self.id_str.decode("utf-8"))
self.__dispatch_msg()
self.__flush_recv_msg()
return
self.__set_serial_no(self.id_str.decode("utf-8"))
self.__dispatch_msg()
self.__flush_recv_msg()
else:
return 0 # don not wait before sending a response
def forward(self, buffer, buflen) -> None:
def forward(self) -> None:
'''add the actual receive msg to the forwarding queue'''
tsun = Config.get('tsun')
if tsun['enabled']:
self._forward_buffer = buffer[:buflen]
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
buffer, buflen)
buflen = self.header_len+self.data_len
buffer = self.ifc.rx_peek(buflen)
self.ifc.fwd_add(buffer)
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
self.__parse_header(self._forward_buffer,
len(self._forward_buffer))
fnc = self.switch.get(self.msg_id, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
return
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
if self.state != self.STATE_UP:
logger.warn(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
return
self.__build_header(0x70, 0x77)
self._send_buffer += b'\x00\x01\xa3\x28' # fixme
self._send_buffer += struct.pack('!B', len(modbus_pdu))
self._send_buffer += modbus_pdu
self.ifc.tx_add(b'\x00\x01\xa3\x28') # magic ?
self.ifc.tx_add(struct.pack('!B', len(modbus_pdu)))
self.ifc.tx_add(modbus_pdu)
self.__finish_send_msg()
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
self._send_buffer, len(self._send_buffer))
self.writer.write(self._send_buffer)
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
self.ifc.tx_flush()
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
if self.state != self.STATE_UP:
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
' as the state is not UP')
def mb_timout_cb(self, exp_cnt):
self.mb_timer.start(self.mb_timeout)
if self.mb_scan:
self._send_modbus_scan()
return
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
if 2 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x2000,
96, logging.DEBUG)
else:
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
@@ -170,9 +192,9 @@ class Talent(Message):
self.msg_id = 0
self.await_conn_resp_cnt += 1
self.__build_header(0x91)
self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
f'{len(contact_mail)+1}p',
contact_name, contact_mail)
self.ifc.tx_add(struct.pack(f'!{len(contact_name)+1}p'
f'{len(contact_mail)+1}p',
contact_name, contact_mail))
self.__finish_send_msg()
return True
@@ -196,14 +218,44 @@ class Talent(Message):
return switch.get(type, '???')
def _timestamp(self): # pragma: no cover
if False:
# utc as epoche
ts = time.time()
else:
# convert localtime in epoche
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
'''returns timestamp fo the inverter as localtime
since 1.1.1970 in msec'''
# convert localtime in epoche
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
return round(ts*1000)
def _utcfromts(self, ts: float):
'''converts inverter timestamp into unix time (epoche)'''
dt = datetime.fromtimestamp(ts/1000, tz=ZoneInfo("UTC")). \
replace(tzinfo=get_localzone())
return dt.timestamp()
def _utc(self): # pragma: no cover
'''returns unix time (epoche)'''
return datetime.now().timestamp()
def _update_header(self, _forward_buffer):
'''update header for message before forwarding,
add time offset to timestamp'''
_len = len(_forward_buffer)
ofs = 0
while ofs < _len:
result = struct.unpack_from('!lB', _forward_buffer, 0)
msg_len = 4 + result[0]
id_len = result[1] # len of variable id string
if _len < 2*id_len + 21:
return
result = struct.unpack_from('!B', _forward_buffer, id_len+6)
msg_code = result[0]
if msg_code == 0x71 or msg_code == 0x04:
result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len)
ts = result[0] + self.ts_offset
logger.debug(f'offset: {self.ts_offset:08x}'
f' proxy-time: {ts:08x}')
struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts)
ofs += msg_len
# check if there is a complete header in the buffer, parse it
# and set
# self.header_len
@@ -219,8 +271,15 @@ class Talent(Message):
if (buf_len < 5): # enough bytes to read len and id_len?
return
result = struct.unpack_from('!lB', buf, 0)
len = result[0] # len of complete message
msg_len = result[0] # len of complete message
id_len = result[1] # len of variable id string
if id_len > 17:
logger.warning(f'len of ID string must == 16 but is {id_len}')
self.inc_counter('Invalid_Msg_Format')
# erase broken recv buffer
self.ifc.rx_clear()
return
hdr_len = 5+id_len+2
@@ -233,37 +292,38 @@ class Talent(Message):
self.id_str = result[0]
self.ctrl = Control(result[1])
self.msg_id = result[2]
self.data_len = len-id_len-3
self.data_len = msg_len-id_len-3
self.header_len = hdr_len
self.header_valid = True
return
def __build_header(self, ctrl, msg_id=None) -> None:
if not msg_id:
msg_id = self.msg_id
self.send_msg_ofs = len(self._send_buffer)
self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB',
0, self.id_str, ctrl, msg_id)
self.send_msg_ofs = self.ifc.tx_len()
self.ifc.tx_add(struct.pack(f'!l{len(self.id_str)+1}pBB',
0, self.id_str, ctrl, msg_id))
fnc = self.switch.get(msg_id, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'tx') +
f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}')
def __finish_send_msg(self) -> None:
_len = len(self._send_buffer) - self.send_msg_ofs
struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4)
_len = self.ifc.tx_len() - self.send_msg_ofs
struct.pack_into('!l', self.ifc.tx_peek(), self.send_msg_ofs,
_len-4)
def __dispatch_msg(self) -> None:
fnc = self.switch.get(self.msg_id, self.msg_unknown)
if self.unique_id:
logger.info(self.__flow_str(self.server_side, 'rx') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
f' Ctl: {int(self.ctrl):#02x} ({self.state}) '
f'Msg: {fnc.__name__!r}')
fnc()
else:
logger.info(self.__flow_str(self.server_side, 'drop') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
def __flush_recv_msg(self) -> None:
self._recv_buffer = self._recv_buffer[(self.header_len+self.data_len):]
self.ifc.rx_get(self.header_len+self.data_len)
self.header_valid = False
'''
@@ -273,181 +333,279 @@ class Talent(Message):
if self.ctrl.is_ind():
if self.server_side and self.__process_contact_info():
self.__build_header(0x91)
self._send_buffer += b'\x01'
self.ifc.tx_add(b'\x01')
self.__finish_send_msg()
# don't forward this contact info here, we will build one
# when the remote connection is established
elif self.await_conn_resp_cnt > 0:
self.await_conn_resp_cnt -= 1
else:
self.forward(self._recv_buffer, self.header_len+self.data_len)
return
self.forward()
else:
logger.warning('Unknown Ctrl')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def __process_contact_info(self) -> bool:
result = struct.unpack_from('!B', self._recv_buffer, self.header_len)
buf = self.ifc.rx_peek()
result = struct.unpack_from('!B', buf, self.header_len)
name_len = result[0]
if self.data_len < name_len+2:
if self.data_len == 1: # this is a response withone status byte
return False
result = struct.unpack_from(f'!{name_len+1}pB', self._recv_buffer,
self.header_len)
self.contact_name = result[0]
mail_len = result[1]
logger.info(f'name: {self.contact_name}')
if self.data_len >= name_len+2:
result = struct.unpack_from(f'!{name_len+1}pB', buf,
self.header_len)
self.contact_name = result[0]
mail_len = result[1]
logger.info(f'name: {self.contact_name}')
result = struct.unpack_from(f'!{mail_len+1}p', self._recv_buffer,
self.header_len+name_len+1)
self.contact_mail = result[0]
result = struct.unpack_from(f'!{mail_len+1}p', buf,
self.header_len+name_len+1)
self.contact_mail = result[0]
logger.info(f'mail: {self.contact_mail}')
return True
def msg_get_time(self):
tsun = Config.get('tsun')
if tsun['enabled']:
if self.ctrl.is_ind():
if self.data_len >= 8:
ts = self._timestamp()
result = struct.unpack_from('!q', self._recv_buffer,
self.header_len)
logger.debug(f'tsun-time: {result[0]:08x}'
f' proxy-time: {ts:08x}')
else:
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
if self.ctrl.is_ind():
if self.data_len == 0:
if self.state == State.up:
self.state = State.pend # block MODBUS cmds
ts = self._timestamp()
logger.debug(f'time: {ts:08x}')
self.__build_header(0x91)
self.ifc.tx_add(struct.pack('!q', ts))
self.__finish_send_msg()
elif self.data_len >= 8:
ts = self._timestamp()
result = struct.unpack_from('!q', self.ifc.rx_peek(),
self.header_len)
self.ts_offset = result[0]-ts
if self.ifc.remote.stream:
self.ifc.remote.stream.ts_offset = self.ts_offset
logger.debug(f'tsun-time: {int(result[0]):08x}'
f' proxy-time: {ts:08x}'
f' offset: {self.ts_offset}')
return # ignore received response
else:
if self.ctrl.is_ind():
if self.data_len == 0:
ts = self._timestamp()
logger.debug(f'time: {ts:08x}')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.__build_header(0x91)
self._send_buffer += struct.pack('!q', ts)
self.__finish_send_msg()
self.forward()
else:
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
def msg_heartbeat(self):
if self.ctrl.is_ind():
if self.data_len == 9:
self.state = State.up # allow MODBUS cmds
if (self.modbus_polling):
self.mb_timer.start(self.mb_first_timeout)
self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout)
self.__build_header(0x99)
self.ifc.tx_add(b'\x02')
self.__finish_send_msg()
result = struct.unpack_from('!Bq', self.ifc.rx_peek(),
self.header_len)
resp_code = result[0]
ts = result[1]+self.ts_offset
logger.debug(f'inv-time: {int(result[1]):08x}'
f' tsun-time: {ts:08x}'
f' offset: {self.ts_offset}')
struct.pack_into('!Bq', self.ifc.rx_peek(),
self.header_len, resp_code, ts)
elif self.ctrl.is_resp():
result = struct.unpack_from('!B', self.ifc.rx_peek(),
self.header_len)
resp_code = result[0]
logging.debug(f'Heartbeat-RespCode: {resp_code}')
return
else:
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward()
def parse_msg_header(self):
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
result = struct.unpack_from('!lB', self.ifc.rx_peek(),
self.header_len)
data_id = result[0] # len of complete message
id_len = result[1] # len of variable id string
logger.debug(f'Data_ID: {data_id} id_len: {id_len}')
logger.debug(f'Data_ID: 0x{data_id:08x} id_len: {id_len}')
msg_hdr_len = 5+id_len+9
result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer,
result = struct.unpack_from(f'!{id_len+1}pBq', self.ifc.rx_peek(),
self.header_len + 4)
timestamp = result[2]
logger.debug(f'ID: {result[0]} B: {result[1]}')
logger.debug(f'time: {result[2]:08x}')
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
return msg_hdr_len, data_id, timestamp
def msg_collector_data(self):
if self.ctrl.is_ind():
self.__build_header(0x99)
self._send_buffer += b'\x01'
self.ifc.tx_add(b'\x01')
self.__finish_send_msg()
self.__process_data()
self.state = self.STATE_UP
self.__process_data(False)
elif self.ctrl.is_resp():
return # ignore received response
else:
logger.warning('Unknown Ctrl')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def msg_inverter_data(self):
if self.ctrl.is_ind():
self.__build_header(0x99)
self._send_buffer += b'\x01'
self.ifc.tx_add(b'\x01')
self.__finish_send_msg()
self.__process_data()
self.state = self.STATE_UP
self.__process_data(True)
self.state = State.up # allow MODBUS cmds
if (self.modbus_polling):
self.mb_timer.start(self.mb_first_timeout)
self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout)
elif self.ctrl.is_resp():
return # ignore received response
else:
logger.warning('Unknown Ctrl')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def __process_data(self):
msg_hdr_len = self.parse_msg_header()
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)
for key, update in self.db.parse(self._recv_buffer, self.header_len
+ msg_hdr_len, self.node_id):
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: # is a replay?
return
inv_update = False
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
+ 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')
elif self.ctrl.is_ind():
pass
pass # Ok, nothing to do
else:
logger.warning('Unknown Ctrl')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def parse_modbus_header(self):
msg_hdr_len = 5
result = struct.unpack_from('!lBB', self._recv_buffer,
result = struct.unpack_from('!lBB', self.ifc.rx_peek(),
self.header_len)
modbus_len = result[1]
# logger.debug(f'Ref: {result[0]}')
# logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}')
return msg_hdr_len, modbus_len
def parse_modbus_header2(self):
msg_hdr_len = 6
result = struct.unpack_from('!lBBB', self.ifc.rx_peek(),
self.header_len)
modbus_len = result[2]
return msg_hdr_len, modbus_len
def get_modbus_log_lvl(self) -> int:
if self.ctrl.is_req():
return logging.INFO
elif self.ctrl.is_ind():
if self.server_side:
return self.mb.last_log_lvl
elif self.ctrl.is_ind() and self.server_side:
return self.mb.last_log_lvl
return logging.WARNING
def msg_modbus(self):
hdr_len, modbus_len = self.parse_modbus_header()
data = self._recv_buffer[self.header_len:
self.header_len+self.data_len]
hdr_len, _ = self.parse_modbus_header()
self.__msg_modbus(hdr_len)
def msg_modbus2(self):
hdr_len, _ = self.parse_modbus_header2()
self.__msg_modbus(hdr_len)
def __msg_modbus(self, hdr_len):
data = self.ifc.rx_peek()[self.header_len:
self.header_len+self.data_len]
if self.ctrl.is_req():
if self.remoteStream.mb.recv_req(data[hdr_len:],
self.msg_forward):
rstream = self.ifc.remote.stream
if rstream.mb.recv_req(data[hdr_len:], rstream.msg_forward):
self.inc_counter('Modbus_Command')
else:
self.inc_counter('Invalid_Msg_Format')
elif self.ctrl.is_ind():
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
self.modbus_elms = 0
# logger.debug(f'Modbus Ind MsgLen: {modbus_len}')
if not self.server_side:
logger.warning('Unknown Message')
self.inc_counter('Unknown_Msg')
return
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:],
self.node_id):
hdr_len:]):
if update:
self._set_mqtt_timestamp(key, self._utc())
self.new_data[key] = True
self.modbus_elms += 1 # count for unit tests
else:
logger.warning('Unknown Ctrl')
logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def msg_forward(self):
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()
def msg_unknown(self):
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
self.inc_counter('Unknown_Msg')
self.forward(self._recv_buffer, self.header_len+self.data_len)
self.forward()

View File

@@ -1,36 +0,0 @@
import logging
# import gc
from async_stream import AsyncStream
from gen3plus.solarman_v5 import SolarmanV5
logger = logging.getLogger('conn')
class ConnectionG3P(AsyncStream, SolarmanV5):
def __init__(self, reader, writer, addr, remote_stream,
server_side: bool) -> None:
AsyncStream.__init__(self, reader, writer, addr)
SolarmanV5.__init__(self, server_side)
self.remoteStream = remote_stream
'''
Our puplic methods
'''
def close(self):
AsyncStream.close(self)
SolarmanV5.close(self)
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
async def async_create_remote(self) -> None:
pass
async def async_publ_mqtt(self) -> None:
pass
'''
Our private methods
'''
def __del__(self):
super().__del__()

View File

@@ -1,36 +1,87 @@
import struct
from typing import Generator
from itertools import chain
if __name__ == "app.src.gen3plus.infos_g3p":
from app.src.infos import Infos, Register
else: # pragma: no cover
from infos import Infos, Register
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
@staticmethod
def cmp_values(info: Infos, params: map) -> None | int:
try:
val = info.get_db_value(params['reg'])
if val < params['cmp_val']:
return params['res'][0]
if val == params['cmp_val']:
return params['res'][1]
return params['res'][2]
except Exception:
pass
return None
class RegisterMap:
# make the class read/only by using __slots__
__slots__ = ()
FMT_2_16BIT_VAL = '!HH'
FMT_3_16BIT_VAL = '!HHH'
FMT_4_16BIT_VAL = '!HHHH'
map = {
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '<L'}, # noqa: E501
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60}, # noqa: E501
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'eval': 'round(result/60)'}, # noqa: E501
0x41020018: {'reg': Register.DATA_UP_INTERVAL, 'fmt': '<B', 'ratio': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '<B', 'quotient': 60, 'dep': ProxyMode.SERVER}, # noqa: E501
0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '<B', 'ratio': 1}, # noqa: E501
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1}, # noqa: E501
0x4102001b: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 Max No Of Connected Devices
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
0x4102001d: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
0x41020046: {'reg': Register.MAC_ADDR, 'fmt': '!6B', 'func': Fmt.mac}, # noqa: E501
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
0x4102005c: {'reg': None, 'fmt': '<B', 'const': 15}, # noqa: E501
0x4102005e: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 No Of Sensors (ListLen)
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
0x41020061: {'reg': None, 'fmt': '<HB', 'const': (15, 255)}, # noqa: E501
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
}
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
# Start MODBUS Block: 0x3000 (R/O Measurements)
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
0x420100c2: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
0x420100c4: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
0x420100c6: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
0x420100c8: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
0x420100ca: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
0x420100cc: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
# 0x420100ce
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
# 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501
0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
# 0x420100da
0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
@@ -55,20 +106,126 @@ class RegisterMap:
0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
0x42010116: {'reg': Register.INV_UNKNOWN_1, 'fmt': '!H'}, # noqa: E501
# Start MODBUS Block: 0x2000 (R/W Config Paramaneters)
0x42010118: {'reg': Register.BOOT_STATUS, 'fmt': '!H'},
0x4201011a: {'reg': Register.DSP_STATUS, 'fmt': '!H'},
0x4201011c: {'reg': None, 'fmt': '!H', 'const': 1}, # noqa: E501
0x4201011e: {'reg': Register.WORK_MODE, 'fmt': '!H'},
0x42010124: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H'},
0x42010128: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
0x4201012a: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
0x4201012c: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
0x4201012e: {'reg': None, 'fmt': '!H', 'const': 1024}, # noqa: E501
0x42010130: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1024, 1, 0xffff, 1)}, # noqa: E501
0x42010138: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
0x4201013a: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x68, 0x68, 0x500)}, # noqa: E501
0x42010140: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9cd, 0x7b6, 0x139c, 0x1324)}, # noqa: E501
0x42010148: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1, 0x7ae, 0x40f, 0x41)}, # noqa: E501
0x42010150: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0xf, 0xa64, 0xa64, 0x6)}, # noqa: E501
0x42010158: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x6, 0x9f6, 0x128c, 0x128c)}, # noqa: E501
0x42010160: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x1452, 0x1452)}, # noqa: E501
0x42010168: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x10, 0x10, 0x151, 0x5)}, # noqa: E501
0x42010170: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
0x42010172: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x1, 0x139c, 0xfa0)}, # noqa: E501
0x42010178: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x4e, 0x66, 0x3e8, 0x400)}, # noqa: E501
0x42010180: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9ce, 0x7a8, 0x139c, 0x1326)}, # noqa: E501
0x42010188: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 0x0, 0)}, # noqa: E501
0x42010190: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x0, 0x0, 1024, 1024)}, # noqa: E501
0x42010198: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0, 0, 0xffff, 0)}, # noqa: E501
0x420101a0: {'reg': None, 'fmt': FMT_2_16BIT_VAL, 'const': (0x0, 0x0)}, # noqa: E501
0xffffff02: {'reg': Register.POLLING_INTERVAL},
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
}
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, DC Voltage PV1
0x42010032: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, DC Current PV1
0x42010034: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, DC Voltage PV2
0x42010036: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, DC Current PV2
0x42010038: {'reg': Register.BATT_TOTAL_CHARG, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x4201003c: {'reg': Register.BATT_PV1_STATUS, 'fmt': '!H'}, # noqa: E501 MPTT-1 Operating Status: 0(Standby), 1(Work)
0x4201003e: {'reg': Register.BATT_PV2_STATUS, 'fmt': '!H'}, # noqa: E501 MPTT-2 Operating Status: 0(Standby), 1(Work)
0x42010040: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x42010042: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501 => Batterie Status: <0(Discharging), 0(Static), 0>(Loading)
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: E501H
0x42010066: {'reg': Register.BATT_TEMP_1, 'fmt': '!h'}, # noqa: E501 Cell Temperture 1
0x42010068: {'reg': Register.BATT_TEMP_2, 'fmt': '!h'}, # noqa: E501 Cell Temperture 2
0x4201006a: {'reg': Register.BATT_TEMP_3, 'fmt': '!h'}, # noqa: E501 Cell Temperture 3
0x4201006c: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 Output Voltage
0x4201006e: {'reg': Register.BATT_OUT_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 Output Current
0x42010070: {'reg': Register.BATT_OUT_STATUS, 'fmt': '!H'}, # noqa: E501 Output Working Status: 0(Standby), 1(Work)
0x42010072: {'reg': Register.BATT_TEMP_4, 'fmt': '!h'}, # noqa: E50, Environment temp
0x42010074: {'reg': Register.BATT_ALARM, 'fmt': '!H'}, # noqa: E501 Warning Alarmcode 1, Bit 0..15
0x42010076: {'reg': Register.BATT_HW_VERS, 'fmt': '!h'}, # noqa: E501 hardware version
0x42010078: {'reg': Register.BATT_SW_VERS, 'fmt': '!h'}, # noqa: E501 software main version
'calc': {
1: {'reg': Register.BATT_PV_PWR, 'func': RegisterFunc.prod_sum, # noqa: E501 Generated Power
'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 Supply Power => Power Supply State: 0(Idle), 0>(Power Supply)
'params': [[Register.BATT_OUT_VOLT, Register.BATT_OUT_CUR]]},
4: {'reg': Register.BATT_PWR_SUPL_STATE, 'func': RegisterFunc.cmp_values, # noqa: E501
'params': {'reg': Register.BATT_OUT_PWR, 'cmp_val': 0, 'res': [0, 0, 1]}}, # noqa: E501
5: {'reg': Register.BATT_STATUS, 'func': RegisterFunc.cmp_values, # noqa: E501
'params': {'reg': Register.BATT_CUR, 'cmp_val': 0.0, 'res': [0, 1, 2]}} # noqa: E501
}
}
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):
def __init__(self):
__slots__ = ('client_mode', )
def __init__(self, client_mode: bool):
super().__init__()
self.client_mode = client_mode
self.set_db_def_value(Register.MANUFACTURER, 'TSUN')
self.set_db_def_value(Register.EQUIPMENT_MODEL, 'TSOL-MSxx00')
self.set_db_def_value(Register.CHIP_TYPE, 'IGEN TECH')
self.set_db_def_value(Register.NO_INPUTS, 4)
def __hide_topic(self, row: dict) -> bool:
if 'dep' in row:
mode = row['dep']
if self.client_mode:
return mode != ProxyMode.CLIENT
else:
return mode != ProxyMode.SERVER
return False
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
sug_area: str = '') \
@@ -82,19 +239,41 @@ 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']
res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
if self.__hide_topic(row):
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
else:
res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
if res:
yield res
def parse(self, buf, msg_type: int, rcv_ftype: int, 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
@@ -103,25 +282,50 @@ class InfosG3P(Infos):
if not isinstance(row, dict):
continue
info_id = row['reg']
fmt = row['fmt']
res = struct.unpack_from(fmt, buf, addr)
result = res[0]
if isinstance(result, (bytearray, bytes)):
result = result.decode().split('\x00')[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
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, sensor: int = 0):
buf = bytearray(len)
for idx, row in RegisterSel.get(sensor).items():
addr = idx & 0xffff
ftype = (idx >> 16) & 0xff
mtype = (idx >> 24) & 0xff
if ftype != rcv_ftype or mtype != msg_type:
continue
if not isinstance(row, dict):
continue
if 'const' in row:
val = row['const']
else:
info_id = row['reg']
val = self.get_db_value(info_id)
if not val:
continue
Fmt.set_value(buf, addr, row, val)
return buf

View File

@@ -1,128 +1,24 @@
import asyncio
import logging
import traceback
import json
from config import Config
from inverter import Inverter
from gen3plus.connection_g3p import ConnectionG3P
from aiomqtt import MqttCodeError
from infos import Infos
from asyncio import StreamReader, StreamWriter
# import gc
# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
from inverter_base import InverterBase
from gen3plus.solarman_v5 import SolarmanV5
from gen3plus.solarman_emu import SolarmanEmu
class InverterG3P(Inverter, ConnectionG3P):
'''class Inverter is a derivation of an Async_Stream
class InverterG3P(InverterBase):
def __init__(self, reader: StreamReader, writer: StreamWriter,
client_mode: bool = False):
# shared value between both inverter connections
self.forward_at_cmd_resp = False
'''Flag if response for the last at command must be send to the cloud.
The class has some class method for managing common resources like a
connection to the MQTT broker or proxy error counter which are common
for all inverter connection
False: send result only to the MQTT broker, cause the AT+ command
came from there
True: send response packet to the cloud, cause the AT+ command
came from the cloud'''
Instances of the class are connections to an inverter and can have an
optional link to an remote connection to the TSUN cloud. A remote
connection dies with the inverter connection.
class methods:
class_init(): initialize the common resources of the proxy (MQTT
broker, Proxy DB, etc). Must be called before the
first inverter instance can be created
class_close(): release the common resources of the proxy. Should not
be called before any instances of the class are
destroyed
methods:
server_loop(addr): Async loop method for receiving messages from the
inverter (server-side)
client_loop(addr): Async loop method for receiving messages from the
TSUN cloud (client-side)
async_create_remote(): Establish a client connection to the TSUN cloud
async_publ_mqtt(): Publish data to MQTT broker
close(): Release method which must be called before a instance can be
destroyed
'''
def __init__(self, reader, writer, addr):
super().__init__(reader, writer, addr, None, True)
self.__ha_restarts = -1
async def async_create_remote(self) -> None:
'''Establish a client connection to the TSUN cloud'''
tsun = Config.get('solarman')
host = tsun['host']
port = tsun['port']
addr = (host, port)
try:
logging.info(f'[{self.node_id}] Connected to {addr}')
connect = asyncio.open_connection(host, port)
reader, writer = await connect
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
False)
asyncio.create_task(self.client_loop(addr))
except (ConnectionRefusedError, TimeoutError) as error:
logging.info(f'{error}')
except Exception:
self.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception for {addr}:\n"
f"{traceback.format_exc()}")
async def async_publ_mqtt(self) -> None:
'''publish data to MQTT broker'''
# check if new inverter or collector infos are available or when the
# home assistant has changed the status back to online
try:
if (('inverter' in self.new_data and self.new_data['inverter'])
or ('collector' in self.new_data and
self.new_data['collector'])
or self.mqtt.ha_restarts != self.__ha_restarts):
await self._register_proxy_stat_home_assistant()
await self.__register_home_assistant()
self.__ha_restarts = self.mqtt.ha_restarts
for key in self.new_data:
await self.__async_publ_mqtt_packet(key)
for key in Infos.new_stat_data:
await self._async_publ_mqtt_proxy_stat(key)
except MqttCodeError as error:
logging.error(f'Mqtt except: {error}')
except Exception:
self.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception:\n"
f"{traceback.format_exc()}")
async def __async_publ_mqtt_packet(self, key):
db = self.db.db
if key in db and self.new_data[key]:
data_json = json.dumps(db[key])
node_id = self.node_id
logger_mqtt.debug(f'{key}: {data_json}')
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
self.new_data[key] = False
async def __register_home_assistant(self) -> None:
'''register all our topics at home assistant'''
for data_json, component, node_id, id in self.db.ha_confs(
self.entity_prfx, self.node_id, self.unique_id,
self.sug_area):
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
f" node_id:'{node_id}' {data_json}")
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
f"/{node_id}{id}/config", data_json)
self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}')
def close(self) -> None:
logging.debug(f'InverterG3P.close() l{self.l_addr} | r{self.r_addr}')
super().close() # call close handler in the parent class
# logger.debug (f'Inverter refs: {gc.get_referrers(self)}')
def __del__(self):
logging.debug("InverterG3P.__del__")
super().__del__()
remote_prot = None
if client_mode:
remote_prot = SolarmanEmu
super().__init__(reader, writer, 'solarman',
SolarmanV5, client_mode, remote_prot)

View File

@@ -0,0 +1,139 @@
import logging
import struct
from async_ifc import AsyncIfc
from gen3plus.solarman_v5 import SolarmanBase
from my_timer import Timer
from infos import Register
logger = logging.getLogger('msg')
class SolarmanEmu(SolarmanBase):
def __init__(self, inverter, addr, ifc: "AsyncIfc",
server_side: bool, client_mode: bool):
super().__init__(addr, ifc, server_side=False,
_send_modbus_cb=None,
mb_timeout=8)
_ = inverter
logging.debug('SolarmanEmu.init()')
self.db = ifc.remote.stream.db
self.snr = ifc.remote.stream.snr
self.hb_timeout = 60
'''actual heatbeat timeout from the last response message'''
self.data_up_inv = self.db.get_db_value(Register.DATA_UP_INTERVAL)
'''time interval for getting new MQTT data messages'''
self.hb_timer = Timer(self.send_heartbeat_cb, self.node_id)
self.data_timer = Timer(self.send_data_cb, self.node_id)
self.last_sync = self._emu_timestamp()
'''timestamp when we send the last sync message (4110)'''
self.pkt_cnt = 0
'''last sent packet number'''
self.switch = {
0x4210: 'msg_data_ind', # real time data
0x1210: self.msg_response, # at least every 5 minutes
0x4710: 'msg_hbeat_ind', # heatbeat
0x1710: self.msg_response, # every 2 minutes
0x4110: 'msg_dev_ind', # device data, sync start
0x1110: self.msg_response, # every 3 hours
}
self.log_lvl = {
0x4110: logging.INFO, # device data, sync start
0x1110: logging.INFO, # every 3 hours
0x4210: logging.INFO, # real time data
0x1210: logging.INFO, # at least every 5 minutes
0x4710: logging.DEBUG, # heatbeat
0x1710: logging.DEBUG, # every 2 minutes
}
'''
Our puplic methods
'''
def close(self) -> None:
logging.info('SolarmanEmu.close()')
# we have references to methods of this class in self.switch
# so we have to erase self.switch, otherwise this instance can't be
# deallocated by the garbage collector ==> we get a memory leak
self.switch.clear()
self.log_lvl.clear()
self.hb_timer.close()
self.data_timer.close()
self.db = None
super().close()
def _set_serial_no(self, snr: int):
logging.debug(f'SolarmanEmu._set_serial_no, snr: {snr}')
self.unique_id = str(snr)
def _init_new_client_conn(self) -> bool:
logging.debug('SolarmanEmu.init_new()')
self.data_timer.start(self.data_up_inv)
return False
def next_pkt_cnt(self):
'''get the next packet number'''
self.pkt_cnt = (self.pkt_cnt + 1) & 0xffffffff
return self.pkt_cnt
def seconds_since_last_sync(self):
'''get seconds since last 0x4110 message was sent'''
return self._emu_timestamp() - self.last_sync
def send_heartbeat_cb(self, exp_cnt):
'''send a heartbeat to the TSUN cloud'''
self._build_header(0x4710)
self.ifc.tx_add(struct.pack('<B', 0))
self._finish_send_msg()
log_lvl = self.log_lvl.get(0x4710, logging.WARNING)
self.ifc.tx_log(log_lvl, 'Send heartbeat:')
self.ifc.tx_flush()
def send_data_cb(self, exp_cnt):
'''send a inverter data message to the TSUN cloud'''
self.hb_timer.start(self.hb_timeout)
self.data_timer.start(self.data_up_inv)
_len = 420
ftype = 1
build_msg = self.db.build(_len, 0x42, ftype, 0x02b0)
self._build_header(0x4210)
self.ifc.tx_add(
struct.pack(
'<BHLLLHL', ftype, 0x02b0,
self._emu_timestamp(),
self.seconds_since_last_sync(),
self.time_ofs,
1, # offset 0x1a
self.next_pkt_cnt()))
self.ifc.tx_add(build_msg[0x20:])
self._finish_send_msg()
log_lvl = self.log_lvl.get(0x4210, logging.WARNING)
self.ifc.tx_log(log_lvl, 'Send inv-data:')
self.ifc.tx_flush()
'''
Message handler methods
'''
def msg_response(self):
'''handle a received response from the TSUN cloud'''
logger.debug("EMU received rsp:")
_, _, ts, hb = super().msg_response()
logger.debug(f"EMU ts:{ts} hb:{hb}")
self.hb_timeout = hb
self.time_ofs = ts - self._emu_timestamp()
self.hb_timer.start(self.hb_timeout)
def msg_unknown(self):
'''counts a unknown or unexpected message from the TSUN cloud'''
logger.warning(f"EMU Unknow Msg: ID:{int(self.control):#04x}")
self.inc_counter('Unknown_Msg')

View File

@@ -1,23 +1,17 @@
import struct
# import json
import logging
import time
import asyncio
from itertools import chain
from datetime import datetime
if __name__ == "app.src.gen3plus.solarman_v5":
from app.src.messages import hex_dump_memory, Message
from app.src.modbus import Modbus
from app.src.config import Config
from app.src.gen3plus.infos_g3p import InfosG3P
from app.src.infos import Register
else: # pragma: no cover
from messages import hex_dump_memory, Message
from config import Config
from modbus import Modbus
from gen3plus.infos_g3p import InfosG3P
from infos import Register
# import traceback
from proxy import Proxy
from async_ifc import AsyncIfc
from messages import hex_dump_memory, Message, State
from cnf.config import Config
from modbus import Modbus
from gen3plus.infos_g3p import InfosG3P
from infos import Register, Fmt
logger = logging.getLogger('msg')
@@ -48,20 +42,228 @@ class Sequence():
return f'{self.rcv_idx:02x}:{self.snd_idx:02x}'
class SolarmanV5(Message):
AT_CMD = 1
MB_RTU_CMD = 2
def __init__(self, server_side: bool):
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
class SolarmanBase(Message):
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
_send_modbus_cb, mb_timeout: int):
super().__init__('G3P', ifc, server_side, _send_modbus_cb,
mb_timeout)
ifc.rx_set_cb(self.read)
ifc.prot_set_timeout_cb(self._timeout)
ifc.prot_set_init_new_client_conn_cb(self._init_new_client_conn)
ifc.prot_set_update_header_cb(self.__update_header)
self.addr = addr
self.conn_no = ifc.get_conn_no()
self.header_len = 11 # overwrite construcor in class Message
self.control = 0
self.seq = Sequence(server_side)
self.snr = 0
self.db = InfosG3P()
self.time_ofs = 0
self.forward_at_cmd_resp = False
def read(self) -> float:
'''process all received messages in the _recv_buffer'''
self._read()
while True:
if not self.header_valid:
self.__parse_header(self.ifc.rx_peek(),
self.ifc.rx_len())
if self.header_valid and self.ifc.rx_len() >= \
(self.header_len + self.data_len+2):
self.__process_complete_received_msg()
self.__flush_recv_msg()
else:
return 0 # wait 0s before sending a response
'''
Our public methods
'''
def _flow_str(self, server_side: bool, type: str): # noqa: F821
switch = {
'rx': ' <',
'tx': ' >',
'forwrd': '<< ',
'drop': ' xx',
'rxS': '> ',
'txS': '< ',
'forwrdS': ' >>',
'dropS': 'xx ',
}
if server_side:
type += 'S'
return switch.get(type, '???')
def get_fnc_handler(self, ctrl):
fnc = self.switch.get(ctrl, self.msg_unknown)
if callable(fnc):
return fnc, repr(fnc.__name__)
else:
return self.msg_unknown, repr(fnc)
def _build_header(self, ctrl) -> None:
'''build header for new transmit message'''
self.send_msg_ofs = self.ifc.tx_len()
self.ifc.tx_add(struct.pack(
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr))
_fnc, _str = self.get_fnc_handler(ctrl)
logger.info(self._flow_str(self.server_side, 'tx') +
f' Ctl: {int(ctrl):#04x} Msg: {_str}')
def _finish_send_msg(self) -> None:
'''finish the transmit message, set lenght and checksum'''
_len = self.ifc.tx_len() - self.send_msg_ofs
struct.pack_into('<H', self.ifc.tx_peek(), self.send_msg_ofs+1,
_len-11)
check = sum(self.ifc.tx_peek()[
self.send_msg_ofs+1:self.send_msg_ofs + _len]) & 0xff
self.ifc.tx_add(struct.pack('<BB', check, 0x15)) # crc & stop
def _timestamp(self):
# utc as epoche
return int(time.time()) # pragma: no cover
def _emu_timestamp(self):
'''timestamp for an emulated inverter (realtime - 1 day)'''
one_day = 24*60*60
return self._timestamp()-one_day
'''
Our private methods
'''
def __update_header(self, _forward_buffer):
'''update header for message before forwarding,
set sequence and checksum'''
_len = len(_forward_buffer)
ofs = 0
while ofs < _len:
result = struct.unpack_from('<BH', _forward_buffer, ofs)
data_len = result[1] # len of variable id string
struct.pack_into('<H', _forward_buffer, ofs+5,
self.seq.get_send())
check = sum(_forward_buffer[ofs+1:ofs+data_len+11]) & 0xff
struct.pack_into('<B', _forward_buffer, ofs+data_len+11, check)
ofs += (13 + data_len)
def __process_complete_received_msg(self):
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
if callable(log_lvl):
log_lvl = log_lvl()
self.ifc.rx_log(log_lvl, f'Received from {self.addr}:')
# self._recv_buffer, self.header_len +
# self.data_len+2)
if self.__trailer_is_ok(self.ifc.rx_peek(), self.header_len
+ self.data_len + 2):
if self.state == State.init:
self.state = State.received
self._set_serial_no(self.snr)
self.__dispatch_msg()
def __parse_header(self, buf: bytes, buf_len: int) -> None:
if (buf_len < self.header_len): # enough bytes for complete header?
return
result = struct.unpack_from('<BHHHL', buf, 0)
# store parsed header values in the class
start = result[0] # start byte
self.data_len = result[1] # len of variable id string
self.control = result[2]
self.seq.set_recv(result[3])
self.snr = result[4]
if start != 0xA5:
hex_dump_memory(logging.ERROR,
'Drop packet w invalid start byte from'
f' {self.addr}:', buf, buf_len)
self.inc_counter('Invalid_Msg_Format')
# erase broken recv buffer
self.ifc.rx_clear()
return
self.header_valid = True
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
crc = buf[self.data_len+11]
stop = buf[self.data_len+12]
if stop != 0x15:
hex_dump_memory(logging.ERROR,
'Drop packet w invalid stop byte from '
f'{self.addr}:', buf, buf_len)
self.inc_counter('Invalid_Msg_Format')
if self.ifc.rx_len() > (self.data_len+13):
next_start = buf[self.data_len+13]
if next_start != 0xa5:
# erase broken recv buffer
self.ifc.rx_clear()
return False
check = sum(buf[1:buf_len-2]) & 0xff
if check != crc:
self.inc_counter('Invalid_Msg_Format')
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
f' Stop:{int(stop):#02x}')
# start & stop byte are valid, discard only this message
return False
return True
def __flush_recv_msg(self) -> None:
self.ifc.rx_get(self.header_len + self.data_len+2)
self.header_valid = False
def __dispatch_msg(self) -> None:
_fnc, _str = self.get_fnc_handler(self.control)
if self.unique_id:
logger.info(self._flow_str(self.server_side, 'rx') +
f' Ctl: {int(self.control):#04x}' +
f' Msg: {_str}')
_fnc()
else:
logger.info(self._flow_str(self.server_side, 'drop') +
f' Ctl: {int(self.control):#04x}' +
f' Msg: {_str}')
'''
Message handler methods
'''
def msg_response(self):
data = self.ifc.rx_peek()[self.header_len:]
result = struct.unpack_from('<BBLL', data, 0)
ftype = result[0] # always 2
valid = result[1] == 1 # status
ts = result[2]
set_hb = result[3] # always 60 or 120
logger.debug(f'ftype:{ftype} accepted:{valid}'
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
dt = datetime.fromtimestamp(ts)
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
return ftype, valid, ts, set_hb
class SolarmanV5(SolarmanBase):
AT_CMD = 1
MB_RTU_CMD = 2
AT_CMD_RSP = 8
MB_CLIENT_DATA_UP = 30
'''Data up time in client mode'''
HDR_FMT = '<BLLL'
'''format string for packing of the header'''
def __init__(self, inverter, addr, ifc: "AsyncIfc",
server_side: bool, client_mode: bool):
super().__init__(addr, ifc, server_side, self.send_modbus_cb,
mb_timeout=8)
self.inverter = inverter
self.db = InfosG3P(client_mode)
self.no_forwarding = False
'''not allowed to connect to TSUN cloud by connection type'''
self.establish_inv_emu = False
'''create an Solarman EMU instance to send data to the TSUN cloud'''
self.switch = {
0x4210: self.msg_data_ind, # real time data
@@ -91,7 +293,7 @@ class SolarmanV5(Message):
# 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 = {
@@ -116,307 +318,248 @@ class SolarmanV5(Message):
0x4510: logging.INFO, # from server
0x1510: self.get_cmd_rsp_log_lvl,
}
self.modbus_elms = 0 # for unit tests
g3p_cnf = Config.get('gen3plus')
if 'at_acl' in g3p_cnf: # pragma: no cover
self.at_acl = g3p_cnf['at_acl']
self.node_id = 'G3P' # will be overwritten in __set_serial_no
# self.forwarding = Config.get('solarman')['enabled']
self.sensor_list = 0
self.mb_regs = [{'addr': 0x3000, 'len': 48},
{'addr': 0x2000, 'len': 96}]
'''
Our puplic methods
'''
def close(self) -> None:
logging.debug('Solarman.close()')
# we have refernces to methods of this class in self.switch
# we have references to methods of this class in self.switch
# so we have to erase self.switch, otherwise this instance can't be
# deallocated by the garbage collector ==> we get a memory leak
self.inverter = None
self.switch.clear()
self.log_lvl.clear()
self.state = self.STATE_CLOSED
super().close()
def __set_serial_no(self, snr: int):
async def send_start_cmd(self, snr: int, host: str,
forward: bool,
start_timeout=MB_CLIENT_DATA_UP):
self.no_forwarding = True
self.establish_inv_emu = forward
self.snr = snr
self._set_serial_no(snr)
self.mb_timeout = start_timeout
self.db.set_db_def_value(Register.IP_ADDRESS, host)
self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout)
self.db.set_db_def_value(Register.DATA_UP_INTERVAL,
300)
self.db.set_db_def_value(Register.COLLECT_INTERVAL,
1)
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
120)
self.db.set_db_def_value(Register.SENSOR_LIST,
Fmt.hex4((self.sensor_list, )))
self.new_data['controller'] = True
self.state = State.up
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):
if self.state is not State.up:
self.state = State.up
if (self.modbus_polling):
self.mb_timer.start(self.mb_first_timeout)
self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout)
def establish_emu(self):
_len = 223
build_msg = self.db.build(_len, 0x41, 2)
struct.pack_into(
'<BHHHLBL', build_msg, 0, 0xA5, _len-11, 0x4110,
0, self.snr, 2, self._emu_timestamp())
self.ifc.fwd_add(build_msg)
self.ifc.fwd_add(struct.pack('<BB', 0, 0x15)) # crc & stop
def _set_config_parms(self, inv: dict, serial_no: str = ""):
'''init connection with params from the configuration'''
super()._set_config_parms(inv, serial_no)
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 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'''
serial_no = str(snr)
if self.unique_id == serial_no:
logger.debug(f'SerialNo: {serial_no}')
else:
found = False
inverters = Config.get('inverters')
batteries = Config.get('batteries')
# logger.debug(f'Inverters: {inverters}')
for inv in inverters.values():
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):
found = True
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
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
if not found:
self.db.set_db_def_value(Register.COLLECTOR_SNR, snr)
self.db.set_db_def_value(Register.SERIAL_NUMBER, key)
break
else:
self.node_id = ''
self.sug_area = ''
if 'allow_all' not in inverters or not inverters['allow_all']:
self.inc_counter('Unknown_SNR')
self.unique_id = None
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
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.debug(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
def read(self) -> None:
self._read()
if not self.header_valid:
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
if self.header_valid and len(self._recv_buffer) >= (self.header_len +
self.data_len+2):
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
if callable(log_lvl):
log_lvl = log_lvl()
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
self._recv_buffer, self.header_len+self.data_len+2)
if self.__trailer_is_ok(self._recv_buffer, self.header_len
+ self.data_len + 2):
self.__set_serial_no(self.snr)
self.__dispatch_msg()
self.__flush_recv_msg()
return
def forward(self, buffer, buflen) -> None:
'''add the actual receive msg to the forwarding queue'''
if self.no_forwarding:
return
tsun = Config.get('solarman')
if tsun['enabled']:
self._forward_buffer = buffer[:buflen]
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
buffer, buflen)
self.ifc.fwd_add(buffer[:buflen])
self.ifc.fwd_log(logging.DEBUG, 'Store for forwarding:')
self.__parse_header(self._forward_buffer,
len(self._forward_buffer))
fnc = self.switch.get(self.control, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'forwrd') +
_, _str = self.get_fnc_handler(self.control)
logger.info(self._flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.control):#04x}'
f' Msg: {fnc.__name__!r}')
return
f' Msg: {_str}')
def _init_new_client_conn(self) -> bool:
# self.__build_header(0x91)
# self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
# f'{len(contact_mail)+1}p',
# contact_name, contact_mail)
# self.__finish_send_msg()
return False
'''
Our private methods
'''
def __flow_str(self, server_side: bool, type: str): # noqa: F821
switch = {
'rx': ' <',
'tx': ' >',
'forwrd': '<< ',
'drop': ' xx',
'rxS': '> ',
'txS': '< ',
'forwrdS': ' >>',
'dropS': 'xx ',
}
if server_side:
type += 'S'
return switch.get(type, '???')
def _timestamp(self):
# utc as epoche
return int(time.time()) # pragma: no cover
def _heartbeat(self) -> int:
return 60 # pragma: no cover
def __parse_header(self, buf: bytes, buf_len: int) -> None:
if (buf_len < self.header_len): # enough bytes for complete header?
return
result = struct.unpack_from('<BHHHL', buf, 0)
# store parsed header values in the class
start = result[0] # start byte
self.data_len = result[1] # len of variable id string
self.control = result[2]
self.seq.set_recv(result[3])
self.snr = result[4]
if start != 0xA5:
self.inc_counter('Invalid_Msg_Format')
# erase broken recv buffer
self._recv_buffer = bytearray()
return
self.header_valid = True
return
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
crc = buf[self.data_len+11]
stop = buf[self.data_len+12]
if stop != 0x15:
self.inc_counter('Invalid_Msg_Format')
if len(self._recv_buffer) > (self.data_len+13):
next_start = buf[self.data_len+13]
if next_start != 0xa5:
# erase broken recv buffer
self._recv_buffer = bytearray()
return False
check = sum(buf[1:buf_len-2]) & 0xff
if check != crc:
self.inc_counter('Invalid_Msg_Format')
logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}'
f' Stop:{int(stop):#02x}')
# start & stop byte are valid, discard only this message
return False
return True
def __build_header(self, ctrl) -> None:
'''build header for new transmit message'''
self.send_msg_ofs = len(self._send_buffer)
self._send_buffer += struct.pack(
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr)
fnc = self.switch.get(ctrl, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'tx') +
f' Ctl: {int(ctrl):#04x} Msg: {fnc.__name__!r}')
def __finish_send_msg(self) -> None:
'''finish the transmit message, set lenght and checksum'''
_len = len(self._send_buffer) - self.send_msg_ofs
struct.pack_into('<H', self._send_buffer, self.send_msg_ofs+1, _len-11)
check = sum(self._send_buffer[self.send_msg_ofs+1:self.send_msg_ofs +
_len]) & 0xff
self._send_buffer += struct.pack('<BB', check, 0x15) # crc & stop
def _update_header(self, _forward_buffer):
'''update header for message before forwarding,
set sequence and checksum'''
_len = len(_forward_buffer)
struct.pack_into('<H', _forward_buffer, 1,
_len-13)
struct.pack_into('<H', _forward_buffer, 5,
self.seq.get_send())
check = sum(_forward_buffer[1:_len-2]) & 0xff
struct.pack_into('<B', _forward_buffer, _len-2, check)
def __dispatch_msg(self) -> None:
fnc = self.switch.get(self.control, self.msg_unknown)
if self.unique_id:
logger.info(self.__flow_str(self.server_side, 'rx') +
f' Ctl: {int(self.control):#04x}' +
f' Msg: {fnc.__name__!r}')
fnc()
else:
logger.info(self.__flow_str(self.server_side, 'drop') +
f' Ctl: {int(self.control):#04x}' +
f' Msg: {fnc.__name__!r}')
def __flush_recv_msg(self) -> None:
self._recv_buffer = self._recv_buffer[(self.header_len +
self.data_len+2):]
self.header_valid = False
def __send_ack_rsp(self, msgtype, ftype, ack=1):
self.__build_header(msgtype)
self._send_buffer += struct.pack('<BBLL', ftype, ack,
self._timestamp(),
self._heartbeat())
self.__finish_send_msg()
self._build_header(msgtype)
self.ifc.tx_add(struct.pack('<BBLL', ftype, ack,
self._timestamp(),
self._heartbeat()))
self._finish_send_msg()
def send_modbus_cb(self, pdu: bytearray, log_lvl: int, state: str):
if self.state != self.STATE_UP:
logger.warn(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore MODBUS cmd,'
' cause the state is not UP anymore')
return
self.__build_header(0x4510)
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
0x2b0, 0, 0, 0)
self._send_buffer += pdu
self.__finish_send_msg()
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
self._send_buffer, len(self._send_buffer))
self.writer.write(self._send_buffer)
self._send_buffer = bytearray(0) # self._send_buffer[sent:]
self._build_header(0x4510)
self.ifc.tx_add(struct.pack('<BHLLL', self.MB_RTU_CMD,
self.sensor_list, 0, 0, 0))
self.ifc.tx_add(pdu)
self._finish_send_msg()
self.ifc.tx_log(log_lvl, f'Send Modbus {state}:{self.addr}:')
self.ifc.tx_flush()
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
if self.state != self.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)
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)
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 \
cmd.startswith(tuple(self.at_acl[connection]['block']))
async def send_at_cmd(self, AT_cmd: str) -> None:
if self.state != self.STATE_UP:
logger.warn(f'[{self.node_id}] ignore AT+ cmd,'
' as the state is not UP')
async def send_at_cmd(self, at_cmd: str) -> None:
if self.state != State.up:
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
' as the state is not UP')
return
AT_cmd = AT_cmd.strip()
at_cmd = at_cmd.strip()
if self.at_cmd_forbidden(cmd=AT_cmd, connection='mqtt'):
data_json = f'\'{AT_cmd}\' is forbidden'
if self.at_cmd_forbidden(cmd=at_cmd, connection='mqtt'):
data_json = f'\'{at_cmd}\' is forbidden'
node_id = self.node_id
key = 'at_resp'
logger.info(f'{key}: {data_json}')
asyncio.ensure_future(
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json)) # noqa: E501
await Proxy.mqtt.publish(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
return
self.forward_at_cmd_resp = False
self.__build_header(0x4510)
self._send_buffer += struct.pack(f'<BHLLL{len(AT_cmd)}sc', self.AT_CMD,
2, 0, 0, 0, AT_cmd.encode('utf-8'),
b'\r')
self.__finish_send_msg()
self.inverter.forward_at_cmd_resp = False
self._build_header(0x4510)
self.ifc.tx_add(struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
0x0002, 0, 0, 0,
at_cmd.encode('utf-8'), b'\r'))
self._finish_send_msg()
self.ifc.tx_log(logging.INFO, 'Send AT Command:')
try:
await self.async_write('Send AT Command:')
self.ifc.tx_flush()
except Exception:
self._send_buffer = bytearray(0)
self.ifc.tx_clear()
def __forward_msg(self):
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2)
def __build_model_name(self):
db = self.db
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
Rated = db.get_db_value(Register.RATED_POWER, 0)
Model = None
if MaxPow == 2000:
if Rated == 800 or Rated == 600:
Model = f'TSOL-MS{MaxPow}({Rated})'
max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
rated = db.get_db_value(Register.RATED_POWER, 0)
model = None
if max_pow == 2000:
if rated == 800 or rated == 600:
model = f'TSOL-MS{max_pow}({rated})'
else:
Model = f'TSOL-MS{MaxPow}'
elif MaxPow == 1800 or MaxPow == 1600:
Model = f'TSOL-MS{MaxPow}'
if Model:
logger.info(f'Model: {Model}')
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
model = f'TSOL-MS{max_pow}'
elif max_pow == 1800 or max_pow == 1600:
model = f'TSOL-MS{max_pow}'
if model:
logger.info(f'Model: {model}')
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
def __process_data(self, ftype):
def __process_data(self, ftype, ts, sensor=0):
inv_update = False
msg_type = self.control >> 8
for key, update in self.db.parse(self._recv_buffer, 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
self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
if inv_update:
@@ -430,46 +573,57 @@ class SolarmanV5(Message):
self.__forward_msg()
def msg_dev_ind(self):
data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0)
data = self.ifc.rx_peek()[self.header_len:]
result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0] # always 2
# total = result[1]
total = result[1]
tim = result[2]
res = result[3] # always zero
logger.info(f'frame type:{ftype:02x}'
f' timer:{tim:08x}s null:{res}')
# if self.time_ofs:
# dt = datetime.fromtimestamp(total + self.time_ofs)
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
self.__process_data(ftype)
if self.time_ofs:
# dt = datetime.fromtimestamp(total + self.time_ofs)
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
ts = total + self.time_ofs
else:
ts = None
self.__process_data(ftype, ts)
self.sensor_list = int(self.db.get_db_value(Register.SENSOR_LIST, 0),
16)
self.__forward_msg()
self.__send_ack_rsp(0x1110, ftype)
def msg_data_ind(self):
data = self._recv_buffer
data = self.ifc.rx_peek()
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
ftype = result[0] # 1 or 0x81
# total = result[2]
sensor = result[1]
total = result[2]
tim = result[3]
if 1 == ftype:
self.time_ofs = result[4]
unkn = result[5]
cnt = result[6]
if sensor != self.sensor_list:
logger.warning(f'Unexpected Sensor-List:{sensor:04x}'
f' (!={self.sensor_list:04x})')
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
f' ??: {unkn:04x} cnt:{cnt}')
# if self.time_ofs:
# dt = datetime.fromtimestamp(total + self.time_ofs)
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
if self.time_ofs:
# dt = datetime.fromtimestamp(total + self.time_ofs)
# logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
ts = total + self.time_ofs
else:
ts = None
self.__process_data(ftype)
self.__process_data(ftype, ts, sensor)
self.__forward_msg()
self.__send_ack_rsp(0x1210, ftype)
self.state = self.STATE_UP
self.new_state_up()
def msg_sync_start(self):
data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0)
data = self.ifc.rx_peek()[self.header_len:]
result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0]
total = result[1]
self.time_ofs = result[3]
@@ -481,90 +635,112 @@ class SolarmanV5(Message):
self.__send_ack_rsp(0x1310, ftype)
def msg_command_req(self):
data = self._recv_buffer[self.header_len:
self.header_len+self.data_len]
data = self.ifc.rx_peek()[self.header_len:
self.header_len+self.data_len]
result = struct.unpack_from('<B', data, 0)
ftype = result[0]
if ftype == self.AT_CMD:
AT_cmd = data[15:].decode()
if self.at_cmd_forbidden(cmd=AT_cmd, connection='tsun'):
at_cmd = data[15:].decode()
if self.at_cmd_forbidden(cmd=at_cmd, connection='tsun'):
self.inc_counter('AT_Command_Blocked')
return
self.inc_counter('AT_Command')
self.forward_at_cmd_resp = True
self.inverter.forward_at_cmd_resp = True
elif ftype == self.MB_RTU_CMD:
if self.remoteStream.mb.recv_req(data[15:],
self.__forward_msg()):
rstream = self.ifc.remote.stream
if rstream.mb.recv_req(data[15:],
rstream.__forward_msg):
self.inc_counter('Modbus_Command')
else:
logger.error('Invalid Modbus Msg')
self.inc_counter('Invalid_Msg_Format')
return
self.__forward_msg()
async def publish_mqtt(self, key, data):
await self.mqtt.publish(key, data) # pragma: no cover
def publish_mqtt(self, key, data): # pragma: no cover
asyncio.ensure_future(
Proxy.mqtt.publish(key, data))
def get_cmd_rsp_log_lvl(self) -> int:
ftype = self._recv_buffer[self.header_len]
if ftype == self.AT_CMD:
if self.forward_at_cmd_resp:
ftype = self.ifc.rx_peek()[self.header_len]
if ftype == self.AT_CMD or \
ftype == self.AT_CMD_RSP:
if self.inverter.forward_at_cmd_resp:
return logging.INFO
return logging.DEBUG
elif ftype == self.MB_RTU_CMD:
if self.server_side:
return self.mb.last_log_lvl
elif ftype == self.MB_RTU_CMD \
and self.server_side:
return self.mb.last_log_lvl
return logging.WARNING
def msg_command_rsp(self):
data = self._recv_buffer[self.header_len:
self.header_len+self.data_len]
data = self.ifc.rx_peek()[self.header_len:
self.header_len+self.data_len]
ftype = data[0]
if ftype == self.AT_CMD:
if not self.forward_at_cmd_resp:
if ftype == self.AT_CMD or \
ftype == self.AT_CMD_RSP:
if not self.inverter.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}')
asyncio.ensure_future(
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:
valid = data[1]
modbus_msg_len = self.data_len - 14
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
if valid == 1 and modbus_msg_len > 4:
# logger.info(f'first byte modbus:{data[14]}')
inv_update = False
self.modbus_elms = 0
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
self.node_id):
self.modbus_elms += 1
if update:
if key == 'inverter':
inv_update = True
self.new_data[key] = True
if inv_update:
self.__build_model_name()
self.__modbus_command_rsp(data)
return
self.__forward_msg()
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, 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):
'''precess MODBUS RTU response'''
valid = data[1]
modbus_msg_len = self.data_len - 14
# 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, modbus_msg_len)
if inv_update:
self.__build_model_name()
if self.establish_inv_emu and not self.ifc.remote.stream:
self.establish_emu()
def msg_hbeat_ind(self):
data = self._recv_buffer[self.header_len:]
data = self.ifc.rx_peek()[self.header_len:]
result = struct.unpack_from('<B', data, 0)
ftype = result[0]
self.__forward_msg()
self.__send_ack_rsp(0x1710, ftype)
self.state = self.STATE_UP
self.new_state_up()
def msg_sync_end(self):
data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0)
data = self.ifc.rx_peek()[self.header_len:]
result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0]
total = result[1]
self.time_ofs = result[3]
@@ -574,16 +750,3 @@ class SolarmanV5(Message):
self.__forward_msg()
self.__send_ack_rsp(0x1810, ftype)
def msg_response(self):
data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BBLL', data, 0)
ftype = result[0] # always 2
valid = result[1] == 1 # status
ts = result[2]
set_hb = result[3] # always 60 or 120
logger.debug(f'ftype:{ftype} accepted:{valid}'
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
dt = datetime.fromtimestamp(ts)
logger.debug(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')

File diff suppressed because it is too large Load Diff

199
app/src/inverter_base.py Normal file
View File

@@ -0,0 +1,199 @@
import weakref
import asyncio
import logging
import traceback
import json
import gc
from aiomqtt import MqttCodeError
from asyncio import StreamReader, StreamWriter
from ipaddress import ip_address
from inverter_ifc import InverterIfc
from proxy import Proxy
from async_stream import StreamPtr
from async_stream import AsyncStreamClient
from async_stream import AsyncStreamServer
from cnf.config import Config
from infos import Infos
logger_mqtt = logging.getLogger('mqtt')
class InverterBase(InverterIfc, Proxy):
def __init__(self, reader: StreamReader, writer: StreamWriter,
config_id: str, prot_class,
client_mode: bool = False,
remote_prot_class=None):
Proxy.__init__(self)
self._registry.append(weakref.ref(self))
self.addr = writer.get_extra_info('peername')
self.client_mode = client_mode
self.config_id = config_id
if remote_prot_class:
self.prot_class = remote_prot_class
self.use_emulation = True
else:
self.prot_class = prot_class
self.use_emulation = False
self.__ha_restarts = -1
self.remote = StreamPtr(None)
ifc = AsyncStreamServer(reader, writer,
self.async_publ_mqtt,
self.create_remote,
self.remote)
self.local = StreamPtr(
prot_class(self, self.addr, ifc, True, client_mode), ifc
)
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb) -> None:
logging.debug(f'InverterBase.__exit__() {self.addr}')
self.__del_remote()
self.local.stream.close()
self.local.stream = None
self.local.ifc.close()
self.local.ifc = None
# now explicitly call garbage collector to release unreachable objects
unreachable_obj = gc.collect()
logging.debug(
f'InverterBase.__exit: freed unreachable obj: {unreachable_obj}')
def __del_remote(self):
if self.remote.stream:
self.remote.stream.close()
self.remote.stream = None
if self.remote.ifc:
self.remote.ifc.close()
self.remote.ifc = None
async def disc(self, shutdown_started=False) -> None:
if self.remote.stream:
self.remote.stream.shutdown_started = shutdown_started
if self.remote.ifc:
await self.remote.ifc.disc()
if self.local.stream:
self.local.stream.shutdown_started = shutdown_started
if self.local.ifc:
await self.local.ifc.disc()
def healthy(self) -> bool:
logging.debug('InverterBase healthy()')
if self.local.ifc and not self.local.ifc.healthy():
return False
if self.remote.ifc and not self.remote.ifc.healthy():
return False
return True
async def create_remote(self) -> None:
'''Establish a client connection to the TSUN cloud'''
tsun = Config.get(self.config_id)
host = tsun['host']
port = tsun['port']
addr = (host, port)
stream = self.local.stream
try:
logging.info(f'[{stream.node_id}] Connect to {addr}')
connect = asyncio.open_connection(host, port)
reader, writer = await connect
r_addr = writer.get_extra_info('peername')
if r_addr is not None:
(ip, _) = r_addr
if ip_address(ip).is_private:
logging.error(
f"""resolve {host} to {ip}, which is a private IP!
\u001B[31m Check your DNS settings and use a public DNS resolver!
To prevent a possible loop, forwarding to local IP addresses is
not supported and is deactivated for subsequent connections
\u001B[0m
""")
Config.act_config[self.config_id]['enabled'] = False
ifc = AsyncStreamClient(
reader, writer, self.local,
self.__del_remote, self.use_emulation)
self.remote.ifc = ifc
if hasattr(stream, 'id_str'):
self.remote.stream = self.prot_class(
self, addr, ifc, server_side=False,
client_mode=False, id_str=stream.id_str)
else:
self.remote.stream = self.prot_class(
self, addr, ifc, server_side=False,
client_mode=False)
logging.info(f'[{self.remote.stream.node_id}:'
f'{self.remote.stream.conn_no}] '
f'Connected to {addr}')
asyncio.create_task(self.remote.ifc.client_loop(addr))
except (ConnectionRefusedError, TimeoutError) as error:
logging.info(f'{error}')
except Exception:
Infos.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception for {addr}:\n"
f"{traceback.format_exc()}")
async def async_publ_mqtt(self) -> None:
'''publish data to MQTT broker'''
stream = self.local.stream
if not stream or not stream.unique_id:
return
# check if new inverter or collector infos are available or when the
# home assistant has changed the status back to online
try:
if (('inverter' in 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):
await self._register_proxy_stat_home_assistant()
await self.__register_home_assistant(stream)
self.__ha_restarts = self.mqtt.ha_restarts
for key in stream.new_data:
await self.__async_publ_mqtt_packet(stream, key)
for key in Infos.new_stat_data:
await Proxy._async_publ_mqtt_proxy_stat(key)
except MqttCodeError as error:
logging.error(f'Mqtt except: {error}')
except Exception:
Infos.inc_counter('SW_Exception')
logging.error(
f"Inverter: Exception:\n"
f"{traceback.format_exc()}")
async def __async_publ_mqtt_packet(self, stream, key):
db = stream.db.db
if key in db and stream.new_data[key]:
data_json = json.dumps(db[key])
node_id = stream.node_id
logger_mqtt.debug(f'{key}: {data_json}')
await self.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
stream.new_data[key] = False
async def __register_home_assistant(self, stream) -> None:
'''register all our topics at home assistant'''
for data_json, component, node_id, id in stream.db.ha_confs(
self.entity_prfx, stream.node_id, stream.unique_id,
stream.sug_area):
logger_mqtt.debug(f"MQTT Register: cmp:'{component}'"
f" node_id:'{node_id}' {data_json}")
await self.mqtt.publish(f"{self.discovery_prfx}{component}"
f"/{node_id}{id}/config", data_json)
stream.db.reg_clr_at_midnight(f'{self.entity_prfx}{stream.node_id}')

37
app/src/inverter_ifc.py Normal file
View File

@@ -0,0 +1,37 @@
from abc import abstractmethod
import logging
from asyncio import StreamReader, StreamWriter
from iter_registry import AbstractIterMeta
logger_mqtt = logging.getLogger('mqtt')
class InverterIfc(metaclass=AbstractIterMeta):
_registry = []
@abstractmethod
def __init__(self, reader: StreamReader, writer: StreamWriter,
config_id: str, prot_class,
client_mode: bool):
pass # pragma: no cover
@abstractmethod
def __enter__(self):
pass # pragma: no cover
@abstractmethod
def __exit__(self, exc_type, exc, tb):
pass # pragma: no cover
@abstractmethod
def healthy(self) -> bool:
pass # pragma: no cover
@abstractmethod
async def disc(self, shutdown_started=False) -> None:
pass # pragma: no cover
@abstractmethod
async def create_remote(self) -> None:
pass # pragma: no cover

9
app/src/iter_registry.py Normal file
View File

@@ -0,0 +1,9 @@
from abc import ABCMeta
class AbstractIterMeta(ABCMeta):
def __iter__(cls):
for ref in cls._registry:
obj = ref()
if obj is not None:
yield obj

View File

@@ -1,16 +1,15 @@
[loggers]
keys=root,tracer,mesg,conn,data,mqtt
keys=root,tracer,mesg,conn,data,mqtt,asyncio,hypercorn_access,hypercorn_error
[handlers]
keys=console_handler,file_handler_name1,file_handler_name2
keys=console_handler,file_handler_name1,file_handler_name2,file_handler_name3,dashboard
[formatters]
keys=console_formatter,file_formatter
[logger_root]
level=DEBUG
handlers=console_handler,file_handler_name1
handlers=console_handler,file_handler_name1,dashboard
[logger_conn]
level=DEBUG
@@ -20,10 +19,16 @@ qualname=conn
[logger_mqtt]
level=INFO
handlers=console_handler,file_handler_name1
handlers=console_handler,file_handler_name1,dashboard
propagate=0
qualname=mqtt
[logger_asyncio]
level=INFO
handlers=console_handler,file_handler_name1,dashboard
propagate=0
qualname=asyncio
[logger_data]
level=DEBUG
handlers=file_handler_name1
@@ -43,6 +48,18 @@ handlers=file_handler_name2
propagate=0
qualname=tracer
[logger_hypercorn_access]
level=INFO
handlers=file_handler_name3
propagate=0
qualname=hypercorn.access
[logger_hypercorn_error]
level=INFO
handlers=file_handler_name1,dashboard
propagate=0
qualname=hypercorn.error
[handler_console_handler]
class=StreamHandler
level=DEBUG
@@ -52,19 +69,29 @@ formatter=console_formatter
class=handlers.TimedRotatingFileHandler
level=INFO
formatter=file_formatter
args=('log/proxy.log', when:='midnight')
args=(handlers.log_path + 'proxy.log', when:='midnight', backupCount:=handlers.log_backups)
[handler_file_handler_name2]
class=handlers.TimedRotatingFileHandler
level=NOTSET
formatter=file_formatter
args=('log/trace.log', when:='midnight')
args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups)
[handler_file_handler_name3]
class=handlers.TimedRotatingFileHandler
level=NOTSET
formatter=file_formatter
args=(handlers.log_path + 'access.log', when:='midnight', backupCount:=handlers.log_backups)
[handler_dashboard]
level=WARNING
class=web.log_handler.LogHandler
[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

@@ -1,84 +1,137 @@
import logging
import weakref
from typing import Callable
from enum import Enum
if __name__ == "app.src.messages":
from app.src.infos import Infos
from app.src.modbus import Modbus
else: # pragma: no cover
from infos import Infos
from modbus import Modbus
from async_ifc import AsyncIfc
from protocol_ifc import ProtocolIfc
from infos import Infos, Register
from modbus import Modbus
from my_timer import Timer
logger = logging.getLogger('msg')
def hex_dump_memory(level, info, data, num):
def __hex_val(n, data, data_len):
line = ''
for j in range(n-16, n):
if j >= data_len:
break
line += '%02x ' % abs(data[j])
return line
def __asc_val(n, data, data_len):
line = ''
for j in range(n-16, n):
if j >= data_len:
break
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
line += '%c' % c
return line
def hex_dump(data, data_len) -> list:
n = 0
lines = []
for i in range(0, data_len, 16):
line = ' '
line += '%04x | ' % (i)
n += 16
line += __hex_val(n, data, data_len)
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
line += __asc_val(n, data, data_len)
lines.append(line)
return lines
def hex_dump_str(data, data_len):
lines = hex_dump(data, data_len)
return '\n'.join(lines)
def hex_dump_memory(level, info, data, data_len):
lines = []
lines.append(info)
tracer = logging.getLogger('tracer')
if not tracer.isEnabledFor(level):
return
for i in range(0, num, 16):
line = ' '
line += '%04x | ' % (i)
n += 16
for j in range(n-16, n):
if j >= len(data):
break
line += '%02x ' % abs(data[j])
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
for j in range(n-16, n):
if j >= len(data):
break
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
line += '%c' % c
lines.append(line)
lines += hex_dump(data, data_len)
tracer.log(level, '\n'.join(lines))
class IterRegistry(type):
def __iter__(cls):
for ref in cls._registry:
obj = ref()
if obj is not None:
yield obj
class State(Enum):
'''state of the logical connection'''
init = 0
'''just created'''
received = 1
'''at least one packet received'''
up = 2
'''at least one cmd-rsp transaction'''
pend = 3
'''inverter transaction pending, don't send MODBUS cmds'''
closed = 4
'''connection closed'''
class Message(metaclass=IterRegistry):
_registry = []
STATE_INIT = 0
STATE_UP = 2
STATE_CLOSED = 3
class Message(ProtocolIfc):
MAX_START_TIME = 400
'''maximum time without a received msg in sec'''
MAX_INV_IDLE_TIME = 120
'''maximum time without a received msg from the inverter in sec'''
MAX_DEF_IDLE_TIME = 360
'''maximum default time without a received msg in sec'''
MB_START_TIMEOUT = 40
'''start delay for Modbus polling in server mode'''
MB_REGULAR_TIMEOUT = 60
'''regular Modbus polling time in server mode'''
def __init__(self, server_side: bool, send_modbus_cb:
Callable[[bytes, int, str], None], mb_timeout):
def __init__(self, node_id, ifc: "AsyncIfc", server_side: bool,
send_modbus_cb: Callable[[bytes, int, str], None],
mb_timeout: int):
self._registry.append(weakref.ref(self))
self.server_side = server_side
self.ifc = ifc
self.node_id = node_id
if server_side:
self.mb = Modbus(send_modbus_cb, mb_timeout)
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
else:
self.mb = None
self.mb_timer = None
self.header_valid = False
self.header_len = 0
self.data_len = 0
self.unique_id = 0
self.node_id = '' # will be overwritten in the child class's __init__
self.inv_serial = ''
self.sug_area = ''
self._recv_buffer = bytearray(0)
self._send_buffer = bytearray(0)
self._forward_buffer = bytearray(0)
self.new_data = {}
self.state = self.STATE_INIT
self.state = State.init
self.shutdown_started = False
self.modbus_elms = 0 # for unit tests
self.mb_timeout = self.MB_REGULAR_TIMEOUT
self.mb_first_timeout = self.MB_START_TIMEOUT
'''timer value for next Modbus polling request'''
self.modbus_polling = False
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):
return self._node_id
@node_id.setter
def node_id(self, value):
self._node_id = value
self.ifc.set_node_id(value)
'''
Empty methods, that have to be implemented in any child class which
@@ -88,18 +141,108 @@ class Message(metaclass=IterRegistry):
# to our _recv_buffer
return # pragma: no cover
def _update_header(self, _forward_buffer):
'''callback for updating the header of the forward buffer'''
return # pragma: no cover
def _set_config_parms(self, inv: dict, inv_serial: str):
'''init connection with params from the configuration'''
self.inv_serial = inv_serial
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]:
if key == 'grid':
info_id = Register.TS_GRID
elif key == 'input':
info_id = Register.TS_INPUT
elif key == 'total':
info_id = Register.TS_TOTAL
else:
return
# tstr = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts))
# logger.info(f'update: key: {key} ts:{tstr}'
self.db.set_db_def_value(info_id, round(ts))
def _timeout(self) -> int:
if self.state == State.init or self.state == State.received:
to = self.MAX_START_TIME
elif self.state == State.up and \
self.server_side and self.modbus_polling:
to = self.MAX_INV_IDLE_TIME
else:
to = self.MAX_DEF_IDLE_TIME
return to
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(dev_id, func, addr, val, log_lvl)
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
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
'''
def close(self) -> None:
if self.server_side:
# set inverter state to offline, if output power is very low
logging.debug('close power: '
f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
self.new_data['env'] = True
self.mb_timer.close()
self.state = State.closed
self.ifc.rx_set_cb(None)
self.ifc.prot_set_timeout_cb(None)
self.ifc.prot_set_init_new_client_conn_cb(None)
self.ifc.prot_set_update_header_cb(None)
self.ifc = None
if self.mb:
del self.mb
self.mb.close()
self.mb = None
pass # pragma: no cover
# pragma: no cover
def inc_counter(self, counter: str) -> None:
self.db.inc_counter(counter)

View File

@@ -16,10 +16,7 @@ import logging
import asyncio
from typing import Generator, Callable
if __name__ == "app.src.modbus":
from app.src.infos import Register
else: # pragma: no cover
from infos import Register
from infos import Register, Fmt
logger = logging.getLogger('data')
@@ -39,14 +36,69 @@ class Modbus():
'''Modbus function code: Write Single Register'''
__crc_tab = []
map = {
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
# 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
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_TOTAL_CHARG, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x000e: {'reg': Register.BATT_PV1_STATUS, 'fmt': '!H'}, # noqa: E501
0x000f: {'reg': Register.BATT_PV2_STATUS, '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_ALARM, 'fmt': '!h'}, # noqa: E501
0x002b: {'reg': Register.BATT_HW_VERS, 'fmt': '!h'}, # noqa: E501
0x002c: {'reg': Register.BATT_SW_VERS, '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'},
0x2006: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0x2008: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
0x2009: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
0x200a: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
0x2010: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
0x3001: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501
0x3002: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501
0x3003: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501
0x3004: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501
0x3005: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501
0x3006: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501
0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501
# 0x300d
0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
@@ -72,6 +124,7 @@ class Modbus():
0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
# 0x302a
}
def __init__(self, snd_handler: Callable[[bytes, int, str], None],
@@ -104,9 +157,19 @@ class Modbus():
self.loop = asyncio.get_event_loop()
self.req_pend = False
self.tim = None
self.node_id = ''
def __del__(self):
logging.debug(f'Modbus __del__:\n {self.counter}')
def close(self):
"""free the queue and erase the callback handlers"""
logging.debug('Modbus close:')
self.__stop_timer()
self.rsp_handler = None
self.snd_handler = None
while not self.que.empty():
self.que.get_nowait()
def set_node_id(self, node_id: str):
self.node_id = node_id
def build_msg(self, addr: int, func: int, reg: int, val: int,
log_lvl=logging.DEBUG) -> None:
@@ -126,7 +189,7 @@ class Modbus():
if self.que.qsize() == 1:
self.__send_next_from_que()
def recv_req(self, buf: bytearray,
def recv_req(self, buf: bytes,
rsp_handler: Callable[[None], None] = None) -> bool:
"""Add the received Modbus RTU request to the tx queue
@@ -151,14 +214,13 @@ class Modbus():
return True
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \
def recv_resp(self, info_db, buf: bytes) -> \
Generator[tuple[str, bool, int | float | str], None, None]:
"""Generator which check and parse a received MODBUS response.
Keyword arguments:
info_db: database for info lockups
buf: received Modbus RTU response frame
node_id: string for logging which identifies the slave
Returns on error and set Self.err to:
1: CRC error
@@ -168,58 +230,19 @@ class Modbus():
5: No MODBUS request pending
"""
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
if not self.req_pend:
self.err = 5
return
if not self.__check_crc(buf):
logger.error(f'[{node_id}] Modbus resp: CRC error')
self.err = 1
return
if buf[0] != self.last_addr:
logger.info(f'[{node_id}] Modbus resp: Wrong addr {buf[0]}')
self.err = 2
return
fcode = buf[1]
if fcode != self.last_fcode:
logger.info(f'[{node_id}] Modbus: Wrong fcode {fcode}'
f' != {self.last_fcode}')
self.err = 3
data_available = self.last_addr == self.INV_ADDR and \
(fcode == 3 or fcode == 4)
if self.__resp_error_check(buf, data_available):
return
if self.last_addr == self.INV_ADDR and \
(fcode == 3 or fcode == 4):
if data_available:
elmlen = buf[2] >> 1
if elmlen != self.last_len:
logger.info(f'[{node_id}] Modbus: len error {elmlen}'
f' != {self.last_len}')
self.err = 4
return
first_reg = self.last_reg # save last_reg before sending next pdu
self.__stop_timer() # stop timer and send next pdu
for i in range(0, elmlen):
addr = first_reg+i
if addr in self.map:
row = self.map[addr]
info_id = row['reg']
fmt = row['fmt']
val = struct.unpack_from(fmt, buf, 3+2*i)
result = val[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
keys, level, unit, must_incr = info_db._key_obj(info_id)
if keys:
name, update = info_db.update_db(keys, must_incr,
result)
yield keys[0], update, result
if update:
info_db.tracer.log(level,
f'[{node_id}] MODBUS: {name}'
f' : {result}{unit}')
yield from self.__process_data(info_db, buf, first_reg, elmlen)
else:
self.__stop_timer()
@@ -228,6 +251,53 @@ class Modbus():
self.rsp_handler()
self.__send_next_from_que()
def __resp_error_check(self, buf: bytes, data_available: bool) -> bool:
'''Check the MODBUS response for errors, returns True if one accure'''
if not self.req_pend:
self.err = 5
return True
if not self.__check_crc(buf):
logger.error(f'[{self.node_id}] Modbus resp: CRC error')
self.err = 1
return True
if buf[0] != self.last_addr:
logger.info(f'[{self.node_id}] Modbus resp: Wrong addr {buf[0]}')
self.err = 2
return True
fcode = buf[1]
if fcode != self.last_fcode:
logger.info(f'[{self.node_id}] Modbus: Wrong fcode {fcode}'
f' != {self.last_fcode}')
self.err = 3
return True
if data_available:
elmlen = buf[2] >> 1
if elmlen != self.last_len:
logger.info(f'[{self.node_id}] Modbus: len error {elmlen}'
f' != {self.last_len}')
self.err = 4
return True
return False
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
'''Generator over received registers, updates the db'''
for i in range(0, elmlen):
addr = first_reg+i
if addr in self.mb_reg_mapping:
row = self.mb_reg_mapping[addr]
info_id = row['reg']
keys, level, unit, must_incr = info_db._key_obj(info_id)
if keys:
result = Fmt.get_value(buf, 3+2*i, row)
name, update = info_db.update_db(keys, must_incr,
result)
yield keys[0], update, result
if update:
info_db.tracer.log(level,
f'[{self.node_id}] MODBUS: {name}'
f' : {result}{unit}')
'''
MODBUS response timer
'''
@@ -243,6 +313,7 @@ class Modbus():
# logging.debug(f'Modbus stop timer {self}')
if self.tim:
self.tim.cancel()
self.tim = None
def __timeout_cb(self) -> None:
'''Rsponse timeout handler retransmit pdu or send next pdu'''
@@ -254,7 +325,10 @@ class Modbus():
self.__start_timer()
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
else:
logger.info(f'Modbus timeout {self}')
logger.info(f'[{self.node_id}] Modbus timeout '
f'(FCode: {self.last_fcode} '
f'Reg: 0x{self.last_reg:04x}, '
f'{self.last_len})')
self.counter['timeouts'] += 1
self.__send_next_from_que()
@@ -283,11 +357,11 @@ class Modbus():
'''
Helper function for CRC-16 handling
'''
def __check_crc(self, msg: bytearray) -> bool:
def __check_crc(self, msg: bytes) -> bool:
'''Check CRC-16 and returns True if valid'''
return 0 == self.__calc_crc(msg)
def __calc_crc(self, buffer: bytearray) -> int:
def __calc_crc(self, buffer: bytes) -> int:
'''Build CRC-16 for buffer and returns it'''
crc = CRC_INIT

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

@@ -0,0 +1,92 @@
import logging
import traceback
import asyncio
from itertools import chain
from cnf.config import Config
from gen3plus.inverter_g3p import InverterG3P
from infos import Infos
logger = logging.getLogger('conn')
class ModbusConn():
def __init__(self, host, port):
self.host = host
self.port = port
self.addr = (host, port)
self.inverter = None
async def __aenter__(self) -> 'InverterG3P':
'''Establish a client connection to the TSUN cloud'''
connection = asyncio.open_connection(self.host, self.port)
reader, writer = await connection
self.inverter = InverterG3P(reader, writer,
client_mode=True)
self.inverter.__enter__()
stream = self.inverter.local.stream
logging.info(f'[{stream.node_id}:{stream.conn_no}] '
f'Connected to {self.addr}')
Infos.inc_counter('Inverter_Cnt')
Infos.inc_counter('ClientMode_Cnt')
await self.inverter.local.ifc.publish_outstanding_mqtt()
return self.inverter
async def __aexit__(self, exc_type, exc, tb):
Infos.dec_counter('ClientMode_Cnt')
Infos.dec_counter('Inverter_Cnt')
await self.inverter.local.ifc.publish_outstanding_mqtt()
self.inverter.__exit__(exc_type, exc, tb)
class ModbusTcp():
def __init__(self, loop, tim_restart=10) -> None:
self.tim_restart = tim_restart
inverters = Config.get('inverters')
batteries = Config.get('batteries')
# logging.info(f'Inverters: {inverters}')
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 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'],
client['forward']))
async def modbus_loop(self, host, port,
snr: int, forward: bool) -> None:
'''Loop for receiving messages from the TSUN cloud (client-side)'''
while True:
try:
async with ModbusConn(host, port) as inverter:
stream = inverter.local.stream
await stream.send_start_cmd(snr, host, forward)
await stream.ifc.loop()
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
f'Connection closed - Shutdown: '
f'{stream.shutdown_started}')
if stream.shutdown_started:
return
del inverter # decrease ref counter after the with block
except (ConnectionRefusedError, TimeoutError) as error:
logging.debug(f'Inv-conn:{error}')
except OSError as error:
if error.errno == 113: # pragma: no cover
logging.debug(f'os-error:{error}')
else:
logging.info(f'os-error: {error}')
except Exception:
logging.error(
f"ModbusTcpCreate: Exception for {(host, port)}:\n"
f"{traceback.format_exc()}")
await asyncio.sleep(self.tim_restart)

View File

@@ -2,26 +2,40 @@ import asyncio
import logging
import aiomqtt
import traceback
from modbus import Modbus
from messages import Message
from config import Config
from cnf.config import Config
from singleton import Singleton
from datetime import datetime
logger_mqtt = logging.getLogger('mqtt')
class Mqtt(metaclass=Singleton):
__client = None
__cb_MqttIsUp = None
__client: aiomqtt.Client = None
__cb_mqtt_is_up = None
ctime = None
published: int = 0
received: int = 0
def __init__(self, cb_MqttIsUp):
def __init__(self, cb_mqtt_is_up):
logger_mqtt.debug('MQTT: __init__')
if cb_MqttIsUp:
self.__cb_MqttIsUp = cb_MqttIsUp
if cb_mqtt_is_up:
self.__cb_mqtt_is_up = cb_mqtt_is_up
loop = asyncio.get_event_loop()
self.task = loop.create_task(self.__loop())
self.ha_restarts = 0
ha = Config.get('ha')
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
@property
def ha_restarts(self):
return self._ha_restarts
@@ -30,25 +44,23 @@ class Mqtt(metaclass=Singleton):
def ha_restarts(self, value):
self._ha_restarts = value
def __del__(self):
logger_mqtt.debug('MQTT: __del__')
async def close(self) -> None:
logger_mqtt.debug('MQTT: close')
self.task.cancel()
try:
await self.task
except Exception as e:
except (asyncio.CancelledError, Exception) as e:
logging.debug(f"Mqtt.close: exception: {e} ...")
async def publish(self, topic: str, payload: str | bytes | bytearray
| int | float | None = None) -> None:
if self.__client:
await self.__client.publish(topic, payload)
self.published += 1
async def __loop(self) -> None:
mqtt = Config.get('mqtt')
ha = Config.get('ha')
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
f'{mqtt["port"]} '
f'user:{mqtt["user"]}')
@@ -58,53 +70,31 @@ class Mqtt(metaclass=Singleton):
password=mqtt['passwd'])
interval = 5 # Seconds
ha_status_topic = f"{ha['auto_conf_prefix']}/status"
mb_rated_topic = "tsun/+/rated_load" # fixme
mb_reads_topic = "tsun/+/modbus_read_regs" # fixme
mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme
mb_at_cmd_topic = "tsun/+/at_cmd" # fixme
while True:
try:
async with self.__client:
logger_mqtt.info('MQTT broker connection established')
self.ctime = datetime.now()
self.published = 0
self.received = 0
if self.__cb_MqttIsUp:
await self.__cb_MqttIsUp()
if self.__cb_mqtt_is_up:
await self.__cb_mqtt_is_up()
# async with self.__client.messages() as messages:
await self.__client.subscribe(ha_status_topic)
await self.__client.subscribe(mb_rated_topic)
await self.__client.subscribe(mb_reads_topic)
await self.__client.subscribe(mb_inputs_topic)
await self.__client.subscribe(mb_at_cmd_topic)
await self.__client.subscribe(self.ha_status_topic)
await self.__client.subscribe(self.mb_rated_topic)
await self.__client.subscribe(self.mb_out_coeff_topic)
await self.__client.subscribe(self.mb_reads_topic)
await self.__client.subscribe(self.mb_inputs_topic)
await self.__client.subscribe(self.mb_at_cmd_topic)
async for message in self.__client.messages:
if message.topic.matches(ha_status_topic):
status = message.payload.decode("UTF-8")
logger_mqtt.info('Home-Assistant Status:'
f' {status}')
if status == 'online':
self.ha_restarts += 1
await self.__cb_MqttIsUp()
if message.topic.matches(mb_rated_topic):
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
1, 0x2008)
if message.topic.matches(mb_reads_topic):
await self.modbus_cmd(message,
Modbus.READ_REGS, 2)
if message.topic.matches(mb_inputs_topic):
await self.modbus_cmd(message,
Modbus.READ_INPUTS, 2)
if message.topic.matches(mb_at_cmd_topic):
await self.at_cmd(message)
await self.dispatch_msg(message)
except aiomqtt.MqttError:
self.ctime = None
if Config.is_default('mqtt'):
logger_mqtt.info(
"MQTT is unconfigured; Check your config.toml!")
@@ -122,50 +112,83 @@ class Mqtt(metaclass=Singleton):
return
except Exception:
# self.inc_counter('SW_Exception') # fixme
self.ctime = None
logger_mqtt.error(
f"Exception:\n"
f"{traceback.format_exc()}")
async def dispatch_msg(self, message):
self.received += 1
if message.topic.matches(self.ha_status_topic):
status = message.payload.decode("UTF-8")
logger_mqtt.info('Home-Assistant Status:'
f' {status}')
if status == 'online':
self.ha_restarts += 1
await self.__cb_mqtt_is_up()
if message.topic.matches(self.mb_rated_topic):
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
1, 0x2008)
if message.topic.matches(self.mb_out_coeff_topic):
payload = message.payload.decode("UTF-8")
try:
val = round(float(payload) * 1024/100)
if val < 0 or val > 1024:
logger_mqtt.error('out_coeff: value must be in'
'the range 0..100,'
f' got: {payload}')
else:
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
0, 0x202c, val)
except Exception:
pass
if message.topic.matches(self.mb_reads_topic):
await self.modbus_cmd(message,
Modbus.READ_REGS, 2)
if message.topic.matches(self.mb_inputs_topic):
await self.modbus_cmd(message,
Modbus.READ_INPUTS, 2)
if message.topic.matches(self.mb_at_cmd_topic):
await self.at_cmd(message)
def each_inverter(self, message, func_name: str):
topic = str(message.topic)
node_id = topic.split('/')[1] + '/'
found = False
for m in Message:
if m.server_side and (m.node_id == node_id):
found = True
logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, func_name, None)
if callable(fnc):
yield fnc
else:
logger_mqtt.warning(f'Cmd not supported by: {node_id}')
break
if not found:
else:
logger_mqtt.warning(f'Node_id: {node_id} not found')
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
topic = str(message.topic)
node_id = topic.split('/')[1] + '/'
# refactor into a loop over a table
payload = message.payload.decode("UTF-8")
logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}')
for m in Message:
if m.server_side and (m.node_id == node_id):
logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, "send_modbus_cmd", None)
res = payload.split(',')
if params != len(res):
logger_mqtt.error(f'Parameter expected: {params}, '
f'got: {len(res)}')
return
if callable(fnc):
if params == 1:
val = int(payload)
elif params == 2:
addr = int(res[0], base=16)
val = int(res[1]) # lenght
await fnc(func, addr, val, logging.INFO)
for fnc in self.each_inverter(message, "send_modbus_cmd"):
res = payload.split(',')
if params > 0 and params != len(res):
logger_mqtt.error(f'Parameter expected: {params}, '
f'got: {len(res)}')
return
if params == 1:
val = int(payload)
elif params == 2:
addr = int(res[0], base=16)
val = int(res[1]) # lenght
await fnc(func, addr, val, logging.INFO)
async def at_cmd(self, message):
payload = message.payload.decode("UTF-8")

35
app/src/my_timer.py Normal file
View File

@@ -0,0 +1,35 @@
import asyncio
import logging
from itertools import count
class Timer:
def __init__(self, cb, id_str: str = ''):
self.__timeout_cb = cb
self.loop = asyncio.get_event_loop()
self.tim = None
self.id_str = id_str
self.exp_count = count(0)
def start(self, timeout: float) -> None:
'''Start timer with timeout seconds'''
if self.tim:
self.tim.cancel()
self.tim = self.loop.call_later(timeout, self.__timeout)
logging.debug(f'[{self.id_str}]Start timer')
def stop(self) -> None:
'''Stop timer'''
logging.debug(f'[{self.id_str}]Stop timer')
if self.tim:
self.tim.cancel()
self.tim = None
def __timeout(self) -> None:
'''timer expired handler'''
logging.debug(f'[{self.id_str}]Timer expired')
self.__timeout_cb(next(self.exp_count))
def close(self) -> None:
self.stop()
self.__timeout_cb = None

17
app/src/protocol_ifc.py Normal file
View File

@@ -0,0 +1,17 @@
from abc import abstractmethod
from async_ifc import AsyncIfc
from iter_registry import AbstractIterMeta
class ProtocolIfc(metaclass=AbstractIterMeta):
_registry = []
@abstractmethod
def __init__(self, addr, ifc: "AsyncIfc", server_side: bool,
client_mode: bool = False, id_str=b''):
pass # pragma: no cover
@abstractmethod
def close(self):
pass # pragma: no cover

View File

@@ -1,18 +1,41 @@
import asyncio
import logging
import json
from config import Config
from itertools import chain
from cnf.config import Config
from mqtt import Mqtt
from infos import Infos
# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
class Inverter():
class Proxy():
'''class Proxy is a baseclass
The class has some class method for managing common resources like a
connection to the MQTT broker or proxy error counter which are common
for all inverter connection
Instances of the class are connections to an inverter and can have an
optional link to an remote connection to the TSUN cloud. A remote
connection dies with the inverter connection.
class methods:
class_init(): initialize the common resources of the proxy (MQTT
broker, Proxy DB, etc). Must be called before the
first inverter instance can be created
class_close(): release the common resources of the proxy. Should not
be called before any instances of the class are
destroyed
methods:
create_remote(): Establish a client connection to the TSUN cloud
async_publ_mqtt(): Publish data to MQTT broker
'''
@classmethod
def class_init(cls) -> None:
logging.debug('Inverter.class_init')
logging.debug('Proxy.class_init')
# initialize the proxy statistics
Infos.static_init()
cls.db_stat = Infos()
@@ -34,8 +57,9 @@ class Inverter():
# reset at midnight when you restart the proxy just before
# midnight!
inverters = Config.get('inverters')
# logger.debug(f'Inverters: {inverters}')
for inv in inverters.values():
batteries = Config.get('batteries')
# logger.debug(f'Proxys: {inverters}')
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}',
@@ -72,8 +96,8 @@ class Inverter():
Infos.new_stat_data[key] = False
@classmethod
def class_close(cls, loop) -> None:
logging.debug('Inverter.class_close')
async def class_close(cls, loop) -> None: # pragma: no cover
logging.debug('Proxy.class_close')
logging.info('Close MQTT Task')
loop.run_until_complete(cls.mqtt.close())
await cls.mqtt.close()
cls.mqtt = None

View File

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

View File

@@ -1,123 +1,305 @@
import logging
import asyncio
import signal
import os
import logging.handlers
from logging import config # noqa F401
from messages import Message
from inverter import Inverter
import asyncio
from asyncio import StreamReader, StreamWriter
import os
import argparse
from quart import Quart, Response
from cnf.config import Config
from cnf.config_read_env import ConfigReadEnv
from cnf.config_read_toml import ConfigReadToml
from cnf.config_read_json import ConfigReadJson
from web import Web
from web.wrapper import url_for
from proxy import Proxy
from inverter_ifc import InverterIfc
from gen3.inverter_g3 import InverterG3
from gen3plus.inverter_g3p import InverterG3P
from scheduler import Schedule
from config import Config
from modbus_tcp import ModbusTcp
async def handle_client(reader, writer):
class Server():
serv_name = ''
version = ''
src_dir = ''
####
# The following default values are used for the unit tests only, since
# `Server.parse_args()' will not be called during test setup.
# Ofcorse, we can call `Server.parse_args()' in a test case explicitly
# to overwrite this values
config_path = './config/'
json_config = ''
toml_config = ''
trans_path = '../translations/'
rel_urls = False
log_path = './log/'
log_backups = 0
log_level = None
def __init__(self, app, parse_args: bool):
''' Applikation Setup
1. Read cli arguments
2. Init the logging system by the ini file
3. Log the config parms
4. Set the log-levels
5. Read the build the config for the app
'''
self.serv_name = os.getenv('SERVICE_NAME', 'proxy')
self.version = os.getenv('VERSION', 'unknown')
self.src_dir = os.path.dirname(__file__) + '/'
if parse_args: # pragma: no cover
self.parse_args(None)
self.init_logging_system()
self.build_config()
@app.context_processor
def utility_processor():
return dict(version=self.version)
def parse_args(self, arg_list: list[str] | None):
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config_path', type=str,
default='./config/',
help='set path for the configuration files')
parser.add_argument('-j', '--json_config', type=str,
help='read user config from json-file')
parser.add_argument('-t', '--toml_config', type=str,
help='read user config from toml-file')
parser.add_argument('-l', '--log_path', type=str,
default='./log/',
help='set path for the logging files')
parser.add_argument('-b', '--log_backups', type=int,
default=0,
help='set max number of daily log-files')
parser.add_argument('-tr', '--trans_path', type=str,
default='../translations/',
help='set path for the translations files')
parser.add_argument('-r', '--rel_urls', action="store_true",
help='use relative dashboard urls')
args = parser.parse_args(arg_list)
self.config_path = args.config_path
self.json_config = args.json_config
self.toml_config = args.toml_config
self.trans_path = args.trans_path
self.rel_urls = args.rel_urls
self.log_path = args.log_path
self.log_backups = args.log_backups
def init_logging_system(self):
setattr(logging.handlers, "log_path", self.log_path)
setattr(logging.handlers, "log_backups", self.log_backups)
os.makedirs(self.log_path, exist_ok=True)
logging.config.fileConfig(self.src_dir + 'logging.ini')
logging.info(
f'Server "{self.serv_name} - {self.version}" will be started')
logging.info(f'current dir: {os.getcwd()}')
logging.info(f"config_path: {self.config_path}")
logging.info(f"json_config: {self.json_config}")
logging.info(f"toml_config: {self.toml_config}")
logging.info(f"trans_path: {self.trans_path}")
logging.info(f"rel_urls: {self.rel_urls}")
logging.info(f"log_path: {self.log_path}")
if self.log_backups == 0:
logging.info("log_backups: unlimited")
else:
logging.info(f"log_backups: {self.log_backups} days")
self.log_level = self.get_log_level()
logging.info('******')
if self.log_level:
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
logging.getLogger().setLevel(self.log_level)
logging.getLogger('msg').setLevel(self.log_level)
logging.getLogger('conn').setLevel(self.log_level)
logging.getLogger('data').setLevel(self.log_level)
logging.getLogger('tracer').setLevel(self.log_level)
logging.getLogger('asyncio').setLevel(self.log_level)
# logging.getLogger('mqtt').setLevel(self.log_level)
def build_config(self):
# read config file
Config.init(ConfigReadToml(self.src_dir + "cnf/default_config.toml"),
log_path=self.log_path)
ConfigReadEnv()
ConfigReadJson(self.config_path + "config.json")
ConfigReadToml(self.config_path + "config.toml")
ConfigReadJson(self.json_config)
ConfigReadToml(self.toml_config)
config_err = Config.get_error()
if config_err is not None:
logging.info(f'config_err: {config_err}')
return
logging.info('******')
def get_log_level(self) -> int | None:
'''checks if LOG_LVL is set in the environment and returns the
corresponding logging.LOG_LEVEL'''
switch = {
'DEBUG': logging.DEBUG,
'WARN': logging.WARNING,
'INFO': logging.INFO,
'ERROR': logging.ERROR,
}
log_lvl = os.getenv('LOG_LVL', None)
logging.info(f"LOG_LVL : {log_lvl}")
return switch.get(log_lvl, None)
class ProxyState:
_is_up = False
@staticmethod
def is_up() -> bool:
return ProxyState._is_up
@staticmethod
def set_up(value: bool):
ProxyState._is_up = value
class HypercornLogHndl:
access_hndl = []
error_hndl = []
must_fix = False
HYPERC_ERR = 'hypercorn.error'
HYPERC_ACC = 'hypercorn.access'
@classmethod
def save(cls):
cls.access_hndl = logging.getLogger(
cls.HYPERC_ACC).handlers
cls.error_hndl = logging.getLogger(
cls.HYPERC_ERR).handlers
cls.must_fix = True
@classmethod
def restore(cls):
if not cls.must_fix:
return
cls.must_fix = False
access_hndl = logging.getLogger(
cls.HYPERC_ACC).handlers
if access_hndl != cls.access_hndl:
print(' * Fix hypercorn.access setting')
logging.getLogger(
cls.HYPERC_ACC).handlers = cls.access_hndl
error_hndl = logging.getLogger(
cls.HYPERC_ERR).handlers
if error_hndl != cls.error_hndl:
print(' * Fix hypercorn.error setting')
logging.getLogger(
cls.HYPERC_ERR).handlers = cls.error_hndl
app = Quart(__name__,
template_folder='web/templates',
static_folder='web/static')
app.secret_key = 'JKLdks.dajlKKKdladkflKwolafallsdfl'
app.jinja_env.globals.update(url_for=url_for)
server = Server(app, __name__ == "__main__")
Web(app, server.trans_path, server.rel_urls)
@app.route('/-/ready')
async def ready():
if ProxyState.is_up():
status = 200
text = 'Is ready'
else:
status = 503
text = 'Not ready'
return Response(status=status, response=text)
@app.route('/-/healthy')
async def healthy():
if ProxyState.is_up():
# logging.info('web reqeust healthy()')
for inverter in InverterIfc:
try:
res = inverter.healthy()
if not res:
return Response(status=503, response="I have a problem")
except Exception as err:
logging.info(f'Exception:{err}')
return Response(status=200, response="I'm fine")
async def handle_client(reader: StreamReader,
writer: StreamWriter,
inv_class): # pragma: no cover
'''Handles a new incoming connection and starts an async loop'''
addr = writer.get_extra_info('peername')
await InverterG3(reader, writer, addr).server_loop(addr)
with inv_class(reader, writer) as inv:
await inv.local.ifc.server_loop()
async def handle_client_v2(reader, writer):
'''Handles a new incoming connection and starts an async loop'''
@app.before_serving
async def startup_app(): # pragma: no cover
HypercornLogHndl.save()
loop = asyncio.get_event_loop()
Proxy.class_init()
Schedule.start()
ModbusTcp(loop)
addr = writer.get_extra_info('peername')
await InverterG3P(reader, writer, addr).server_loop(addr)
for inv_class, port in [(InverterG3, 5005), (InverterG3P, 10000)]:
logging.info(f'listen on port: {port} for inverters')
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
handle_client(r, w, i),
'0.0.0.0', port))
ProxyState.set_up(True)
async def handle_shutdown(loop):
@app.before_request
async def startup_request():
HypercornLogHndl.restore()
@app.after_serving
async def handle_shutdown(): # pragma: no cover
'''Close all TCP connections and stop the event loop'''
logging.info('Shutdown due to SIGTERM')
loop = asyncio.get_event_loop()
ProxyState.set_up(False)
#
# first, disc all open TCP connections gracefully
#
for stream in Message:
try:
await asyncio.wait_for(stream.disc(), 2)
except Exception:
pass
logging.info('Disconnecting done')
for inverter in InverterIfc:
await inverter.disc(True)
#
# second, close all open TCP connections
#
for stream in Message:
stream.close()
logging.info('Proxy disconnecting done')
#
# at last, we stop the loop
#
loop.stop()
logging.info('Shutdown complete')
await Proxy.class_close(loop)
def get_log_level() -> int:
'''checks if LOG_LVL is set in the environment and returns the
corresponding logging.LOG_LEVEL'''
log_level = os.getenv('LOG_LVL', 'INFO')
if log_level == 'DEBUG':
log_level = logging.DEBUG
elif log_level == 'WARN':
log_level = logging.WARNING
else:
log_level = logging.INFO
return log_level
if __name__ == "__main__":
#
# Setup our daily, rotating logger
#
serv_name = os.getenv('SERVICE_NAME', 'proxy')
version = os.getenv('VERSION', 'unknown')
logging.config.fileConfig('logging.ini')
logging.info(f'Server "{serv_name} - {version}" will be started')
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
log_level = get_log_level()
logging.getLogger().setLevel(log_level)
logging.getLogger('msg').setLevel(log_level)
logging.getLogger('conn').setLevel(log_level)
logging.getLogger('data').setLevel(log_level)
logging.getLogger('tracer').setLevel(log_level)
# logging.getLogger('mqtt').setLevel(log_level)
# read config file
Config.class_init()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Inverter.class_init()
Schedule.start()
#
# Register some UNIX Signal handler for a gracefully server shutdown
# on Docker restart and stop
#
for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame),
lambda loop=loop: asyncio.create_task(
handle_shutdown(loop)))
#
# Create taska for our listening servera. These must be tasks! If we call
# start_server directly out of our main task, the eventloop will be blocked
# and we can't receive and handle the UNIX signals!
#
loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005))
loop.create_task(asyncio.start_server(handle_client_v2, '0.0.0.0', 10000))
if __name__ == "__main__": # pragma: no cover
try:
loop.run_forever()
logging.info("Start Quart")
app.run(host='0.0.0.0', port=8127, use_reloader=False,
debug=server.log_level == logging.DEBUG)
logging.info("Quart stopped")
except KeyboardInterrupt:
pass
except asyncio.exceptions.CancelledError:
logging.info("Quart cancelled")
finally:
Inverter.class_close(loop)
logging.info('Close event loop')
loop.close()
logging.info(f'Finally, exit Server "{serv_name}"')
logging.info(f'Finally, exit Server "{server.serv_name}"')

View File

@@ -1,9 +1,14 @@
from weakref import WeakValueDictionary
class Singleton(type):
_instances = {}
_instances = WeakValueDictionary()
def __call__(cls, *args, **kwargs):
# logger_mqtt.debug('singleton: __call__')
if cls not in cls._instances:
cls._instances[cls] = super(Singleton,
cls).__call__(*args, **kwargs)
instance = super(Singleton,
cls).__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]

25
app/src/utils/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
import mimetypes
from importlib import import_module
from pathlib import Path
from collections.abc import Callable
class SourceFileLoader:
""" Represents a SouceFileLoader (__loader__)"""
name: str
get_resource_reader: Callable
def load_modules(loader: SourceFileLoader):
"""Load the entire modules from a SourceFileLoader (__loader__)"""
pkg = loader.name
for load in loader.get_resource_reader().contents():
if "python" not in str(mimetypes.guess_type(load)[0]):
continue
mod = Path(load).stem
if mod == "__init__":
continue
import_module(pkg + "." + mod, pkg)

32
app/src/web/__init__.py Normal file
View File

@@ -0,0 +1,32 @@
'''Quart blueprint for the proxy webserver with the dashboard
Usage:
app = Quart(__name__, ...)
Web(app)
'''
from quart import Quart, Blueprint
from quart_babel import Babel
from utils import load_modules
web = Blueprint('web', __name__)
load_modules(__loader__)
class Web:
'''Helper Class to register the Blueprint at Quart and
initializing Babel'''
def __init__(self,
app: Quart,
translation_directories: str | list[str],
rel_urls: bool):
web.build_relative_urls = rel_urls
app.register_blueprint(web)
from .i18n import get_locale, get_tz
global babel
babel = Babel(
app,
locale_selector=get_locale,
timezone_selector=get_tz,
default_translation_directories=translation_directories)

87
app/src/web/conn_table.py Normal file
View File

@@ -0,0 +1,87 @@
from inverter_base import InverterBase
from quart import render_template
from quart_babel import format_datetime, _
from infos import Infos
from . import web
from .log_handler import LogHandler
def _get_device_icon(client_mode: bool):
'''returns the icon for the device conntection'''
if client_mode:
return 'fa-download fa-rotate-180'
return 'fa-upload fa-rotate-180'
def _get_cloud_icon(emu_mode: bool):
'''returns the icon for the cloud conntection'''
if emu_mode:
return 'fa-cloud-arrow-up-alt'
return 'fa-cloud'
def _get_row(inv: InverterBase):
'''build one row for the connection table'''
client_mode = inv.client_mode
inv_serial = inv.local.stream.inv_serial
icon1 = _get_device_icon(client_mode)
ip1, port1 = inv.addr
icon2 = ''
ip2 = '--'
port2 = '--'
if inv.remote.ifc:
ip2, port2 = inv.remote.ifc.r_addr
icon2 = _get_cloud_icon(client_mode)
row = []
row.append(f'<i class="fa {icon1}"></i> {ip1}:{port1}')
row.append(f'<i class="fa {icon1}"></i> {ip1}')
row.append(inv_serial)
row.append(f'<i class="fa {icon2}"></i> {ip2}:{port2}')
row.append(f'<i class="fa {icon2}"></i> {ip2}')
return row
def get_table_data():
'''build the connection table'''
table = {
"headline": _('Connections'),
"col_classes": [
"w3-hide-small w3-hide-medium", "w3-hide-large",
"",
"w3-hide-small w3-hide-medium", "w3-hide-large",
],
"thead": [[
_('Device-IP:Port'), _('Device-IP'),
_("Serial-No"),
_("Cloud-IP:Port"), _("Cloud-IP")
]],
"tbody": []
}
for inverter in InverterBase:
table['tbody'].append(_get_row(inverter))
return table
@web.route('/data-fetch')
async def data_fetch():
data = {
"update-time": format_datetime(format="medium"),
"server-cnt": f"<h3>{Infos.get_counter('ServerMode_Cnt')}</h3>",
"client-cnt": f"<h3>{Infos.get_counter('ClientMode_Cnt')}</h3>",
"proxy-cnt": f"<h3>{Infos.get_counter('ProxyMode_Cnt')}</h3>",
"emulation-cnt": f"<h3>{Infos.get_counter('EmuMode_Cnt')}</h3>",
}
data["conn-table"] = await render_template('templ_table.html.j2',
table=get_table_data())
data["notes-list"] = await render_template(
'templ_notes_list.html.j2',
notes=LogHandler().get_buffer(3),
hide_if_empty=True)
return data

37
app/src/web/favicon.py Normal file
View File

@@ -0,0 +1,37 @@
import os
from quart import send_from_directory
from . import web
async def get_icon(file: str, mime: str = 'image/png'):
return await send_from_directory(
os.path.join(web.root_path, 'static/images'),
file,
mimetype=mime)
@web.route('/favicon-96x96.png')
async def favicon():
return await get_icon('favicon-96x96.png')
@web.route('/favicon.ico')
async def favicon_ico():
return await get_icon('favicon.ico', 'image/x-icon')
@web.route('/favicon.svg')
async def favicon_svg():
return await get_icon('favicon.svg', 'image/svg+xml')
@web.route('/apple-touch-icon.png')
async def apple_touch():
return await get_icon('apple-touch-icon.png')
@web.route('/site.webmanifest')
async def webmanifest():
return await get_icon('site.webmanifest', 'application/manifest+json')

45
app/src/web/i18n.py Normal file
View File

@@ -0,0 +1,45 @@
from quart import request, session, redirect, abort
from quart_babel.locale import get_locale as babel_get_locale
from . import web
LANGUAGES = {
'en': 'English',
'de': 'Deutsch',
# 'fr': 'Français'
}
def get_locale():
try:
language = session['language']
except KeyError:
language = None
if language is not None:
return language
# check how to get the locale form for the add-on - hass.selectedLanguage
# logging.info("get_locale(%s)", request.accept_languages)
return request.accept_languages.best_match(LANGUAGES.keys())
def get_tz():
return 'CET'
@web.context_processor
def utility_processor():
return dict(lang=babel_get_locale(),
lang_str=LANGUAGES.get(str(babel_get_locale()), "English"),
languages=LANGUAGES)
@web.route('/language/<language>')
async def set_language(language=None):
if language in LANGUAGES:
session['language'] = language
rsp = redirect(request.referrer if request.referrer else '../#')
rsp.content_language = language
return rsp
return abort(404)

60
app/src/web/log_files.py Normal file
View File

@@ -0,0 +1,60 @@
from quart import render_template
from quart_babel import format_datetime, format_decimal
from quart.helpers import send_from_directory
from werkzeug.utils import secure_filename
from cnf.config import Config
import os
from . import web
def _get_file(file):
'''build one row for the connection table'''
entry = {}
entry['name'] = file.name
stat = file.stat()
entry['size'] = format_decimal(stat.st_size)
entry['date'] = stat.st_mtime
entry['created'] = format_datetime(stat.st_ctime, format="short")
entry['modified'] = format_datetime(stat.st_mtime, format="short")
return entry
def get_list_data():
'''build the connection table'''
file_list = []
with os.scandir(Config.get_log_path()) as it:
for entry in it:
if entry.is_file():
file_list.append(_get_file(entry))
file_list.sort(key=lambda x: x['date'], reverse=True)
return file_list
@web.route('/file-fetch')
async def file_fetch():
data = {
"update-time": format_datetime(format="medium"),
}
data["file-list"] = await render_template('templ_log_files_list.html.j2',
dir_list=get_list_data())
return data
@web.route('/send-file/<file>')
async def send(file):
return await send_from_directory(
directory=Config.get_log_path(),
file_name=secure_filename(file),
as_attachment=True)
@web.route('/del-file/<file>', methods=['DELETE'])
async def delete(file):
try:
os.remove(Config.get_log_path() + secure_filename(file))
except OSError:
return 'File not found', 404
return '', 204

View File

@@ -0,0 +1,24 @@
from logging import Handler
from logging import LogRecord
import logging
from collections import deque
from singleton import Singleton
class LogHandler(Handler, metaclass=Singleton):
def __init__(self, capacity=64):
super().__init__(logging.WARNING)
self.capacity = capacity
self.buffer = deque(maxlen=capacity)
def emit(self, record: LogRecord):
self.buffer.append({
'ctime': record.created,
'level': record.levelno,
'lname': record.levelname,
'msg': record.getMessage()
})
def get_buffer(self, elms=0) -> list:
return list(self.buffer)[-elms:]

64
app/src/web/mqtt_table.py Normal file
View File

@@ -0,0 +1,64 @@
from inverter_base import InverterBase
from quart import render_template
from quart_babel import format_datetime, _
from mqtt import Mqtt
from . import web
from .log_handler import LogHandler
def _get_row(inv: InverterBase):
'''build one row for the connection table'''
entity_prfx = inv.entity_prfx
inv_serial = inv.local.stream.inv_serial
node_id = inv.local.stream.node_id
sug_area = inv.local.stream.sug_area
row = []
row.append(inv_serial)
row.append(entity_prfx+node_id)
row.append(sug_area)
return row
def get_table_data():
'''build the connection table'''
table = {
"headline": _('MQTT devices'),
"col_classes": [
"",
"",
"",
],
"thead": [[
_("Serial-No"),
_('Node-ID'),
_('HA-Area'),
]],
"tbody": []
}
for inverter in InverterBase:
table['tbody'].append(_get_row(inverter))
return table
@web.route('/mqtt-fetch')
async def mqtt_fetch():
mqtt = Mqtt(None)
ctime = format_datetime(dt=mqtt.ctime, format='short')
data = {
"update-time": format_datetime(format="medium"),
"mqtt-ctime": f"<h3>{ctime}</h3>",
"mqtt-tx": f"<h3>{mqtt.published}</h3>",
"mqtt-rx": f"<h3>{mqtt.received}</h3>",
}
data["mqtt-table"] = await render_template('templ_table.html.j2',
table=get_table_data())
data["notes-list"] = await render_template(
'templ_notes_list.html.j2',
notes=LogHandler().get_buffer(3),
hide_if_empty=True)
return data

19
app/src/web/notes_list.py Normal file
View File

@@ -0,0 +1,19 @@
from quart import render_template
from quart_babel import format_datetime
from . import web
from .log_handler import LogHandler
@web.route('/notes-fetch')
async def notes_fetch():
data = {
"update-time": format_datetime(format="medium"),
}
data["notes-list"] = await render_template(
'templ_notes_list.html.j2',
notes=LogHandler().get_buffer(),
hide_if_empty=False)
return data

32
app/src/web/pages.py Normal file
View File

@@ -0,0 +1,32 @@
from quart import render_template
from .wrapper import url_for
from . import web
@web.route('/')
async def index():
return await render_template(
'page_index.html.j2',
fetch_url=url_for('.data_fetch'))
@web.route('/mqtt')
async def mqtt():
return await render_template(
'page_mqtt.html.j2',
fetch_url=url_for('.mqtt_fetch'))
@web.route('/notes')
async def notes():
return await render_template(
'page_notes.html.j2',
fetch_url=url_for('.notes_fetch'))
@web.route('/logging')
async def logging():
return await render_template(
'page_logging.html.j2',
fetch_url=url_for('.file_fetch'))

View File

@@ -0,0 +1,251 @@
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;overflow-x:hidden}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent;color:inherit}
a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}
code,kbd,pre,samp{font-family:monospace;font-size:1em}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;appearance:textfoeld;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{box-sizing:content-box;height:0;overflow:visible;border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{border-style:none;vertical-align:middle}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.w3-code,.w3-codespan{font-family:Consolas,"courier new",monospace;font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-danger{color:#fff!important;background-color:#dd0000!important}
.w3-note{color:#000!important;background-color:#fff599!important}
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
.w3-warning{color:#000!important;background-color:#ffb305!important}
.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 730 KiB

View File

@@ -0,0 +1,801 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<font id="FontAwesome5Free-Regular" horiz-adv-x="512" >
<font-face
font-family="Font Awesome 5 Free Regular"
font-weight="400"
font-stretch="normal"
units-per-em="512"
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="448"
descent="-64"
bbox="-0.0663408 -64.0662 640.004 448.1"
underline-thickness="25"
underline-position="-50"
unicode-range="U+0020-F5C8"
/>
<missing-glyph />
<glyph glyph-name="heart" unicode="&#xf004;"
d="M458.4 383.7c75.2998 -63.4004 64.0996 -166.601 10.5996 -221.3l-175.4 -178.7c-10 -10.2002 -23.2998 -15.7998 -37.5996 -15.7998c-14.2002 0 -27.5996 5.69922 -37.5996 15.8994l-175.4 178.7c-53.5996 54.7002 -64.5996 157.9 10.5996 221.2
c57.8008 48.7002 147.101 41.2998 202.4 -15c55.2998 56.2998 144.6 63.5996 202.4 15zM434.8 196.2c36.2002 36.8994 43.7998 107.7 -7.2998 150.8c-38.7002 32.5996 -98.7002 27.9004 -136.5 -10.5996l-35 -35.7002l-35 35.7002
c-37.5996 38.2998 -97.5996 43.1992 -136.5 10.5c-51.2002 -43.1006 -43.7998 -113.5 -7.2998 -150.7l175.399 -178.7c2.40039 -2.40039 4.40039 -2.40039 6.80078 0z" />
<glyph glyph-name="star" unicode="&#xf005;" horiz-adv-x="576"
d="M528.1 276.5c26.2002 -3.7998 36.7002 -36.0996 17.7002 -54.5996l-105.7 -103l25 -145.5c4.5 -26.3008 -23.1992 -45.9004 -46.3994 -33.7002l-130.7 68.7002l-130.7 -68.7002c-23.2002 -12.2998 -50.8994 7.39941 -46.3994 33.7002l25 145.5l-105.7 103
c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2998l65.2998 132.4c11.7998 23.8994 45.7002 23.5996 57.4004 0l65.2998 -132.4zM388.6 135.7l100.601 98l-139 20.2002l-62.2002 126l-62.2002 -126l-139 -20.2002l100.601 -98l-23.7002 -138.4l124.3 65.2998
l124.3 -65.2998z" />
<glyph glyph-name="user" unicode="&#xf007;" horiz-adv-x="448"
d="M313.6 144c74.2002 0 134.4 -60.2002 134.4 -134.4v-25.5996c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v25.5996c0 74.2002 60.2002 134.4 134.4 134.4c28.7998 0 42.5 -16 89.5996 -16s60.9004 16 89.5996 16zM400 -16v25.5996
c0 47.6006 -38.7998 86.4004 -86.4004 86.4004c-14.6992 0 -37.8994 -16 -89.5996 -16c-51.2998 0 -75 16 -89.5996 16c-47.6006 0 -86.4004 -38.7998 -86.4004 -86.4004v-25.5996h352zM224 160c-79.5 0 -144 64.5 -144 144s64.5 144 144 144s144 -64.5 144 -144
s-64.5 -144 -144 -144zM224 400c-52.9004 0 -96 -43.0996 -96 -96s43.0996 -96 96 -96s96 43.0996 96 96s-43.0996 96 -96 96z" />
<glyph glyph-name="clock" unicode="&#xf017;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM317.8 96.4004l-84.8994 61.6992
c-3.10059 2.30078 -4.90039 5.90039 -4.90039 9.7002v164.2c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12v-141.7l66.7998 -48.5996c5.40039 -3.90039 6.5 -11.4004 2.60059 -16.7998l-18.8008 -25.9004c-3.89941 -5.2998 -11.3994 -6.5 -16.7998 -2.59961z
" />
<glyph glyph-name="list-alt" unicode="&#xf022;"
d="M464 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h416zM458 16c3.31152 0 6 2.68848 6 6v340c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-340
c0 -3.31152 2.68848 -6 6 -6h404zM416 108v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 204v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12
v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 300v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM164 288c0 -19.8818 -16.1182 -36 -36 -36
s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 192c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 96c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36z" />
<glyph glyph-name="flag" unicode="&#xf024;"
d="M336.174 368c35.4668 0 73.0195 12.6914 108.922 28.1797c31.6406 13.6514 66.9043 -9.65723 66.9043 -44.1162v-239.919c0 -16.1953 -8.1543 -31.3057 -21.7129 -40.1631c-26.5762 -17.3643 -70.0693 -39.9814 -128.548 -39.9814c-68.6084 0 -112.781 32 -161.913 32
c-56.5674 0 -89.957 -11.2803 -127.826 -28.5566v-83.4434c0 -8.83691 -7.16309 -16 -16 -16h-16c-8.83691 0 -16 7.16309 -16 16v406.438c-14.3428 8.2998 -24 23.7979 -24 41.5615c0 27.5693 23.2422 49.71 51.2012 47.8965
c22.9658 -1.49023 41.8662 -19.4717 44.4805 -42.3379c0.213867 -1.83398 0.308594 -3.65918 0.308594 -5.5498c0 -5.30273 -0.860352 -10.4053 -2.4502 -15.1768c22.418 8.68555 49.4199 15.168 80.7207 15.168c68.6084 0 112.781 -32 161.913 -32zM464 112v240
c-31.5059 -14.6338 -84.5547 -32 -127.826 -32c-59.9111 0 -101.968 32 -161.913 32c-41.4365 0 -80.4766 -16.5879 -102.261 -32v-232c31.4473 14.5967 84.4648 24 127.826 24c59.9111 0 101.968 -32 161.913 -32c41.4365 0 80.4775 16.5879 102.261 32z" />
<glyph glyph-name="bookmark" unicode="&#xf02e;" horiz-adv-x="384"
d="M336 448c26.5098 0 48 -21.4902 48 -48v-464l-192 112l-192 -112v464c0 26.5098 21.4902 48 48 48h288zM336 19.5703v374.434c0 3.31348 -2.68555 5.99609 -6 5.99609h-276c-3.31152 0 -6 -2.68848 -6 -6v-374.43l144 84z" />
<glyph glyph-name="image" unicode="&#xf03e;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM458 48c3.31152 0 6 2.68848 6 6v276c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-276
c0 -3.31152 2.68848 -6 6 -6h404zM128 296c22.0908 0 40 -17.9092 40 -40s-17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40zM96 96v48l39.5137 39.5146c4.6875 4.68652 12.2852 4.68652 16.9717 0l39.5146 -39.5146l119.514 119.515
c4.6875 4.68652 12.2852 4.68652 16.9717 0l87.5146 -87.5146v-80h-320z" />
<glyph glyph-name="edit" unicode="&#xf044;" horiz-adv-x="576"
d="M402.3 103.1l32 32c5 5 13.7002 1.5 13.7002 -5.69922v-145.4c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h273.5c7.09961 0 10.7002 -8.59961 5.7002 -13.7002l-32 -32c-1.5 -1.5 -3.5 -2.2998 -5.7002 -2.2998h-241.5v-352h352
v113.5c0 2.09961 0.799805 4.09961 2.2998 5.59961zM558.9 304.9l-262.601 -262.601l-90.3994 -10c-26.2002 -2.89941 -48.5 19.2002 -45.6006 45.6006l10 90.3994l262.601 262.601c22.8994 22.8994 59.8994 22.8994 82.6992 0l43.2002 -43.2002
c22.9004 -22.9004 22.9004 -60 0.100586 -82.7998zM460.1 274l-58.0996 58.0996l-185.8 -185.899l-7.2998 -65.2998l65.2998 7.2998zM524.9 353.7l-43.2002 43.2002c-4.10059 4.09961 -10.7998 4.09961 -14.7998 0l-30.9004 -30.9004l58.0996 -58.0996l30.9004 30.8994
c4 4.2002 4 10.7998 -0.0996094 14.9004z" />
<glyph glyph-name="times-circle" unicode="&#xf057;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM357.8 254.2l-62.2002 -62.2002l62.2002 -62.2002
c4.7002 -4.7002 4.7002 -12.2998 0 -17l-22.5996 -22.5996c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-62.2002 62.2002l-62.2002 -62.2002c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-22.5996 22.5996c-4.7002 4.7002 -4.7002 12.2998 0 17l62.2002 62.2002l-62.2002 62.2002
c-4.7002 4.7002 -4.7002 12.2998 0 17l22.5996 22.5996c4.7002 4.7002 12.2998 4.7002 17 0l62.2002 -62.2002l62.2002 62.2002c4.7002 4.7002 12.2998 4.7002 17 0l22.5996 -22.5996c4.7002 -4.7002 4.7002 -12.2998 0 -17z" />
<glyph glyph-name="check-circle" unicode="&#xf058;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200z
M396.204 261.733c4.66699 -4.70508 4.63672 -12.3037 -0.0673828 -16.9717l-172.589 -171.204c-4.70508 -4.66797 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66797 4.70605 -4.63672 12.3047 0.0683594 16.9717l22.7188 22.5361
c4.70508 4.66699 12.3027 4.63574 16.9697 -0.0693359l59.792 -60.2773l141.353 140.216c4.70508 4.66797 12.3027 4.6377 16.9697 -0.0673828z" />
<glyph glyph-name="question-circle" unicode="&#xf059;"
d="M256 440c136.957 0 248 -111.083 248 -248c0 -136.997 -111.043 -248 -248 -248s-248 111.003 -248 248c0 136.917 111.043 248 248 248zM256 -8c110.569 0 200 89.4697 200 200c0 110.529 -89.5088 200 -200 200c-110.528 0 -200 -89.5049 -200 -200
c0 -110.569 89.4678 -200 200 -200zM363.244 247.2c0 -67.0518 -72.4209 -68.084 -72.4209 -92.8633v-6.33691c0 -6.62695 -5.37305 -12 -12 -12h-45.6475c-6.62695 0 -12 5.37305 -12 12v8.65918c0 35.7451 27.1006 50.0342 47.5791 61.5156
c17.5615 9.84473 28.3242 16.541 28.3242 29.5791c0 17.2461 -21.999 28.6934 -39.7842 28.6934c-23.1885 0 -33.8936 -10.9775 -48.9424 -29.9697c-4.05664 -5.11914 -11.46 -6.07031 -16.666 -2.12402l-27.8232 21.0986
c-5.10742 3.87207 -6.25098 11.0654 -2.64453 16.3633c23.627 34.6934 53.7217 54.1846 100.575 54.1846c49.0713 0 101.45 -38.3037 101.45 -88.7998zM298 80c0 -23.1592 -18.8408 -42 -42 -42s-42 18.8408 -42 42s18.8408 42 42 42s42 -18.8408 42 -42z" />
<glyph glyph-name="eye" unicode="&#xf06e;" horiz-adv-x="576"
d="M288 304c0.114258 0 0.240234 -0.0175781 0.354492 -0.0175781c61.6543 0 111.71 -50.0557 111.71 -111.71s-50.0557 -111.71 -111.71 -111.71s-111.71 50.0557 -111.71 111.71c0 10.7422 1.51953 21.1328 4.35547 30.9678
c7.95898 -4.52637 17.2129 -7.17188 27 -7.24023c30.9072 0 56 25.0928 56 56c-0.0683594 9.78711 -2.71387 19.041 -7.24023 27c9.88379 3.07617 20.3896 4.83008 31.2402 5zM572.52 206.6c2.21387 -4.37793 3.46094 -9.38965 3.46094 -14.626
c0 -5.2373 -1.24707 -10.1855 -3.46094 -14.5635c-54.1992 -105.771 -161.59 -177.41 -284.52 -177.41s-230.29 71.5898 -284.52 177.4c-2.21387 4.37793 -3.46094 9.38965 -3.46094 14.626c0 5.2373 1.24707 10.1855 3.46094 14.5635
c54.1992 105.771 161.59 177.41 284.52 177.41s230.29 -71.5898 284.52 -177.4zM288 48c98.6602 0 189.1 55 237.93 144c-48.8398 89 -139.27 144 -237.93 144s-189.09 -55 -237.93 -144c48.8398 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="eye-slash" unicode="&#xf070;" horiz-adv-x="640"
d="M634 -23c3.66895 -2.93262 6.00391 -7.45117 6.00391 -12.5088c0 -3.7832 -1.31543 -7.26074 -3.51367 -10.001l-10 -12.4902c-2.93359 -3.66309 -7.44824 -5.99414 -12.502 -5.99414c-3.77637 0 -7.25 1.31152 -9.98828 3.50391l-598 467.49
c-3.66895 2.93262 -6.00391 7.45117 -6.00391 12.5088c0 3.7832 1.31543 7.26074 3.51367 10.001l10 12.4902c2.93359 3.66309 7.44824 5.99414 12.502 5.99414c3.77637 0 7.25 -1.31152 9.98828 -3.50391zM296.79 301.53c7.51172 1.60254 15.2266 2.45508 23.21 2.46973
c60.4805 0 109.36 -47.9102 111.58 -107.85zM343.21 82.46c-7.51367 -1.59375 -15.2285 -2.44336 -23.21 -2.45996c-60.4697 0 -109.35 47.9102 -111.58 107.84zM320 336c-19.8799 0 -39.2803 -2.7998 -58.2197 -7.09961l-46.4102 36.29
c32.9199 11.8096 67.9297 18.8096 104.63 18.8096c122.93 0 230.29 -71.5898 284.57 -177.4c2.21289 -4.37793 3.45996 -9.38965 3.45996 -14.626c0 -5.2373 -1.24707 -10.1855 -3.45996 -14.5635c-14.1924 -27.5625 -31.9229 -52.6689 -52.9004 -75.1104l-37.7402 29.5
c17.2305 18.0527 31.9385 38.1318 44 60.2002c-48.8398 89 -139.279 144 -237.93 144zM320 48c19.8896 0 39.2803 2.7998 58.2197 7.08984l46.4102 -36.2803c-32.9199 -11.7598 -67.9297 -18.8096 -104.63 -18.8096c-122.92 0 -230.28 71.5898 -284.51 177.4
c-2.21387 4.37793 -3.46094 9.38965 -3.46094 14.626c0 5.2373 1.24707 10.1855 3.46094 14.5635c14.1885 27.5586 31.916 52.6621 52.8896 75.1006l37.7402 -29.5c-17.249 -18.0469 -31.9727 -38.1221 -44.0498 -60.1904c48.8496 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="calendar-alt" unicode="&#xf073;" horiz-adv-x="448"
d="M148 160h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM256 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40zM352 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM256 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM160 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM352 76c0 -6.59961 -5.40039 -12 -12 -12h-40
c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="comment" unicode="&#xf075;"
d="M256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5c-3.80078 8.7998 -2 19 4.59961 26
c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004
l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="folder" unicode="&#xf07b;"
d="M464 320c26.5098 0 48 -21.4902 48 -48v-224c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h146.74c8.49023 0 16.6299 -3.37012 22.6299 -9.37012l54.6299 -54.6299h192zM464 48v224h-198.62
c-8.49023 0 -16.6299 3.37012 -22.6299 9.37012l-54.6299 54.6299h-140.12v-288h416z" />
<glyph glyph-name="folder-open" unicode="&#xf07c;" horiz-adv-x="576"
d="M527.9 224c37.6992 0 60.6992 -41.5 40.6992 -73.4004l-79.8994 -128c-8.7998 -14.0996 -24.2002 -22.5996 -40.7002 -22.5996h-400c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h160l64 -64h160c26.5 0 48 -21.5 48 -48v-48h47.9004zM48 330v-233.4l62.9004 104.2
c8.69922 14.4004 24.2998 23.2002 41.0996 23.2002h280v42c0 3.2998 -2.7002 6 -6 6h-173.9l-64 64h-134.1c-3.2998 0 -6 -2.7002 -6 -6zM448 48l80 128h-378.8l-77.2002 -128h376z" />
<glyph glyph-name="chart-bar" unicode="&#xf080;"
d="M396.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v230.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-230.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM204.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v198.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-198.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM300.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v134.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-134.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM496 48c8.83984 0 16 -7.16016 16 -16v-16
c0 -8.83984 -7.16016 -16 -16 -16h-464c-17.6699 0 -32 14.3301 -32 32v336c0 8.83984 7.16016 16 16 16h16c8.83984 0 16 -7.16016 16 -16v-320h448zM108.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v70.4004c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004
c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-70.4004c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004z" />
<glyph glyph-name="comments" unicode="&#xf086;" horiz-adv-x="576"
d="M532 61.7998c15.2998 -30.7002 37.4004 -54.5 37.7998 -54.7998c6.2998 -6.7002 8 -16.5 4.40039 -25c-3.7002 -8.5 -12 -14 -21.2002 -14c-53.5996 0 -96.7002 20.2998 -125.2 38.7998c-19 -4.39941 -39 -6.7998 -59.7998 -6.7998
c-86.2002 0 -159.9 40.4004 -191.3 97.7998c-9.7002 1.2002 -19.2002 2.7998 -28.4004 4.90039c-28.5 -18.6006 -71.7002 -38.7998 -125.2 -38.7998c-9.19922 0 -17.5996 5.5 -21.1992 14c-3.7002 8.5 -1.90039 18.2998 4.39941 25
c0.400391 0.399414 22.4004 24.1992 37.7002 54.8994c-27.5 27.2002 -44 61.2002 -44 98.2002c0 88.4004 93.0996 160 208 160c86.2998 0 160.3 -40.5 191.8 -98.0996c99.7002 -11.8008 176.2 -77.9004 176.2 -157.9c0 -37.0996 -16.5 -71.0996 -44 -98.2002zM139.2 154.1
l19.7998 -4.5c16 -3.69922 32.5 -5.59961 49 -5.59961c86.7002 0 160 51.2998 160 112s-73.2998 112 -160 112s-160 -51.2998 -160 -112c0 -28.7002 16.2002 -50.5996 29.7002 -64l24.7998 -24.5l-15.5 -31.0996c-2.59961 -5.10059 -5.2998 -10.1006 -8 -14.8008
c14.5996 5.10059 29 12.3008 43.0996 21.4004zM498.3 96c13.5 13.4004 29.7002 35.2998 29.7002 64c0 49.2002 -48.2998 91.5 -112.7 106c0.299805 -3.2998 0.700195 -6.59961 0.700195 -10c0 -80.9004 -78 -147.5 -179.3 -158.3
c29.0996 -29.6006 77.2998 -49.7002 131.3 -49.7002c16.5 0 33 1.90039 49 5.59961l19.9004 4.60059l17.0996 -11.1006c14.0996 -9.09961 28.5 -16.2998 43.0996 -21.3994c-2.69922 4.7002 -5.39941 9.7002 -8 14.7998l-15.5 31.0996z" />
<glyph glyph-name="star-half" unicode="&#xf089;" horiz-adv-x="576"
d="M288 62.7002v-54.2998l-130.7 -68.6006c-23.3994 -12.2998 -50.8994 7.60059 -46.3994 33.7002l25 145.5l-105.7 103c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2002l65.2998 132.4c5.90039 11.8994 17.2998 17.7998 28.7002 17.7998v-68.0996l-62.2002 -126
l-139 -20.2002l100.601 -98l-23.7002 -138.4z" />
<glyph glyph-name="lemon" unicode="&#xf094;"
d="M484.112 420.111c28.1221 -28.123 35.9434 -68.0039 19.0215 -97.0547c-23.0576 -39.584 50.1436 -163.384 -82.3311 -295.86c-132.301 -132.298 -256.435 -59.3594 -295.857 -82.3291c-29.0459 -16.917 -68.9219 -9.11426 -97.0576 19.0205
c-28.1221 28.1221 -35.9434 68.0029 -19.0215 97.0547c23.0566 39.5859 -50.1436 163.386 82.3301 295.86c132.308 132.309 256.407 59.3496 295.862 82.332c29.0498 16.9219 68.9307 9.09863 97.0537 -19.0234zM461.707 347.217
c13.5166 23.2031 -27.7578 63.7314 -50.4883 50.4912c-66.6025 -38.7939 -165.646 45.5898 -286.081 -74.8457c-120.444 -120.445 -36.0449 -219.472 -74.8447 -286.08c-13.542 -23.2471 27.8145 -63.6953 50.4932 -50.4883
c66.6006 38.7949 165.636 -45.5996 286.076 74.8428c120.444 120.445 36.0449 219.472 74.8447 286.08zM291.846 338.481c1.37012 -10.96 -6.40332 -20.957 -17.3643 -22.3271c-54.8467 -6.85547 -135.779 -87.7871 -142.636 -142.636
c-1.37305 -10.9883 -11.3984 -18.7334 -22.3262 -17.3643c-10.9609 1.37012 -18.7344 11.3652 -17.3643 22.3262c9.16211 73.2852 104.167 168.215 177.364 177.364c10.9531 1.36816 20.9561 -6.40234 22.3262 -17.3633z" />
<glyph glyph-name="credit-card" unicode="&#xf09d;" horiz-adv-x="576"
d="M527.9 416c26.5996 0 48.0996 -21.5 48.0996 -48v-352c0 -26.5 -21.5 -48 -48.0996 -48h-479.801c-26.5996 0 -48.0996 21.5 -48.0996 48v352c0 26.5 21.5 48 48.0996 48h479.801zM54.0996 368c-3.2998 0 -6 -2.7002 -6 -6v-42h479.801v42c0 3.2998 -2.7002 6 -6 6
h-467.801zM521.9 16c3.2998 0 6 2.7002 6 6v170h-479.801v-170c0 -3.2998 2.7002 -6 6 -6h467.801zM192 116v-40c0 -6.59961 -5.40039 -12 -12 -12h-72c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h72c6.59961 0 12 -5.40039 12 -12zM384 116v-40
c0 -6.59961 -5.40039 -12 -12 -12h-136c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h136c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="hdd" unicode="&#xf0a0;" horiz-adv-x="576"
d="M567.403 212.358c5.59668 -8.04688 8.59668 -17.6113 8.59668 -27.4121v-136.946c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v136.946c0 10.167 3.19531 19.6465 8.59668 27.4121l105.08 151.053
c8.67383 12.4678 23.0791 20.5889 39.4043 20.5889h269.838c16.3252 0 30.7305 -8.12109 39.4043 -20.5889zM153.081 336l-77.9131 -112h425.664l-77.9131 112h-269.838zM528 48v128h-480v-128h480zM496 112c0 -17.6729 -14.3271 -32 -32 -32s-32 14.3271 -32 32
s14.3271 32 32 32s32 -14.3271 32 -32zM400 112c0 -17.6729 -14.3271 -32 -32 -32s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32z" />
<glyph glyph-name="hand-point-right" unicode="&#xf0a4;"
d="M428.8 310.4c45.0996 0 83.2002 -38.1016 83.2002 -83.2002c0 -45.6162 -37.7646 -83.2002 -83.2002 -83.2002h-35.6475c-1.71387 -7.70605 -4.43555 -15.2051 -7.92969 -22.0645c2.50586 -22.0059 -3.50293 -44.9775 -15.9844 -62.791
c-1.14062 -52.4863 -37.3984 -91.1445 -99.9404 -91.1445h-21.2988c-60.0635 0 -98.5117 40 -127.2 40h-2.67871c-5.74707 -4.95215 -13.5361 -8 -22.1201 -8h-64c-17.6729 0 -32 12.8936 -32 28.7998v230.4c0 15.9062 14.3271 28.7998 32 28.7998h64.001
c8.58398 0 16.373 -3.04785 22.1201 -8h2.67871c6.96387 0 14.8623 6.19336 30.1816 23.6689l0.128906 0.148438l0.130859 0.145508c8.85645 9.93652 18.1162 20.8398 25.8506 33.2529c18.7051 30.2471 30.3936 78.7842 75.707 78.7842c56.9277 0 92 -35.2861 92 -83.2002
c0 -0.0283203 0 0.0361328 0 0.0078125c0 -7.66602 -0.748047 -15.1582 -2.17578 -22.4072h86.1768zM428.8 192c18.9756 0 35.2002 16.2246 35.2002 35.2002c0 18.7002 -16.7754 35.2002 -35.2002 35.2002h-158.399c0 17.3242 26.3994 35.1992 26.3994 70.3994
c0 26.4004 -20.625 35.2002 -44 35.2002c-8.79395 0 -20.4443 -32.7119 -34.9258 -56.0996c-9.07422 -14.5752 -19.5244 -27.2256 -30.7988 -39.875c-16.1094 -18.374 -33.8359 -36.6328 -59.0752 -39.5967v-176.753c42.79 -3.7627 74.5088 -39.6758 120 -39.6758h21.2988
c40.5244 0 57.124 22.1973 50.6006 61.3252c14.6113 8.00098 24.1514 33.9785 12.9248 53.625c19.3652 18.2246 17.7871 46.3809 4.9502 61.0498h91.0254zM88 64c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="hand-point-left" unicode="&#xf0a5;"
d="M0 227.2c0 45.0986 38.1006 83.2002 83.2002 83.2002h86.1758c-1.3623 6.91016 -2.17578 14.374 -2.17578 22.3994c0 47.9141 35.0723 83.2002 92 83.2002c45.3135 0 57.002 -48.5371 75.7061 -78.7852c7.73438 -12.4121 16.9951 -23.3154 25.8506 -33.2529
l0.130859 -0.145508l0.128906 -0.148438c15.3213 -17.4746 23.2197 -23.668 30.1836 -23.668h2.67871c5.74707 4.95215 13.5361 8 22.1201 8h64c17.6729 0 32 -12.8936 32 -28.7998v-230.4c0 -15.9062 -14.3271 -28.7998 -32 -28.7998h-64
c-8.58398 0 -16.373 3.04785 -22.1201 8h-2.67871c-28.6885 0 -67.1367 -40 -127.2 -40h-21.2988c-62.542 0 -98.8008 38.6582 -99.9404 91.1445c-12.4814 17.8135 -18.4922 40.7852 -15.9844 62.791c-3.49414 6.85938 -6.21582 14.3584 -7.92969 22.0645h-35.6465
c-45.4355 0 -83.2002 37.584 -83.2002 83.2002zM48 227.2c0 -18.9756 16.2246 -35.2002 35.2002 -35.2002h91.0244c-12.8369 -14.6689 -14.415 -42.8252 4.9502 -61.0498c-11.2256 -19.6465 -1.68652 -45.624 12.9248 -53.625
c-6.52246 -39.1279 10.0771 -61.3252 50.6016 -61.3252h21.2988c45.4912 0 77.21 35.9131 120 39.6768v176.752c-25.2393 2.96289 -42.9658 21.2227 -59.0752 39.5967c-11.2744 12.6494 -21.7246 25.2998 -30.7988 39.875
c-14.4814 23.3877 -26.1318 56.0996 -34.9258 56.0996c-23.375 0 -44 -8.7998 -44 -35.2002c0 -35.2002 26.3994 -53.0752 26.3994 -70.3994h-158.399c-18.4248 0 -35.2002 -16.5 -35.2002 -35.2002zM448 88c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24
s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-up" unicode="&#xf0a6;" horiz-adv-x="448"
d="M105.6 364.8c0 45.0996 38.1016 83.2002 83.2002 83.2002c45.6162 0 83.2002 -37.7646 83.2002 -83.2002v-35.6465c7.70605 -1.71387 15.2051 -4.43555 22.0645 -7.92969c22.0059 2.50684 44.9775 -3.50293 62.791 -15.9844
c52.4863 -1.14062 91.1445 -37.3984 91.1445 -99.9404v-21.2988c0 -60.0635 -40 -98.5117 -40 -127.2v-2.67871c4.95215 -5.74707 8 -13.5361 8 -22.1201v-64c0 -17.6729 -12.8936 -32 -28.7998 -32h-230.4c-15.9062 0 -28.7998 14.3271 -28.7998 32v64
c0 8.58398 3.04785 16.373 8 22.1201v2.67871c0 6.96387 -6.19336 14.8623 -23.6689 30.1816l-0.148438 0.128906l-0.145508 0.130859c-9.93652 8.85645 -20.8398 18.1162 -33.2529 25.8506c-30.2471 18.7051 -78.7842 30.3936 -78.7842 75.707
c0 56.9277 35.2861 92 83.2002 92c0.0283203 0 -0.0361328 0 -0.0078125 0c7.66602 0 15.1582 -0.748047 22.4072 -2.17578v86.1768zM224 364.8c0 18.9756 -16.2246 35.2002 -35.2002 35.2002c-18.7002 0 -35.2002 -16.7754 -35.2002 -35.2002v-158.399
c-17.3242 0 -35.1992 26.3994 -70.3994 26.3994c-26.4004 0 -35.2002 -20.625 -35.2002 -44c0 -8.79395 32.7119 -20.4443 56.0996 -34.9258c14.5752 -9.07422 27.2256 -19.5244 39.875 -30.7988c18.374 -16.1094 36.6328 -33.8359 39.5967 -59.0752h176.753
c3.7627 42.79 39.6758 74.5088 39.6758 120v21.2988c0 40.5244 -22.1973 57.124 -61.3252 50.6006c-8.00098 14.6113 -33.9785 24.1514 -53.625 12.9248c-18.2246 19.3652 -46.3809 17.7871 -61.0498 4.9502v91.0254zM352 24c-13.2549 0 -24 -10.7451 -24 -24
s10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-down" unicode="&#xf0a7;" horiz-adv-x="448"
d="M188.8 -64c-45.0986 0 -83.2002 38.1006 -83.2002 83.2002v86.1758c-6.91016 -1.3623 -14.374 -2.17578 -22.3994 -2.17578c-47.9141 0 -83.2002 35.0723 -83.2002 92c0 45.3135 48.5371 57.002 78.7852 75.707c12.4121 7.73438 23.3154 16.9951 33.2529 25.8506
l0.145508 0.130859l0.148438 0.128906c17.4746 15.3213 23.668 23.2197 23.668 30.1836v2.67871c-4.95215 5.74707 -8 13.5361 -8 22.1201v64c0 17.6729 12.8936 32 28.7998 32h230.4c15.9062 0 28.7998 -14.3271 28.7998 -32v-64.001
c0 -8.58398 -3.04785 -16.373 -8 -22.1201v-2.67871c0 -28.6885 40 -67.1367 40 -127.2v-21.2988c0 -62.542 -38.6582 -98.8008 -91.1445 -99.9404c-17.8135 -12.4814 -40.7852 -18.4922 -62.791 -15.9844c-6.85938 -3.49414 -14.3584 -6.21582 -22.0645 -7.92969v-35.6465
c0 -45.4355 -37.584 -83.2002 -83.2002 -83.2002zM188.8 -16c18.9756 0 35.2002 16.2246 35.2002 35.2002v91.0244c14.6689 -12.8369 42.8252 -14.415 61.0498 4.9502c19.6465 -11.2256 45.624 -1.68652 53.625 12.9248c39.1279 -6.52246 61.3252 10.0771 61.3252 50.6016
v21.2988c0 45.4912 -35.9131 77.21 -39.6768 120h-176.752c-2.96289 -25.2393 -21.2227 -42.9658 -39.5967 -59.0752c-12.6494 -11.2744 -25.2998 -21.7246 -39.875 -30.7988c-23.3877 -14.4814 -56.0996 -26.1318 -56.0996 -34.9258c0 -23.375 8.7998 -44 35.2002 -44
c35.2002 0 53.0752 26.3994 70.3994 26.3994v-158.399c0 -18.4248 16.5 -35.2002 35.2002 -35.2002zM328 384c0 -13.2549 10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24z" />
<glyph glyph-name="copy" unicode="&#xf0c5;" horiz-adv-x="448"
d="M433.941 382.059c8.68848 -8.68848 14.0586 -20.6943 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-80v-48c0 -26.5098 -21.4902 -48 -48 -48h-224c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h80v48c0 26.5098 21.4902 48 48 48
h172.118c13.2461 0 25.252 -5.37012 33.9404 -14.0586zM266 -16c3.31152 0 6 2.68848 6 6v42h-96c-26.5098 0 -48 21.4902 -48 48v224h-74c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM394 80c3.31152 0 6 2.68848 6 6v202h-88
c-13.2549 0 -24 10.7451 -24 24v88h-106c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM400 336v9.63184c0 1.65527 -0.670898 3.15723 -1.75684 4.24316l-48.3682 48.3682c-1.12598 1.125 -2.65234 1.75684 -4.24316 1.75684h-9.63184v-64h64z" />
<glyph glyph-name="save" unicode="&#xf0c7;" horiz-adv-x="448"
d="M433.941 318.059c8.68848 -8.68848 14.0586 -20.6943 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h268.118c13.2461 0 25.252 -5.37012 33.9404 -14.0586zM272 368h-128v-80h128v80z
M394 16c3.31152 0 6 2.68848 6 6v259.632c0 1.65527 -0.670898 3.15723 -1.75684 4.24316l-78.2432 78.2432v-100.118c0 -13.2549 -10.7451 -24 -24 -24h-176c-13.2549 0 -24 10.7451 -24 24v104h-42c-3.31152 0 -6 -2.68848 -6 -6v-340c0 -3.31152 2.68848 -6 6 -6h340z
M224 216c48.5234 0 88 -39.4766 88 -88s-39.4766 -88 -88 -88s-88 39.4766 -88 88s39.4766 88 88 88zM224 88c22.0557 0 40 17.9443 40 40s-17.9443 40 -40 40s-40 -17.9443 -40 -40s17.9443 -40 40 -40z" />
<glyph glyph-name="square" unicode="&#xf0c8;" horiz-adv-x="448"
d="M400 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352zM394 16c3.2998 0 6 2.7002 6 6v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="envelope" unicode="&#xf0e0;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM464 336h-416v-40.8047c22.4248 -18.2627 58.1797 -46.6602 134.587 -106.49
c16.834 -13.2422 50.2051 -45.0762 73.4131 -44.7012c23.2119 -0.371094 56.5723 31.4541 73.4131 44.7012c76.4189 59.8389 112.165 88.2305 134.587 106.49v40.8047zM48 48h416v185.601c-22.915 -18.252 -55.4189 -43.8691 -104.947 -82.6523
c-22.5439 -17.748 -60.3359 -55.1787 -103.053 -54.9473c-42.9277 -0.231445 -81.2051 37.75 -103.062 54.9551c-49.5293 38.7842 -82.0244 64.3945 -104.938 82.6455v-185.602z" />
<glyph glyph-name="lightbulb" unicode="&#xf0eb;" horiz-adv-x="352"
d="M176 368c8.83984 0 16 -7.16016 16 -16s-7.16016 -16 -16 -16c-35.2803 0 -64 -28.7002 -64 -64c0 -8.83984 -7.16016 -16 -16 -16s-16 7.16016 -16 16c0 52.9404 43.0596 96 96 96zM96.0596 -11.1699l-0.0400391 43.1797h159.961l-0.0507812 -43.1797
c-0.00976562 -3.13965 -0.939453 -6.21973 -2.67969 -8.83984l-24.5098 -36.8398c-2.95996 -4.45996 -7.95996 -7.14062 -13.3203 -7.14062h-78.8496c-5.35059 0 -10.3506 2.68066 -13.3203 7.14062l-24.5098 36.8398c-1.75 2.62012 -2.68066 5.68945 -2.68066 8.83984z
M176 448c97.2002 0 176 -78.7998 176 -176c0 -44.3701 -16.4502 -84.8496 -43.5498 -115.79c-16.6406 -18.9795 -42.7402 -58.79 -52.4199 -92.1602v-0.0498047h-48v0.0996094c0.00488281 4.98145 0.790039 9.78809 2.21973 14.3008
c5.67969 17.9893 22.9902 64.8496 62.0996 109.46c20.4102 23.29 31.6504 53.1699 31.6504 84.1396c0 70.5801 -57.4199 128 -128 128c-68.2803 0 -128.15 -54.3604 -127.95 -128c0.0898438 -30.9902 11.0703 -60.71 31.6104 -84.1396
c39.3496 -44.9004 56.5801 -91.8604 62.1699 -109.67c1.42969 -4.56055 2.13965 -9.30078 2.15039 -14.0703v-0.120117h-48v0.0595703c-9.68066 33.3604 -35.7803 73.1709 -52.4209 92.1602c-27.1094 30.9307 -43.5596 71.4102 -43.5596 115.78
c0 93.0303 73.7197 176 176 176z" />
<glyph glyph-name="bell" unicode="&#xf0f3;" horiz-adv-x="448"
d="M439.39 85.71c6 -6.44043 8.66016 -14.1602 8.61035 -21.71c-0.0996094 -16.4004 -12.9805 -32 -32.0996 -32h-383.801c-19.1191 0 -31.9893 15.5996 -32.0996 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c19.3193 20.7598 55.4697 51.9902 55.4697 154.29
c0 77.7002 54.4795 139.9 127.939 155.16v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -102.3 36.1504 -133.53 55.4697 -154.29zM67.5303 80h312.939
c-21.2197 27.96 -44.4199 74.3203 -44.5293 159.42c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112s-112 -50.1396 -112 -112c0 -0.200195 0.0595703 -0.379883 0.0595703 -0.580078
c-0.109375 -85.0898 -23.3096 -131.45 -44.5293 -159.42zM224 -64c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="hospital" unicode="&#xf0f8;" horiz-adv-x="448"
d="M128 204v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12zM268 192c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40
c0 -6.62695 -5.37305 -12 -12 -12h-40zM192 108c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40zM268 96c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.62695 5.37305 12 12 12h19.5v378.965c0 11.6172 10.7451 21.0352 24 21.0352h88.5v40c0 13.2549 10.7451 24 24 24h112c13.2549 0 24 -10.7451 24 -24v-40h88.5
c13.2549 0 24 -9.41797 24 -21.0352v-378.965h19.5c6.62695 0 12 -5.37305 12 -12zM79.5 -15h112.5v67c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-67h112.5v351h-64.5v-24c0 -13.2549 -10.7451 -24 -24 -24h-112c-13.2549 0 -24 10.7451 -24 24v24
h-64.5v-351zM266 384h-26v26c0 3.31152 -2.68848 6 -6 6h-20c-3.31152 0 -6 -2.68848 -6 -6v-26h-26c-3.31152 0 -6 -2.68848 -6 -6v-20c0 -3.31152 2.68848 -6 6 -6h26v-26c0 -3.31152 2.68848 -6 6 -6h20c3.31152 0 6 2.68848 6 6v26h26c3.31152 0 6 2.68848 6 6v20
c0 3.31152 -2.68848 6 -6 6z" />
<glyph glyph-name="plus-square" unicode="&#xf0fe;" horiz-adv-x="448"
d="M352 208v-32c0 -6.59961 -5.40039 -12 -12 -12h-88v-88c0 -6.59961 -5.40039 -12 -12 -12h-32c-6.59961 0 -12 5.40039 -12 12v88h-88c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h88v88c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12
v-88h88c6.59961 0 12 -5.40039 12 -12zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340
c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="circle" unicode="&#xf111;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200z" />
<glyph glyph-name="smile" unicode="&#xf118;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM332 135.4c8.5 10.1992 23.7002 11.5 33.7998 3.09961c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998
c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.0996 8.40039 25.2998 7.09961 33.7998 -3.09961c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004z" />
<glyph glyph-name="frown" unicode="&#xf119;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 144c40.2002 0 78 -17.7002 103.8 -48.5996c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008
c-10.7002 -8.7998 -25.7002 -6.59961 -33.7998 3.10059c-16.6006 20 -41 31.3994 -66.9004 31.3994s-50.2998 -11.5 -66.9004 -31.3994c-8.5 -10.2002 -23.5996 -11.5 -33.7998 -3.10059c-10.2002 8.5 -11.5996 23.6006 -3.09961 33.8008
c25.7998 30.8994 63.5996 48.5996 103.8 48.5996z" />
<glyph glyph-name="meh" unicode="&#xf11a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM336 128c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-176c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h176z
" />
<glyph glyph-name="keyboard" unicode="&#xf11c;" horiz-adv-x="576"
d="M528 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h480zM536 48v288c0 4.41113 -3.58887 8 -8 8h-480c-4.41113 0 -8 -3.58887 -8 -8v-288c0 -4.41113 3.58887 -8 8 -8
h480c4.41113 0 8 3.58887 8 8zM170 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM266 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM362 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM458 178c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 96c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 96
c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28
c6.62695 0 12 -5.37305 12 -12v-28zM218 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM314 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM410 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 260c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM408 102c0 -6.62695 -5.37305 -12 -12 -12h-216c-6.62695 0 -12 5.37305 -12 12v16c0 6.62695 5.37305 12 12 12h216c6.62695 0 12 -5.37305 12 -12v-16z" />
<glyph glyph-name="calendar" unicode="&#xf133;" horiz-adv-x="448"
d="M400 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12
v-52h48zM394 -16c3.2998 0 6 2.7002 6 6v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="play-circle" unicode="&#xf144;"
d="M371.7 210c16.3994 -9.2002 16.3994 -32.9004 0 -42l-176 -101c-15.9004 -8.7998 -35.7002 2.59961 -35.7002 21v208c0 18.5 19.9004 29.7998 35.7002 21zM504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192
c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200z" />
<glyph glyph-name="minus-square" unicode="&#xf146;" horiz-adv-x="448"
d="M108 164c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h232c6.59961 0 12 -5.40039 12 -12v-32c0 -6.59961 -5.40039 -12 -12 -12h-232zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352
c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="check-square" unicode="&#xf14a;" horiz-adv-x="448"
d="M400 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h352zM400 16v352h-352v-352h352zM364.136 257.724l-172.589 -171.204
c-4.70508 -4.66699 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66699 4.70508 -4.63672 12.3037 0.0693359 16.9717l22.7188 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l59.792 -60.2773l141.353 140.217
c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0683594l22.5361 -22.7178c4.66699 -4.70605 4.63672 -12.3047 -0.0683594 -16.9717z" />
<glyph glyph-name="share-square" unicode="&#xf14d;" horiz-adv-x="576"
d="M561.938 289.94c18.75 -18.7402 18.75 -49.1406 0 -67.8809l-143.998 -144c-29.9727 -29.9727 -81.9404 -9.05273 -81.9404 33.9404v53.7998c-101.266 -7.83691 -99.625 -31.6406 -84.1104 -78.7598c14.2285 -43.0889 -33.4736 -79.248 -71.0195 -55.7402
c-51.6924 32.3057 -84.8701 83.0635 -84.8701 144.76c0 39.3408 12.2197 72.7402 36.3301 99.3008c19.8398 21.8398 47.7402 38.4697 82.9102 49.4199c36.7295 11.4395 78.3096 16.1094 120.76 17.9893v57.1982c0 42.9355 51.9258 63.9541 81.9404 33.9404zM384 112l144 144
l-144 144v-104.09c-110.86 -0.90332 -240 -10.5166 -240 -119.851c0 -52.1396 32.79 -85.6094 62.3096 -104.06c-39.8174 120.65 48.999 141.918 177.69 143.84v-103.84zM408.74 27.5068c7.4375 2.125 14.5508 5.30566 20.9736 9.30273
c7.97656 4.95215 18.2861 -0.825195 18.2861 -10.2139v-42.5957c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h132c6.62695 0 12 -5.37305 12 -12v-4.48633c0 -4.91699 -2.9873 -9.36914 -7.56934 -11.1514
c-13.7021 -5.33105 -26.3955 -11.5371 -38.0498 -18.585c-1.82715 -1.11523 -3.98633 -1.76953 -6.28027 -1.77734h-86.1006c-3.31152 0 -6 -2.68848 -6 -6v-340c0 -3.31152 2.68848 -6 6 -6h340c3.31152 0 6 2.68848 6 6v25.9658c0 5.37012 3.5791 10.0596 8.74023 11.541z
" />
<glyph glyph-name="compass" unicode="&#xf14e;" horiz-adv-x="496"
d="M347.94 318.14c16.6592 7.61035 33.8096 -9.54004 26.1992 -26.1992l-65.9697 -144.341c-3.19238 -6.9834 -8.78613 -12.5771 -15.7695 -15.7695l-144.341 -65.9697c-16.6592 -7.61035 -33.8096 9.5498 -26.1992 26.1992l65.9697 144.341
c3.19238 6.9834 8.78613 12.5771 15.7695 15.7695zM270.58 169.42c12.4697 12.4697 12.4697 32.6904 0 45.1602s-32.6904 12.4697 -45.1602 0s-12.4697 -32.6904 0 -45.1602s32.6904 -12.4697 45.1602 0zM248 440c136.97 0 248 -111.03 248 -248s-111.03 -248 -248 -248
s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="caret-square-down" unicode="&#xf150;" horiz-adv-x="448"
d="M125.1 240h197.801c10.6992 0 16.0996 -13 8.5 -20.5l-98.9004 -98.2998c-4.7002 -4.7002 -12.2002 -4.7002 -16.9004 0l-98.8994 98.2998c-7.7002 7.5 -2.2998 20.5 8.39941 20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-up" unicode="&#xf151;" horiz-adv-x="448"
d="M322.9 144h-197.801c-10.6992 0 -16.0996 13 -8.5 20.5l98.9004 98.2998c4.7002 4.7002 12.2002 4.7002 16.9004 0l98.8994 -98.2998c7.7002 -7.5 2.2998 -20.5 -8.39941 -20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-right" unicode="&#xf152;" horiz-adv-x="448"
d="M176 93.0996v197.801c0 10.6992 13 16.0996 20.5 8.5l98.2998 -98.9004c4.7002 -4.7002 4.7002 -12.2002 0 -16.9004l-98.2998 -98.8994c-7.5 -7.7002 -20.5 -2.2998 -20.5 8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="file" unicode="&#xf15b;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416z" />
<glyph glyph-name="file-alt" unicode="&#xf15c;" horiz-adv-x="384"
d="M288 200v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v28c0 6.59961 5.40039 12 12 12h168c6.59961 0 12 -5.40039 12 -12zM276 128c6.59961 0 12 -5.40039 12 -12v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12
v28c0 6.59961 5.40039 12 12 12h168zM384 316.1v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996l83.9004 -83.9004c9 -8.90039 14.0996 -21.2002 14.0996 -33.9004z
M256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288z" />
<glyph glyph-name="thumbs-up" unicode="&#xf164;"
d="M466.27 161.31c4.6748 -22.6465 0.864258 -44.5371 -8.98926 -62.9893c2.95898 -23.8682 -4.02148 -48.5654 -17.3398 -66.9902c-0.954102 -55.9072 -35.8232 -95.3301 -112.94 -95.3301c-7 0 -15 0.00976562 -22.2197 0.00976562
c-102.742 0 -133.293 38.9395 -177.803 39.9404c-3.56934 -13.7764 -16.085 -23.9502 -30.9775 -23.9502h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h98.7598c19.1455 16.9531 46.0137 60.6533 68.7598 83.4004
c13.667 13.667 10.1533 108.6 71.7607 108.6c57.5801 0 95.2695 -31.9355 95.2695 -104.73c0 -18.4092 -3.92969 -33.7295 -8.84961 -46.5391h36.4795c48.6025 0 85.8203 -41.5654 85.8203 -85.5801c0 -19.1504 -4.95996 -34.9902 -13.7305 -49.8408zM404.52 107.48
c21.5811 20.3838 18.6992 51.0645 5.21094 65.6191c9.44922 0 22.3594 18.9102 22.2695 37.8105c-0.0898438 18.9102 -16.71 37.8203 -37.8203 37.8203h-103.989c0 37.8193 28.3594 55.3691 28.3594 94.5391c0 23.75 0 56.7305 -47.2695 56.7305
c-18.9102 -18.9102 -9.45996 -66.1797 -37.8203 -94.54c-26.5596 -26.5703 -66.1797 -97.46 -94.54 -97.46h-10.9199v-186.17c53.6113 0 100.001 -37.8203 171.64 -37.8203h37.8203c35.5117 0 60.8203 17.1201 53.1201 65.9004
c15.2002 8.16016 26.5 36.4395 13.9395 57.5703zM88 16c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="thumbs-down" unicode="&#xf165;"
d="M466.27 222.69c8.77051 -14.8506 13.7305 -30.6904 13.7305 -49.8408c0 -44.0146 -37.2178 -85.5801 -85.8203 -85.5801h-36.4795c4.91992 -12.8096 8.84961 -28.1299 8.84961 -46.5391c0 -72.7949 -37.6895 -104.73 -95.2695 -104.73
c-61.6074 0 -58.0938 94.9326 -71.7607 108.6c-22.7461 22.7471 -49.6133 66.4473 -68.7598 83.4004h-7.05176c-5.5332 -9.56152 -15.8662 -16 -27.708 -16h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h64c8.11328 0 15.5146 -3.02539 21.1553 -8
h10.8447c40.9971 0 73.1953 39.9902 176.78 39.9902c7.21973 0 15.2197 0.00976562 22.2197 0.00976562c77.1172 0 111.986 -39.4229 112.94 -95.3301c13.3184 -18.4248 20.2979 -43.1221 17.3398 -66.9902c9.85352 -18.4521 13.6641 -40.3428 8.98926 -62.9893zM64 152
c13.2549 0 24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24zM394.18 135.27c21.1104 0 37.7305 18.9102 37.8203 37.8203c0.0898438 18.9004 -12.8203 37.8105 -22.2695 37.8105c13.4883 14.5547 16.3701 45.2354 -5.21094 65.6191
c12.5605 21.1309 1.26074 49.4102 -13.9395 57.5703c7.7002 48.7803 -17.6084 65.9004 -53.1201 65.9004h-37.8203c-71.6387 0 -118.028 -37.8203 -171.64 -37.8203v-186.17h10.9199c28.3604 0 67.9805 -70.8896 94.54 -97.46
c28.3604 -28.3604 18.9102 -75.6299 37.8203 -94.54c47.2695 0 47.2695 32.9805 47.2695 56.7305c0 39.1699 -28.3594 56.7197 -28.3594 94.5391h103.989z" />
<glyph glyph-name="sun" unicode="&#xf185;"
d="M494.2 226.1c11.2002 -7.59961 17.7998 -20.0996 17.8994 -33.6992c0 -13.4004 -6.69922 -26 -17.7998 -33.5l-59.7998 -40.5l13.7002 -71c2.5 -13.2002 -1.60059 -26.8008 -11.1006 -36.3008s-22.8994 -13.7998 -36.2998 -11.0996l-70.8994 13.7002l-40.4004 -59.9004
c-7.5 -11.0996 -20.0996 -17.7998 -33.5 -17.7998s-26 6.7002 -33.5 17.9004l-40.4004 59.8994l-70.7998 -13.7002c-13.3994 -2.59961 -26.7998 1.60059 -36.2998 11.1006s-13.7002 23.0996 -11.0996 36.2998l13.6992 71l-59.7998 40.5
c-11.0996 7.5 -17.7998 20 -17.7998 33.5s6.59961 26 17.7998 33.5996l59.7998 40.5l-13.6992 71c-2.60059 13.2002 1.59961 26.7002 11.0996 36.3008c9.5 9.59961 23 13.6992 36.2998 11.1992l70.7998 -13.6992l40.4004 59.8994c15.0996 22.2998 51.9004 22.2998 67 0
l40.4004 -59.8994l70.8994 13.6992c13 2.60059 26.6006 -1.59961 36.2002 -11.0996c9.5 -9.59961 13.7002 -23.2002 11.0996 -36.4004l-13.6992 -71zM381.3 140.5l76.7998 52.0996l-76.7998 52l17.6006 91.1006l-91 -17.6006l-51.9004 76.9004l-51.7998 -76.7998
l-91 17.5996l17.5996 -91.2002l-76.7998 -52l76.7998 -52l-17.5996 -91.1992l90.8994 17.5996l51.9004 -77l51.9004 76.9004l91 -17.6006zM256 296c57.2998 0 104 -46.7002 104 -104s-46.7002 -104 -104 -104s-104 46.7002 -104 104s46.7002 104 104 104zM256 136
c30.9004 0 56 25.0996 56 56s-25.0996 56 -56 56s-56 -25.0996 -56 -56s25.0996 -56 56 -56z" />
<glyph glyph-name="moon" unicode="&#xf186;"
d="M279.135 -64c-141.424 0 -256 114.64 -256 256c0 141.425 114.641 256 256 256c16.0342 -0.00292969 31.5078 -1.46875 46.7354 -4.27734c44.0205 -8.13086 53.7666 -66.8691 15.0215 -88.9189c-41.374 -23.5439 -67.4336 -67.4121 -67.4336 -115.836
c0 -83.5234 75.9238 -146.475 158.272 -130.792c43.6904 8.32129 74.5186 -42.5693 46.248 -77.4004c-47.8613 -58.9717 -120.088 -94.7754 -198.844 -94.7754zM279.135 400c-114.875 0 -208 -93.125 -208 -208s93.125 -208 208 -208
c65.2314 0 123.439 30.0361 161.575 77.0244c-111.611 -21.2568 -215.252 64.0957 -215.252 177.943c0 67.5127 36.9326 126.392 91.6934 157.555c-12.3271 2.27637 -25.0312 3.47754 -38.0166 3.47754z" />
<glyph glyph-name="caret-square-left" unicode="&#xf191;" horiz-adv-x="448"
d="M272 290.9v-197.801c0 -10.6992 -13 -16.0996 -20.5 -8.5l-98.2998 98.9004c-4.7002 4.7002 -4.7002 12.2002 0 16.9004l98.2998 98.8994c7.5 7.7002 20.5 2.2998 20.5 -8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="dot-circle" unicode="&#xf192;"
d="M256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200zM256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248z
M256 272c44.1826 0 80 -35.8174 80 -80s-35.8174 -80 -80 -80s-80 35.8174 -80 80s35.8174 80 80 80z" />
<glyph glyph-name="building" unicode="&#xf1ad;" horiz-adv-x="448"
d="M128 300v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12zM268 288c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40
c0 -6.59961 -5.40039 -12 -12 -12h-40zM140 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM268 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM192 108c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM268 96c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.59961 5.40039 12 12 12h19.5v440c0 13.2998 10.7002 24 24 24h337c13.2998 0 24 -10.7002 24 -24v-440h19.5
c6.59961 0 12 -5.40039 12 -12zM79.5 -15h112.5v67c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-67h112.5v414l-288.5 1z" />
<glyph glyph-name="file-pdf" unicode="&#xf1c1;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM298.2 127.7c10.5 -10.5 8 -38.7002 -17.5 -38.7002c-14.7998 0 -36.9004 6.7998 -55.7998 17c-21.6006 -3.59961 -46 -12.7002 -68.4004 -20.0996c-50.0996 -86.4004 -79.4004 -47 -76.0996 -31.2002
c4 20 31 35.8994 51 46.2002c10.5 18.3994 25.3994 50.5 35.3994 74.3994c-7.39941 28.6006 -11.3994 51 -7 67.1006c4.7998 17.6992 38.4004 20.2998 42.6006 -5.90039c4.69922 -15.4004 -1.5 -39.9004 -5.40039 -56c8.09961 -21.2998 19.5996 -35.7998 36.7998 -46.2998
c17.4004 2.2002 52.2002 5.5 64.4004 -6.5zM100.1 49.9004c0 -0.700195 11.4004 4.69922 30.4004 35c-5.90039 -5.5 -25.2998 -21.3008 -30.4004 -35zM181.7 240.5c-2.5 0 -2.60059 -26.9004 1.7998 -40.7998c4.90039 8.7002 5.59961 40.7998 -1.7998 40.7998zM157.3 103.9
c15.9004 6.09961 34 14.8994 54.7998 19.1992c-11.1992 8.30078 -21.7998 20.4004 -30.0996 35.5c-6.7002 -17.6992 -15 -37.7998 -24.7002 -54.6992zM288.9 108.9c3.59961 2.39941 -2.2002 10.3994 -37.3008 7.7998c32.3008 -13.7998 37.3008 -7.7998 37.3008 -7.7998z" />
<glyph glyph-name="file-word" unicode="&#xf1c2;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM268.1 192v0.200195h15.8008c7.7998 0 13.5 -7.2998 11.5996 -14.9004c-4.2998 -17 -13.7002 -54.0996 -34.5 -136c-1.2998 -5.39941 -6.09961 -9.09961 -11.5996 -9.09961h-24.7002
c-5.5 0 -10.2998 3.7998 -11.6006 9.09961c-5.2998 20.9004 -17.7998 71 -17.8994 71.4004l-2.90039 17.2998c-0.5 -5.2998 -1.5 -11.0996 -3 -17.2998l-17.8994 -71.4004c-1.30078 -5.39941 -6.10059 -9.09961 -11.6006 -9.09961h-25.2002
c-5.59961 0 -10.3994 3.7002 -11.6992 9.09961c-6.5 26.5 -25.2002 103.4 -33.2002 136c-1.7998 7.5 3.89941 14.7998 11.7002 14.7998h16.7998c5.7998 0 10.7002 -4.09961 11.7998 -9.69922c5 -25.7002 18.4004 -93.8008 19.0996 -99
c0.300781 -1.7002 0.400391 -3.10059 0.5 -4.2002c0.800781 7.5 0.400391 4.7002 24.8008 103.7c1.39941 5.2998 6.19922 9.09961 11.6992 9.09961h13.3008c5.59961 0 10.3994 -3.7998 11.6992 -9.2002c23.9004 -99.7002 22.8008 -94.3994 23.6006 -99.5
c0.299805 -1.7002 0.5 -3.09961 0.700195 -4.2998c0.599609 8.09961 0.399414 5.7998 21 103.5c1.09961 5.5 6 9.5 11.6992 9.5z" />
<glyph glyph-name="file-excel" unicode="&#xf1c3;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM260 224c9.2002 0 15 -10 10.2998 -18c-16 -27.5 -45.5996 -76.9004 -46.2998 -78l46.4004 -78c4.59961 -8 -1.10059 -18 -10.4004 -18h-28.7998c-4.40039 0 -8.5 2.40039 -10.6006 6.2998
c-22.6992 41.7998 -13.6992 27.5 -28.5996 57.7002c-5.59961 -12.7002 -6.90039 -17.7002 -28.5996 -57.7002c-2.10059 -3.89941 -6.10059 -6.2998 -10.5 -6.2998h-28.9004c-9.2998 0 -15.0996 10 -10.4004 18l46.3008 78l-46.3008 78c-4.59961 8 1.10059 18 10.4004 18
h28.9004c4.39941 0 8.5 -2.40039 10.5996 -6.2998c21.7002 -40.4004 14.7002 -28.6006 28.5996 -57.7002c6.40039 15.2998 10.6006 24.5996 28.6006 57.7002c2.09961 3.89941 6.09961 6.2998 10.5 6.2998h28.7998z" />
<glyph glyph-name="file-powerpoint" unicode="&#xf1c4;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM120 44v168c0 6.59961 5.40039 12 12 12h69.2002c36.7002 0 62.7998 -27 62.7998 -66.2998c0 -74.2998 -68.7002 -66.5 -95.5 -66.5v-47.2002c0 -6.59961 -5.40039 -12 -12 -12h-24.5c-6.59961 0 -12 5.40039 -12 12z
M168.5 131.4h23c7.90039 0 13.9004 2.39941 18.0996 7.19922c8.5 9.80078 8.40039 28.5 0.100586 37.8008c-4.10059 4.59961 -9.90039 7 -17.4004 7h-23.8994v-52h0.0996094z" />
<glyph glyph-name="file-image" unicode="&#xf1c5;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM80 32v64l39.5 39.5c4.7002 4.7002 12.2998 4.7002 17 0l39.5 -39.5l87.5 87.5c4.7002 4.7002 12.2998 4.7002 17 0l23.5 -23.5v-128h-224zM128 272c26.5 0 48 -21.5 48 -48s-21.5 -48 -48 -48s-48 21.5 -48 48
s21.5 48 48 48z" />
<glyph glyph-name="file-archive" unicode="&#xf1c6;" horiz-adv-x="384"
d="M128.3 288h32v-32h-32v32zM192.3 384v-32h-32v32h32zM128.3 352h32v-32h-32v32zM192.3 320v-32h-32v32h32zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1
c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-48.2998v-16h-32v16h-79.7002v-416h288zM194.2 182.3l17.2998 -87.7002c6.40039 -32.3994 -18.4004 -62.5996 -51.5 -62.5996
c-33.2002 0 -58 30.4004 -51.4004 62.9004l19.7002 97.0996v32h32v-32h22.1006c5.7998 0 10.6992 -4.09961 11.7998 -9.7002zM160.3 57.9004c17.9004 0 32.4004 12.0996 32.4004 27c0 14.8994 -14.5 27 -32.4004 27c-17.8994 0 -32.3994 -12.1006 -32.3994 -27
c0 -14.9004 14.5 -27 32.3994 -27zM192.3 256v-32h-32v32h32z" />
<glyph glyph-name="file-audio" unicode="&#xf1c7;" horiz-adv-x="384"
d="M369.941 350.059c8.68848 -8.68848 14.0586 -20.6943 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c13.2461 0 25.252 -5.37012 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM192 60.0244c0 -10.6914 -12.9258 -16.0459 -20.4854 -8.48535l-35.5146 35.9746h-28c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h28
l35.5146 36.9473c7.56055 7.56055 20.4854 2.20605 20.4854 -8.48535v-135.951zM233.201 107.154c9.05078 9.29688 9.05957 24.1328 0.000976562 33.4385c-22.1494 22.752 12.2344 56.2461 34.3945 33.4814c27.1982 -27.9404 27.2119 -72.4443 0.000976562 -100.401
c-21.793 -22.3857 -56.9463 10.3154 -34.3965 33.4814z" />
<glyph glyph-name="file-video" unicode="&#xf1c8;" horiz-adv-x="384"
d="M369.941 350.059c8.68848 -8.68848 14.0586 -20.6943 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c13.2461 0 25.252 -5.37012 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM276.687 195.303c10.0049 10.0049 27.3135 2.99707 27.3135 -11.3135v-111.976c0 -14.2939 -17.2959 -21.332 -27.3135 -11.3135l-52.6865 52.6738v-37.374
c0 -11.0459 -8.9541 -20 -20 -20h-104c-11.0459 0 -20 8.9541 -20 20v104c0 11.0459 8.9541 20 20 20h104c11.0459 0 20 -8.9541 20 -20v-37.374z" />
<glyph glyph-name="file-code" unicode="&#xf1c9;" horiz-adv-x="384"
d="M149.9 98.9004c3.5 -3.30078 3.69922 -8.90039 0.399414 -12.4004l-17.3994 -18.5996c-1.60059 -1.80078 -4 -2.80078 -6.40039 -2.80078c-2.2002 0 -4.40039 0.900391 -6 2.40039l-57.7002 54.0996c-3.7002 3.40039 -3.7002 9.30078 0 12.8008l57.7002 54.0996
c3.40039 3.2998 9 3.2002 12.4004 -0.400391l17.3994 -18.5996l0.200195 -0.200195c3.2002 -3.59961 2.7998 -9.2002 -0.799805 -12.3994l-32.7998 -28.9004l32.7998 -28.9004zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288
c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288zM209.6 234l24.4004 -7
c4.7002 -1.2998 7.40039 -6.2002 6 -10.9004l-54.7002 -188.199c-1.2998 -4.60059 -6.2002 -7.40039 -10.8994 -6l-24.4004 7.09961c-4.7002 1.2998 -7.40039 6.2002 -6 10.9004l54.7002 188.1c1.39941 4.7002 6.2002 7.40039 10.8994 6zM234.1 157.1
c-3.5 3.30078 -3.69922 8.90039 -0.399414 12.4004l17.3994 18.5996c3.30078 3.60059 8.90039 3.7002 12.4004 0.400391l57.7002 -54.0996c3.7002 -3.40039 3.7002 -9.30078 0 -12.8008l-57.7002 -54.0996c-3.5 -3.2998 -9.09961 -3.09961 -12.4004 0.400391
l-17.3994 18.5996l-0.200195 0.200195c-3.2002 3.59961 -2.7998 9.2002 0.799805 12.3994l32.7998 28.9004l-32.7998 28.9004z" />
<glyph glyph-name="life-ring" unicode="&#xf1cd;"
d="M256 -56c-136.967 0 -248 111.033 -248 248s111.033 248 248 248s248 -111.033 248 -248s-111.033 -248 -248 -248zM152.602 20.7197c63.2178 -38.3184 143.579 -38.3184 206.797 0l-53.4111 53.4111c-31.8467 -13.5215 -68.168 -13.5059 -99.9746 0zM336 192
c0 44.1123 -35.8877 80 -80 80s-80 -35.8877 -80 -80s35.8877 -80 80 -80s80 35.8877 80 80zM427.28 88.6016c38.3184 63.2178 38.3184 143.579 0 206.797l-53.4111 -53.4111c13.5215 -31.8467 13.5049 -68.168 0 -99.9746zM359.397 363.28
c-63.2168 38.3184 -143.578 38.3184 -206.796 0l53.4111 -53.4111c31.8457 13.5215 68.167 13.5049 99.9736 0zM84.7197 295.398c-38.3184 -63.2178 -38.3184 -143.579 0 -206.797l53.4111 53.4111c-13.5215 31.8467 -13.5059 68.168 0 99.9746z" />
<glyph glyph-name="paper-plane" unicode="&#xf1d8;"
d="M440 441.5c34.5996 19.9004 77.5996 -8.7998 71.5 -48.9004l-59.4004 -387.199c-2.2998 -14.5 -11.0996 -27.3008 -23.8994 -34.5c-7.2998 -4.10059 -15.4004 -6.2002 -23.6006 -6.2002c-6.19922 0 -12.3994 1.2002 -18.2998 3.59961l-111.899 46.2002l-43.8008 -59.0996
c-27.3994 -36.9004 -86.5996 -17.8008 -86.5996 28.5996v84.4004l-114.3 47.2998c-36.7998 15.0996 -40.1006 66 -5.7002 85.8994zM192 -16l36.5996 49.5l-36.5996 15.0996v-64.5996zM404.6 12.7002l59.4004 387.3l-416 -240l107.8 -44.5996l211.5 184.3
c14.2002 12.2998 34.4004 -5.7002 23.7002 -21.2002l-140.2 -202.3z" />
<glyph glyph-name="futbol" unicode="&#xf1e3;" horiz-adv-x="496"
d="M483.8 268.6c42.2998 -130.199 -29 -270.1 -159.2 -312.399c-25.5 -8.2998 -51.2998 -12.2002 -76.6992 -12.2002c-104.5 0 -201.7 66.5996 -235.7 171.4c-42.2998 130.199 29 270.1 159.2 312.399c25.5 8.2998 51.2998 12.2002 76.6992 12.2002
c104.5 0 201.7 -66.5996 235.7 -171.4zM409.3 74.9004c6.10059 8.39941 12.1006 16.8994 16.7998 26.1992c14.3008 28.1006 21.5 58.5 21.7002 89.2002l-38.8994 36.4004l-71.1006 -22.1006l-24.3994 -75.1992l43.6992 -60.9004zM409.3 310.3
c-24.5 33.4004 -58.7002 58.4004 -97.8994 71.4004l-47.4004 -26.2002v-73.7998l64.2002 -46.5l70.7002 22zM184.9 381.6c-39.9004 -13.2998 -73.5 -38.5 -97.8008 -71.8994l10.1006 -52.5l70.5996 -22l64.2002 46.5v73.7998zM139 68.5l43.5 61.7002l-24.2998 74.2998
l-71.1006 22.2002l-39 -36.4004c0.5 -55.7002 23.4004 -95.2002 37.8008 -115.3zM187.2 1.5c64.0996 -20.4004 115.5 -1.7998 121.7 0l22.3994 48.0996l-44.2998 61.7002h-78.5996l-43.6006 -61.7002z" />
<glyph glyph-name="newspaper" unicode="&#xf1ea;" horiz-adv-x="576"
d="M552 384c13.2549 0 24 -10.7451 24 -24v-336c0 -13.2549 -10.7451 -24 -24 -24h-496c-30.9277 0 -56 25.0723 -56 56v272c0 13.2549 10.7451 24 24 24h42.752c6.60547 18.623 24.3896 32 45.248 32h440zM48 56c0 -4.41113 3.58887 -8 8 -8s8 3.58887 8 8v248h-16v-248z
M528 48v288h-416v-280c0 -2.7168 -0.204102 -5.38574 -0.578125 -8h416.578zM172 168c-6.62695 0 -12 5.37305 -12 12v96c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-96c0 -6.62695 -5.37305 -12 -12 -12h-136zM200 248v-40h80v40h-80zM160 108v24
c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-136c-6.62695 0 -12 5.37305 -12 12zM352 108v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104
c-6.62695 0 -12 5.37305 -12 12zM352 252v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12zM352 180v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24
c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12z" />
<glyph glyph-name="bell-slash" unicode="&#xf1f6;" horiz-adv-x="640"
d="M633.99 -23.0195c6.91016 -5.52051 8.01953 -15.5908 2.5 -22.4902l-10 -12.4902c-5.53027 -6.88965 -15.5898 -8.00977 -22.4902 -2.49023l-598 467.51c-6.90039 5.52051 -8.01953 15.5908 -2.49023 22.4902l10 12.4902
c5.52051 6.90039 15.5898 8.00977 22.4902 2.49023zM163.53 80h182.84l61.3994 -48h-279.659c-19.1201 0 -31.9902 15.5996 -32.1006 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c18.3701 19.7402 51.5703 49.6904 54.8398 140.42l45.4697 -35.5498
c-6.91992 -54.7803 -24.6895 -88.5498 -41.3994 -110.58zM320 352c-23.3496 0 -45 -7.17969 -62.9404 -19.4004l-38.1699 29.8408c19.6807 15.7793 43.1104 27.3096 69.1299 32.7197v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398
c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -41.3604 6.03027 -70.7197 14.3398 -92.8496l-59.5293 46.54c-1.63086 13.96 -2.77051 28.8896 -2.79004 45.7295c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112zM320 -64
c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="copyright" unicode="&#xf1f9;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M363.351 93.0645c-9.61328 -9.71289 -45.5293 -41.3965 -104.064 -41.3965c-82.4297 0 -140.484 61.4248 -140.484 141.567c0 79.1514 60.2754 139.4 139.763 139.4c55.5303 0 88.7373 -26.6201 97.5928 -34.7783c2.37793 -2.1875 3.86914 -5.3252 3.86914 -8.80762
c0 -2.39746 -0.717773 -4.64258 -1.93359 -6.51465l-18.1543 -28.1133c-3.8418 -5.9502 -11.9668 -7.28223 -17.499 -2.9209c-8.5957 6.77637 -31.8145 22.5381 -61.708 22.5381c-48.3037 0 -77.916 -35.3301 -77.916 -80.082c0 -41.5889 26.8877 -83.6924 78.2764 -83.6924
c32.6572 0 56.8428 19.0391 65.7266 27.2256c5.26953 4.85645 13.5957 4.03906 17.8193 -1.73828l19.8652 -27.1699c1.45996 -1.98145 2.32422 -4.42969 2.32422 -7.07715c0 -3.28809 -1.32422 -6.2793 -3.47656 -8.44043z" />
<glyph glyph-name="closed-captioning" unicode="&#xf20a;"
d="M464 384c26.5 0 48 -21.5 48 -48v-288c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h416zM458 48c3.2998 0 6 2.7002 6 6v276c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-276c0 -3.2998 2.7002 -6 6 -6h404z
M246.9 133.7c1.69922 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.6006 -56.7998 -172.801 -32.0996 -172.801 67.9004c0 97.2998 121.7 119.5 172.5 70.0996c2.10059 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.89941 -3.09961 -6.19922 -4 -9.09961 -1.7002
c-40.7998 32 -94.5996 14.9004 -94.5996 -31.1992c0 -48 51 -70.5 92.1992 -32.6006c2.80078 2.5 7.10059 2.10059 9.2002 -0.899414zM437.3 133.7c1.7002 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.5996 -56.9004 -172.8 -32.0996 -172.8 67.9004
c0 97.2998 121.7 119.5 172.5 70.0996c2.09961 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.90039 -3.09961 -6.2002 -4 -9.09961 -1.7002c-40.8008 32 -94.6006 14.9004 -94.6006 -31.1992c0 -48 51 -70.5 92.2002 -32.6006c2.7998 2.5 7.09961 2.10059 9.2002 -0.899414z
" />
<glyph glyph-name="object-group" unicode="&#xf247;"
d="M500 320h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v256h-12
c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM448 384v-32h32v32h-32zM32 384v-32h32v32h-32zM64 0v32
h-32v-32h32zM480 0v32h-32v-32h32zM440 64v256h-12c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h12zM404 256c6.62695 0 12 -5.37207 12 -12v-168
c0 -6.62793 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37207 -12 12v52h-84c-6.62695 0 -12 5.37207 -12 12v168c0 6.62793 5.37305 12 12 12h200c6.62695 0 12 -5.37207 12 -12v-52h84zM136 280v-112h144v112h-144zM376 104v112h-56v-76
c0 -6.62793 -5.37305 -12 -12 -12h-76v-24h144z" />
<glyph glyph-name="object-ungroup" unicode="&#xf248;" horiz-adv-x="576"
d="M564 224h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v24h-88v-12
c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h72
c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-12v-24h88v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM352 384v-32h32v32h-32zM352 128v-32h32v32h-32zM64 96v32h-32v-32h32zM64 352v32
h-32v-32h32zM96 136h224v12c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-12zM224 0v32h-32v-32h32zM504 64v160h-12c-6.62695 0 -12 5.37305 -12 12v12
h-88v-88h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-88v-24h12c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h12zM544 0v32h-32v-32h32zM544 256v32h-32v-32h32z" />
<glyph glyph-name="sticky-note" unicode="&#xf249;" horiz-adv-x="448"
d="M448 99.8936c0 -13.2451 -5.37012 -25.252 -14.0586 -33.9404l-83.8828 -83.8818c-8.68848 -8.68848 -20.6943 -14.0596 -33.9404 -14.0596h-268.118c-26.5098 0 -48 21.4902 -48 48v351.988c0 26.5098 21.4902 48 48 48h352c26.5098 0 48 -21.4902 48 -48v-268.106z
M320 19.8936l76.1182 76.1182h-76.1182v-76.1182zM400 368h-352v-351.988h224v104c0 13.2549 10.7451 24 24 24h104v223.988z" />
<glyph glyph-name="clone" unicode="&#xf24d;"
d="M464 448c26.5098 0 48 -21.4902 48 -48v-320c0 -26.5098 -21.4902 -48 -48 -48h-48v-48c0 -26.5098 -21.4902 -48 -48 -48h-320c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h48v48c0 26.5098 21.4902 48 48 48h320zM362 -16c3.31152 0 6 2.68848 6 6
v42h-224c-26.5098 0 -48 21.4902 -48 48v224h-42c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308zM458 80c3.31152 0 6 2.68848 6 6v308c0 3.31152 -2.68848 6 -6 6h-308c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308z" />
<glyph glyph-name="hourglass" unicode="&#xf254;" horiz-adv-x="384"
d="M368 400c0 -80.0996 -31.8984 -165.619 -97.1797 -208c64.9912 -42.1934 97.1797 -127.436 97.1797 -208h4c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-360c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h4
c0 80.0996 31.8994 165.619 97.1797 208c-64.9912 42.1934 -97.1797 127.436 -97.1797 208h-4c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h360c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-4zM64 400
c0 -101.621 57.3066 -184 128 -184s128 82.3799 128 184h-256zM320 -16c0 101.62 -57.3076 184 -128 184s-128 -82.3799 -128 -184h256z" />
<glyph glyph-name="hand-rock" unicode="&#xf255;"
d="M408.864 368.948c48.8213 20.751 103.136 -15.0723 103.136 -67.9111v-114.443c0 -15.3955 -3.08887 -30.3906 -9.18262 -44.5674l-42.835 -99.6562c-4.99707 -11.625 -3.98242 -18.8574 -3.98242 -42.3701c0 -17.6729 -14.3271 -32 -32 -32h-252
c-17.6729 0 -32 14.3271 -32 32c0 27.3301 1.1416 29.2012 -3.11035 32.9033l-97.71 85.0811c-24.8994 21.6797 -39.1797 52.8926 -39.1797 85.6338v56.9531c0 47.4277 44.8457 82.0215 91.0459 71.1807c1.96094 55.751 63.5107 87.8262 110.671 60.8057
c29.1895 31.0713 78.8604 31.4473 108.334 -0.0214844c32.7051 18.6846 76.4121 10.3096 98.8135 -23.5879zM464 186.594v114.445c0 34.29 -52 33.8232 -52 0.676758c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v26.751
c0 34.457 -52 33.707 -52 0.676758v-27.4287c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v40.4658c0 34.3525 -52 33.8115 -52 0.677734v-41.1436c0 -8.83594 -7.16406 -16 -16 -16h-7c-8.83594 0 -16 7.16406 -16 16v26.751
c0 34.4023 -52 33.7744 -52 0.676758v-116.571c0 -8.83105 -7.17773 -15.9961 -16.0078 -15.9961c-4.0166 0 -7.68848 1.48242 -10.499 3.92969l-7 6.09473c-3.37012 2.93457 -5.49316 7.25293 -5.49316 12.0674v41.2275c0 34.2148 -52 33.8857 -52 0.677734v-56.9531
c0 -18.8555 8.27441 -36.874 22.7002 -49.4365l97.71 -85.0801c12.4502 -10.8398 19.5898 -26.4463 19.5898 -42.8164v-10.2861h220v7.07617c0 13.21 2.65332 26.0791 7.88281 38.25l42.835 99.6553c3.37891 7.82715 5.28223 16.501 5.28223 25.5625v0.0498047z" />
<glyph glyph-name="hand-paper" unicode="&#xf256;" horiz-adv-x="448"
d="M372.57 335.359c39.9062 5.63281 75.4297 -25.7393 75.4297 -66.3594v-131.564c-0.00292969 -15.7393 -1.80566 -30.9482 -5.19531 -45.666l-30.1836 -130.958c-3.34668 -14.5234 -16.2783 -24.8125 -31.1816 -24.8125h-222.897
c-10.7539 0 -20.2588 5.28613 -26.0615 13.4316l-119.97 168.415c-21.2441 29.8203 -14.8047 71.3574 14.5498 93.1533c18.7754 13.9395 42.1309 16.2979 62.083 8.87109v126.13c0 44.0547 41.125 75.5439 82.4053 64.9834c23.8926 48.1963 92.3535 50.2471 117.982 0.74707
c42.5186 11.1445 83.0391 -21.9346 83.0391 -65.5469v-10.8242zM399.997 137.437l-0.00195312 131.563c0 24.9492 -36.5703 25.5508 -36.5703 -0.691406v-76.3086c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v154.184
c0 25.501 -36.5703 26.3633 -36.5703 0.691406v-154.875c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v188.309c0 25.501 -36.5703 26.3545 -36.5703 0.691406v-189c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16
v153.309c0 25.501 -36.5713 26.3359 -36.5713 0.691406v-206.494c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832l-27.1279 38.0791c-14.3711 20.1709 -43.833 -2.33496 -29.3945 -22.6045l115.196 -161.697h201.92l27.3252 118.551
c2.63086 11.417 3.96484 23.1553 3.96484 34.8857z" />
<glyph glyph-name="hand-scissors" unicode="&#xf257;"
d="M256 -32c-44.9561 0 -77.3428 43.2627 -64.0244 85.8535c-21.6484 13.71 -34.0156 38.7617 -30.3408 65.0068h-87.6348c-40.8037 0 -74 32.8105 -74 73.1406c0 40.3291 33.1963 73.1396 74 73.1396l94 -9.14062l-78.8496 18.6787
c-38.3076 14.7422 -57.04 57.4707 -41.9424 95.1123c15.0303 37.4736 57.7549 55.7803 95.6416 41.2012l144.929 -55.7568c24.9551 30.5566 57.8086 43.9932 92.2178 24.7324l97.999 -54.8525c20.9746 -11.7393 34.0049 -33.8457 34.0049 -57.6904v-205.702
c0 -30.7422 -21.4404 -57.5576 -51.7979 -64.5537l-118.999 -27.4268c-4.97168 -1.14648 -10.0889 -1.72949 -15.2031 -1.72949zM256 16.0127l70 -0.000976562c1.52441 0 2.99707 0.174805 4.42285 0.501953l119.001 27.4277
c8.58203 1.97754 14.5762 9.29102 14.5762 17.7812v205.701c0 6.4873 -3.62109 12.542 -9.44922 15.8047l-98 54.8545c-8.13965 4.55566 -18.668 2.61914 -24.4873 -4.50781l-21.7646 -26.6475c-2.93457 -3.59375 -7.40332 -5.87305 -12.4004 -5.87305
c-2.02246 0 -3.95703 0.375977 -5.73828 1.06152l-166.549 64.0908c-32.6543 12.5664 -50.7744 -34.5771 -19.2227 -46.7168l155.357 -59.7852c6 -2.30859 10.2539 -8.12402 10.2539 -14.9326v-11.6328c0 -8.83691 -7.16309 -16 -16 -16h-182
c-34.375 0 -34.4297 -50.2803 0 -50.2803h182c8.83691 0 16 -7.16309 16 -16v-6.85645c0 -8.83691 -7.16309 -16 -16 -16h-28c-25.1221 0 -25.1592 -36.5674 0 -36.5674h28c8.83691 0 16 -7.16211 16 -16v-6.85547c0 -8.83691 -7.16309 -16 -16 -16
c-25.1201 0 -25.1602 -36.5674 0 -36.5674z" />
<glyph glyph-name="hand-lizard" unicode="&#xf258;" horiz-adv-x="576"
d="M556.686 157.458c12.6357 -19.4863 19.3145 -42.0615 19.3145 -65.2871v-124.171h-224v71.582l-99.751 38.7871c-2.7832 1.08203 -5.70996 1.63086 -8.69727 1.63086h-131.552c-30.8789 0 -56 25.1211 -56 56c0 48.5234 39.4766 88 88 88h113.709l18.333 48h-196.042
c-44.1123 0 -80 35.8877 -80 80v8c0 30.8779 25.1211 56 56 56h293.917c24.5 0 47.084 -12.2725 60.4111 -32.8291zM528 16v76.1709c0 0.0166016 -0.0439453 0.106445 -0.0439453 0.12207c0 14.3945 -4.24219 27.8057 -11.5439 39.0498l-146.358 225.715
c-4.44336 6.85254 -11.9707 10.9424 -20.1367 10.9424h-293.917c-4.41113 0 -8 -3.58887 -8 -8v-8c0 -17.6445 14.3555 -32 32 -32h213.471c25.2021 0 42.626 -25.293 33.6299 -48.8457l-24.5518 -64.2812c-7.05371 -18.4658 -25.0732 -30.873 -44.8398 -30.873h-113.709
c-22.0557 0 -40 -17.9443 -40 -40c0 -4.41113 3.58887 -8 8 -8h131.552c0.0175781 0 0.0712891 -0.0273438 0.0888672 -0.0273438c9.16992 0 17.9404 -1.72461 26.0039 -4.86621l99.752 -38.7881c18.5898 -7.22852 30.6035 -24.7881 30.6035 -44.7363v-23.582h128z" />
<glyph glyph-name="hand-spock" unicode="&#xf259;"
d="M501.03 331.824c6.92773 -11.1826 10.9697 -24.4053 10.9697 -38.5146c0 -5.92676 -0.706055 -11.6885 -2.03809 -17.208l-57.623 -241.963c-13.2236 -56.1904 -63.707 -98.1387 -123.908 -98.1387h-0.352539h-107.455
c-0.0761719 0 -0.193359 0.00195312 -0.270508 0.00195312c-40.9248 0 -78.1475 15.9814 -105.761 42.0391l-91.3652 85.9766c-14.3076 13.4434 -23.2246 32.5547 -23.2246 53.7168c0 19.5254 7.61035 37.2861 20.0254 50.4766
c5.31836 5.66406 29.875 29.3926 68.1152 21.8477l-24.3594 82.1973c-1.97363 6.64844 -2.97656 13.6836 -2.97656 20.9688c0 38.6953 29.8926 70.4639 67.8262 73.4531c-0.246094 2.45117 -0.34082 4.85547 -0.34082 7.37207c0 34.4199 23.585 63.376 55.4619 71.5752
c43.248 10.9785 80.5645 -17.7012 89.6602 -53.0723l13.6836 -53.207l4.64648 22.6602c6.99023 33.5186 36.6826 58.8037 72.2373 58.916c8.73438 0 56.625 -3.26953 70.7383 -54.0801c15.0664 0.710938 46.9199 -3.50977 66.3105 -35.0176zM463.271 287.219
c7.86914 32.9844 -42.1211 45.2695 -50.0859 11.9219l-24.8008 -104.146c-4.38867 -18.4141 -31.7783 -11.8926 -28.0557 6.2168l28.5479 139.166c7.39844 36.0703 -43.3076 45.0703 -50.1182 11.9629l-31.791 -154.971
c-3.54883 -17.3086 -28.2832 -18.0469 -32.7109 -0.804688l-47.3262 184.035c-8.43359 32.8105 -58.3691 20.2676 -49.8652 -12.8359l42.4414 -165.039c4.81641 -18.7207 -23.3711 -26.9121 -28.9648 -8.00781l-31.3438 105.779
c-9.6875 32.6465 -59.1191 18.2578 -49.3867 -14.625l36.0137 -121.539c6.59375 -22.2441 10.1777 -45.7803 10.1777 -70.1523c0 -6.54297 -8.05664 -10.9355 -13.4824 -5.82617l-51.123 48.1074c-24.7852 23.4082 -60.0527 -14.1875 -35.2793 -37.4902l91.3691 -85.9805
c19.0469 -17.9736 44.75 -28.998 72.9795 -28.998h0.157227h107.455c0.0732422 0 0.138672 0.0429688 0.212891 0.0429688c37.5791 0 69.1016 26.1416 77.3564 61.2168z" />
<glyph glyph-name="hand-pointer" unicode="&#xf25a;" horiz-adv-x="448"
d="M358.182 268.639c43.1934 16.6348 89.8184 -15.7949 89.8184 -62.6387v-84c-0.000976562 -5.24023 -0.600586 -10.3037 -1.72754 -15.2041l-27.4297 -118.999c-6.98242 -30.2969 -33.7549 -51.7969 -64.5566 -51.7969h-178.286
c-21.2588 0 -41.3682 10.4102 -53.791 27.8457l-109.699 154.001c-21.2432 29.8193 -14.8047 71.3574 14.5498 93.1523c18.8115 13.9658 42.1748 16.2822 62.083 8.87207v161.129c0 36.9443 29.7363 67 66.2861 67s66.2861 -30.0557 66.2861 -67v-73.6338
c20.4131 2.85742 41.4678 -3.94238 56.5947 -19.6289c27.1934 12.8467 60.3799 5.66992 79.8721 -19.0986zM80.9854 168.303c-14.4004 20.2119 -43.8008 -2.38281 -29.3945 -22.6055l109.712 -154c3.43457 -4.81934 8.92871 -7.69727 14.6973 -7.69727h178.285
c8.49219 0 15.8037 5.99414 17.7822 14.5762l27.4297 119.001c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285v84c0 25.1602 -36.5713 25.1211 -36.5713 0c0 -8.83594 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16406 -16 16v21
c0 25.1602 -36.5713 25.1201 -36.5713 0v-21c0 -8.83594 -7.16309 -16 -16 -16h-6.85938c-8.83691 0 -16 7.16406 -16 16v35c0 25.1602 -36.5703 25.1201 -36.5703 0v-35c0 -8.83594 -7.16309 -16 -16 -16h-6.85742c-8.83691 0 -16 7.16406 -16 16v175
c0 25.1602 -36.5713 25.1201 -36.5713 0v-241.493c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832zM176.143 48v96c0 8.83691 6.26855 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16zM251.571 48v96
c0 8.83691 6.26758 16 14 16h6c7.73145 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26855 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16zM327 48v96c0 8.83691 6.26758 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6
c-7.73242 0 -14 7.16309 -14 16z" />
<glyph glyph-name="hand-peace" unicode="&#xf25b;" horiz-adv-x="448"
d="M362.146 256.024c42.5908 13.3184 85.8535 -19.0684 85.8535 -64.0244l-0.0117188 -70.001c-0.000976562 -5.24023 -0.600586 -10.3027 -1.72949 -15.2031l-27.4268 -118.999c-6.99707 -30.3564 -33.8105 -51.7969 -64.5547 -51.7969h-205.702
c-23.8447 0 -45.9502 13.0303 -57.6904 34.0059l-54.8525 97.999c-19.2607 34.4092 -5.82422 67.2617 24.7324 92.2178l-55.7568 144.928c-14.5791 37.8867 3.72754 80.6113 41.2012 95.6416c37.6406 15.0977 80.3691 -3.63477 95.1123 -41.9424l18.6787 -78.8496
l-9.14062 94c0 40.8037 32.8096 74 73.1396 74s73.1406 -33.1963 73.1406 -74v-87.6348c26.2451 3.6748 51.2959 -8.69238 65.0068 -30.3408zM399.987 122l-0.000976562 70c0 25.1602 -36.5674 25.1201 -36.5674 0c0 -8.83691 -7.16309 -16 -16 -16h-6.85547
c-8.83789 0 -16 7.16309 -16 16v28c0 25.1592 -36.5674 25.1221 -36.5674 0v-28c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v182c0 34.4297 -50.2803 34.375 -50.2803 0v-182c0 -8.83691 -7.16309 -16 -16 -16h-11.6328
c-6.80859 0 -12.624 4.25391 -14.9326 10.2539l-59.7842 155.357c-12.1396 31.5518 -59.2842 13.4326 -46.7168 -19.2227l64.0898 -166.549c0.685547 -1.78125 1.07812 -3.71875 1.07812 -5.74121c0 -4.99707 -2.2959 -9.46289 -5.88965 -12.3975l-26.6475 -21.7646
c-7.12695 -5.81934 -9.06445 -16.3467 -4.50781 -24.4873l54.8535 -98c3.26367 -5.82812 9.31934 -9.44922 15.8057 -9.44922h205.701c8.49121 0 15.8037 5.99414 17.7812 14.5762l27.4277 119.001c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285z" />
<glyph glyph-name="registered" unicode="&#xf25d;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M366.442 73.791c4.40332 -7.99219 -1.37012 -17.791 -10.5107 -17.791h-42.8096c-0.00488281 0 -0.000976562 -0.0126953 -0.00585938 -0.0126953c-4.58594 0 -8.57422 2.58301 -10.5869 6.37305l-47.5156 89.3027h-31.958v-83.6631c0 -6.61719 -5.38281 -12 -12 -12
h-38.5674c-6.61719 0 -12 5.38281 -12 12v248.304c0 6.61719 5.38281 12 12 12h78.667c71.251 0 101.498 -32.749 101.498 -85.252c0 -31.6123 -15.2148 -59.2969 -39.4824 -73.1758c3.02148 -4.61719 0.225586 0.199219 53.2715 -96.085zM256.933 208.094
c20.9131 0 32.4307 11.5186 32.4316 32.4316c0 19.5752 -6.5127 31.709 -38.9297 31.709h-27.377v-64.1406h33.875z" />
<glyph glyph-name="calendar-plus" unicode="&#xf271;" horiz-adv-x="448"
d="M336 156v-24c0 -6.59961 -5.40039 -12 -12 -12h-76v-76c0 -6.59961 -5.40039 -12 -12 -12h-24c-6.59961 0 -12 5.40039 -12 12v76h-76c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h76v76c0 6.59961 5.40039 12 12 12h24c6.59961 0 12 -5.40039 12 -12
v-76h76c6.59961 0 12 -5.40039 12 -12zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-minus" unicode="&#xf272;" horiz-adv-x="448"
d="M124 120c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h200c6.59961 0 12 -5.40039 12 -12v-24c0 -6.59961 -5.40039 -12 -12 -12h-200zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-times" unicode="&#xf273;" horiz-adv-x="448"
d="M311.7 73.2998l-17 -17c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-53.7002 53.7998l-53.7002 -53.6992c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-17 17c-4.7002 4.69922 -4.7002 12.2998 0 17l53.7002 53.6992l-53.7002 53.7002c-4.7002 4.7002 -4.7002 12.2998 0 17
l17 17c4.7002 4.7002 12.2998 4.7002 17 0l53.7002 -53.7002l53.7002 53.7002c4.7002 4.7002 12.2998 4.7002 17 0l17 -17c4.7002 -4.7002 4.7002 -12.2998 0 -17l-53.7998 -53.7998l53.6992 -53.7002c4.80078 -4.7002 4.80078 -12.2998 0.100586 -17zM448 336v-352
c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10
v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-check" unicode="&#xf274;" horiz-adv-x="448"
d="M400 384c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h48v52c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-52h128v52c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-52h48zM394 -16c3.31152 0 6 2.68848 6 6v298h-352v-298c0 -3.31152 2.68848 -6 6 -6h340zM341.151 184.65l-142.31 -141.169c-4.70508 -4.66699 -12.3027 -4.6377 -16.9707 0.0673828l-75.0908 75.6992
c-4.66699 4.70508 -4.6377 12.3027 0.0673828 16.9707l22.7197 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l44.1035 -44.4609l111.072 110.182c4.70508 4.66699 12.3027 4.63672 16.9707 -0.0683594l22.5361 -22.7178
c4.66699 -4.70508 4.63672 -12.3027 -0.0683594 -16.9697z" />
<glyph glyph-name="map" unicode="&#xf279;" horiz-adv-x="576"
d="M560.02 416c8.4502 0 15.9805 -6.83008 15.9805 -16.0195v-346.32c0 -13.4707 -8.32422 -24.9951 -20.1201 -29.71l-151.83 -52.8105c-6.23242 -2.02832 -12.9023 -3.12305 -19.8076 -3.12305c-7.07324 0 -13.8799 1.15039 -20.2422 3.27344l-172 60.71l-170.05 -62.8398
c-1.99023 -0.790039 -4 -1.16016 -5.95996 -1.16016c-8.45996 0 -15.9902 6.83008 -15.9902 16.0195v346.32c0.00292969 13.4697 8.32617 24.9932 20.1201 29.71l151.83 52.8105c6.43945 2.08984 13.1201 3.13965 19.8096 3.13965
c7.06641 -0.00292969 13.8789 -1.16602 20.2402 -3.28027l172 -60.7197h0.00976562l170.05 62.8398c1.98047 0.790039 4 1.16016 5.95996 1.16016zM224 357.58v-285.97l128 -45.1904v285.97zM48 29.9502l127.36 47.0801l0.639648 0.229492v286.2l-128 -44.5303v-288.979z
M528 65.0801v288.97l-127.36 -47.0693l-0.639648 -0.240234v-286.19z" />
<glyph glyph-name="comment-alt" unicode="&#xf27a;"
d="M448 448c35.2998 0 64 -28.7002 64 -64v-288c0 -35.2998 -28.7002 -64 -64 -64h-144l-124.9 -93.5996c-2.19922 -1.7002 -4.69922 -2.40039 -7.09961 -2.40039c-6.2002 0 -12 4.90039 -12 12v84h-96c-35.2998 0 -64 28.7002 -64 64v288c0 35.2998 28.7002 64 64 64h384z
M464 96v288c0 8.7998 -7.2002 16 -16 16h-384c-8.7998 0 -16 -7.2002 -16 -16v-288c0 -8.7998 7.2002 -16 16 -16h144v-60l67.2002 50.4004l12.7998 9.59961h160c8.7998 0 16 7.2002 16 16z" />
<glyph glyph-name="pause-circle" unicode="&#xf28b;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16zM240 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="stop-circle" unicode="&#xf28d;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-160
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h160c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="handshake" unicode="&#xf2b5;" horiz-adv-x="640"
d="M519.2 320.1h120.8v-255.699h-64c-17.5 0 -31.7998 14.1992 -31.9004 31.6992h-57.8994c-1.7998 -8.19922 -5.2998 -16.0996 -10.9004 -23l-26.2002 -32.2998c-15.7998 -19.3994 -41.8994 -25.5 -64 -16.7998c-13.5 -16.5996 -30.5996 -24 -48.7998 -24
c-15.0996 0 -28.5996 5.09961 -41.0996 15.9004c-31.7998 -21.9004 -74.7002 -21.3008 -105.601 3.7998l-84.5996 76.3994h-9.09961c-0.100586 -17.5 -14.3008 -31.6992 -31.9004 -31.6992h-64v255.699h118l47.5996 47.6006c10.5 10.3994 24.8008 16.2998 39.6006 16.2998
h226.8c15.4326 0 29.4326 -6.22168 39.5996 -16.2998zM48 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16zM438 103.3c2.7002 3.40039 2.2002 8.5 -1.2002 11.2998l-108.2 87.8008l-8.19922 -7.5
c-40.3008 -36.8008 -86.7002 -11.8008 -101.5 4.39941c-26.7002 29 -25 74.4004 4.39941 101.3l38.7002 35.5h-56.7002c-2 -0.799805 -3.7002 -1.5 -5.7002 -2.2998l-61.6992 -61.5996h-41.9004v-128.101h27.7002l97.2998 -88
c16.0996 -13.0996 41.4004 -10.5 55.2998 6.60059l15.6006 19.2002l36.7998 -31.5c3 -2.40039 12 -4.90039 18 2.39941l30 36.5l23.8994 -19.3994c3.5 -2.80078 8.5 -2.2002 11.3008 1.19922zM544 144.1v128h-44.7002l-61.7002 61.6006
c-1.39941 1.5 -3.39941 2.2998 -5.5 2.2998l-83.6992 -0.200195c-10 0 -19.6006 -3.7002 -27 -10.5l-65.6006 -60.0996c-9.7002 -8.7998 -10.5 -24 -1.2002 -33.9004c8.90039 -9.39941 25.1006 -8.7002 34.6006 0l55.2002 50.6006c6.5 5.89941 16.5996 5.5 22.5996 -1
l10.9004 -11.7002c6 -6.5 5.5 -16.6006 -1 -22.6006l-12.5 -11.3994l102.699 -83.4004c2.80078 -2.2998 5.40039 -4.89941 7.7002 -7.7002h69.2002zM592 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16z
" />
<glyph glyph-name="envelope-open" unicode="&#xf2b6;"
d="M494.586 283.484c10.6523 -8.80762 17.4141 -22.1064 17.4141 -36.9932v-262.491c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v262.515c0 14.9355 6.80469 28.2705 17.5146 37.0771c4.08008 3.35449 110.688 89.0996 135.15 108.549
c22.6992 18.1426 60.1299 55.8594 103.335 55.8594c43.4365 0 81.2314 -38.1914 103.335 -55.8594c23.5283 -18.707 130.554 -104.773 135.251 -108.656zM464 -10v253.632c0 0.00195312 0.00390625 0.000976562 0.00390625 0.00292969
c0 1.88184 -0.869141 3.56152 -2.22754 4.66016c-15.8633 12.8232 -108.793 87.5752 -132.366 106.316c-17.5527 14.0195 -49.7168 45.3887 -73.4102 45.3887c-23.6016 0 -55.2451 -30.8799 -73.4102 -45.3887c-23.5713 -18.7393 -116.494 -93.4795 -132.364 -106.293
c-1.40918 -1.13965 -2.22559 -2.85254 -2.22559 -4.66504v-253.653c0 -3.31152 2.68848 -6 6 -6h404c3.31152 0 6 2.68848 6 6zM432.009 177.704c4.24902 -5.15918 3.46484 -12.7949 -1.74512 -16.9814c-28.9746 -23.2822 -59.2734 -47.5967 -70.9287 -56.8623
c-22.6992 -18.1436 -60.1299 -55.8604 -103.335 -55.8604c-43.4521 0 -81.2871 38.2373 -103.335 55.8604c-11.2793 8.9668 -41.7441 33.4131 -70.9268 56.8643c-5.20996 4.1875 -5.99316 11.8223 -1.74512 16.9814l15.2578 18.5283
c4.17773 5.07227 11.6572 5.84277 16.7793 1.72559c28.6182 -23.001 58.5654 -47.0352 70.5596 -56.5713c17.5527 -14.0195 49.7168 -45.3887 73.4102 -45.3887c23.6016 0 55.2461 30.8799 73.4102 45.3887c11.9941 9.53516 41.9434 33.5703 70.5625 56.5684
c5.12207 4.11621 12.6016 3.3457 16.7783 -1.72656z" />
<glyph glyph-name="address-book" unicode="&#xf2b9;" horiz-adv-x="448"
d="M436 288h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48
h320c26.5 0 48 -21.5 48 -48v-48h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM368 -16v416h-320v-416h320zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM118.4 64
c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002
h-179.199z" />
<glyph glyph-name="address-card" unicode="&#xf2bb;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v352h-480v-352h480zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z
M118.4 64c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002
c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199zM360 128c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 192c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 256c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112z" />
<glyph glyph-name="user-circle" unicode="&#xf2bd;" horiz-adv-x="496"
d="M248 344c53 0 96 -43 96 -96s-43 -96 -96 -96s-96 43 -96 96s43 96 96 96zM248 200c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c49.7002 0 95.0996 18.2998 130.1 48.4004c-14.8994 23 -40.3994 38.5 -69.5996 39.5c-20.7998 -6.5 -40.5996 -9.60059 -60.5 -9.60059s-39.7002 3.2002 -60.5 9.60059c-29.2002 -0.900391 -54.7002 -16.5 -69.5996 -39.5c35 -30.1006 80.3994 -48.4004 130.1 -48.4004z
M410.7 76.0996c23.3994 32.7002 37.2998 72.7002 37.2998 115.9c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -43.2002 13.9004 -83.2002 37.2998 -115.9c24.5 31.4004 62.2002 51.9004 105.101 51.9004c10.1992 0 26.0996 -9.59961 57.5996 -9.59961
c31.5996 0 47.4004 9.59961 57.5996 9.59961c43 0 80.7002 -20.5 105.101 -51.9004z" />
<glyph glyph-name="id-badge" unicode="&#xf2c1;" horiz-adv-x="384"
d="M336 448c26.5 0 48 -21.5 48 -48v-416c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h288zM336 -16v416h-288v-416h288zM144 336c-8.7998 0 -16 7.2002 -16 16s7.2002 16 16 16h96c8.7998 0 16 -7.2002 16 -16s-7.2002 -16 -16 -16
h-96zM192 160c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM102.4 32c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8
c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199z" />
<glyph glyph-name="id-card" unicode="&#xf2c2;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v288h-480v-288h32.7998c-1 4.5 -0.799805 -3.59961 -0.799805 22.4004c0 31.7998 30.0996 57.5996 67.2002 57.5996
c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996c0 -26 0.0996094 -17.9004 -0.799805 -22.4004h224.8zM360 96c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16
c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 160c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 224c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM192 128c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z" />
<glyph glyph-name="window-maximize" unicode="&#xf2d0;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v234h-416v-234c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="window-minimize" unicode="&#xf2d1;"
d="M480 -32h-448c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32h448c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="window-restore" unicode="&#xf2d2;"
d="M464 448c26.5 0 48 -21.5 48 -48v-320c0 -26.5 -21.5 -48 -48 -48h-48v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v320c0 26.5 21.5 48 48 48h48v48c0 26.5 21.5 48 48 48h320zM368 -16v208h-320v-208h320zM464 80v320h-320v-48h224
c26.5 0 48 -21.5 48 -48v-224h48z" />
<glyph glyph-name="snowflake" unicode="&#xf2dc;" horiz-adv-x="448"
d="M440.1 92.7998c7.60059 -4.39941 10.1006 -14.2002 5.5 -21.7002l-7.89941 -13.8994c-4.40039 -7.7002 -14 -10.2998 -21.5 -5.90039l-39.2002 23l9.09961 -34.7002c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.5996l-15.2002 -4.09961
c-8.39941 -2.30078 -17.0996 2.7998 -19.2998 11.2998l-21.2998 81l-71.9004 42.2002v-84.5l58.2998 -59.3008c6.10059 -6.19922 6.10059 -16.3994 0 -22.5996l-11.0996 -11.2998c-6.09961 -6.2002 -16.0996 -6.2002 -22.2002 0l-24.8994 25.3994v-46.0996
c0 -8.7998 -7 -16 -15.7002 -16h-15.7002c-8.7002 0 -15.7002 7.2002 -15.7002 16v45.9004l-24.8994 -25.4004c-6.10059 -6.2002 -16.1006 -6.2002 -22.2002 0l-11.1006 11.2998c-6.09961 6.2002 -6.09961 16.4004 0 22.6006l58.3008 59.2998v84.5l-71.9004 -42.2002
l-21.2998 -81c-2.2998 -8.5 -10.9004 -13.5996 -19.2998 -11.2998l-15.2002 4.09961c-8.40039 2.2998 -13.2998 11.1006 -11.1006 19.6006l9.10059 34.6992l-39.2002 -23c-7.5 -4.39941 -17.2002 -1.7998 -21.5 5.90039l-7.90039 13.9004
c-4.2998 7.69922 -1.69922 17.5 5.80078 21.8994l39.1992 23l-34.0996 9.2998c-8.40039 2.30078 -13.2998 11.1006 -11.0996 19.6006l4.09961 15.5c2.2998 8.5 10.9004 13.5996 19.2998 11.2998l79.7002 -21.7002l71.9004 42.2002l-71.9004 42.2002l-79.7002 -21.7002
c-8.39941 -2.2998 -17.0996 2.7998 -19.2998 11.2998l-4.09961 15.5c-2.30078 8.5 2.69922 17.2998 11.0996 19.6006l34.0996 9.09961l-39.1992 23c-7.60059 4.5 -10.1006 14.2002 -5.80078 21.9004l7.90039 13.8994c4.40039 7.7002 14 10.2998 21.5 5.90039l39.2002 -23
l-9.10059 34.7002c-2.2998 8.5 2.7002 17.2998 11.1006 19.5996l15.2002 4.09961c8.39941 2.30078 17.0996 -2.7998 19.2998 -11.2998l21.2998 -81l71.9004 -42.2002v84.5l-58.3008 59.3008c-6.09961 6.19922 -6.09961 16.3994 0 22.5996l11.5 11.2998
c6.10059 6.2002 16.1006 6.2002 22.2002 0l24.9004 -25.3994v46.0996c0 8.7998 7 16 15.7002 16h15.6992c8.7002 0 15.7002 -7.2002 15.7002 -16v-45.9004l24.9004 25.4004c6.09961 6.2002 16.0996 6.2002 22.2002 0l11.0996 -11.2998
c6.09961 -6.2002 6.09961 -16.4004 0 -22.6006l-58.2998 -59.2998v-84.5l71.8994 42.2002l21.3008 81c2.2998 8.5 10.8994 13.5996 19.2998 11.2998l15.2002 -4.09961c8.39941 -2.2998 13.2998 -11.1006 11.0996 -19.6006l-9.09961 -34.6992l39.1992 23
c7.5 4.39941 17.2002 1.7998 21.5 -5.90039l7.90039 -13.9004c4.2998 -7.69922 1.7002 -17.5 -5.7998 -21.8994l-39.2002 -23l34.0996 -9.2998c8.40039 -2.30078 13.3008 -11.1006 11.1006 -19.6006l-4.10059 -15.5c-2.2998 -8.5 -10.8994 -13.5996 -19.2998 -11.2998
l-79.7002 21.7002l-71.8994 -42.2002l71.7998 -42.2002l79.7002 21.7002c8.39941 2.2998 17.0996 -2.7998 19.2998 -11.2998l4.09961 -15.5c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.6006l-34.0996 -9.2998z" />
<glyph glyph-name="trash-alt" unicode="&#xf2ed;" horiz-adv-x="448"
d="M268 32c-6.62305 0 -12 5.37695 -12 12v216c0 6.62305 5.37695 12 12 12h24c6.62305 0 12 -5.37695 12 -12v-216c0 -6.62305 -5.37695 -12 -12 -12h-24zM432 368c8.83105 0 16 -7.16895 16 -16v-16c0 -8.83105 -7.16895 -16 -16 -16h-16v-336
c0 -26.4922 -21.5078 -48 -48 -48h-288c-26.4922 0 -48 21.5078 -48 48v336h-16c-8.83105 0 -16 7.16895 -16 16v16c0 8.83105 7.16895 16 16 16h82.4102l34.0195 56.7002c8.39258 13.9844 23.6777 23.2998 41.1602 23.2998h100.82
c0.0078125 0 -0.015625 0.0517578 -0.0078125 0.0517578c17.4824 0 32.7949 -9.36719 41.1875 -23.3516l34 -56.7002h82.4102zM171.84 397.09l-17.4502 -29.0898h139.221l-17.46 29.0898c-1.0498 1.74707 -2.95898 2.91016 -5.14355 2.91016h-0.00683594h-94
c-0.00585938 0 -0.00683594 0.00683594 -0.0126953 0.00683594c-2.18457 0 -4.09766 -1.16992 -5.14746 -2.91699zM368 -16v336h-288v-336h288zM156 32c-6.62305 0 -12 5.37695 -12 12v216c0 6.62305 5.37695 12 12 12h24c6.62305 0 12 -5.37695 12 -12v-216
c0 -6.62305 -5.37695 -12 -12 -12h-24z" />
<glyph glyph-name="images" unicode="&#xf302;" horiz-adv-x="576"
d="M480 32v-16c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256c0 26.5098 21.4902 48 48 48h16v-48h-10c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v10h48zM522 368h-372
c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v244c0 3.31152 -2.68848 6 -6 6zM528 416c26.5098 0 48 -21.4902 48 -48v-256c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256
c0 26.5098 21.4902 48 48 48h384zM264 304c0 -22.0908 -17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40s40 -17.9092 40 -40zM192 208l39.5146 39.5146c4.68652 4.68652 12.2842 4.68652 16.9717 0l39.5137 -39.5146l103.515 103.515
c4.68652 4.68652 12.2842 4.68652 16.9717 0l71.5137 -71.5146v-80h-288v48z" />
<glyph glyph-name="clipboard" unicode="&#xf328;" horiz-adv-x="384"
d="M336 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h80c0 35.2998 28.7002 64 64 64s64 -28.7002 64 -64h80zM192 408c-13.2998 0 -24 -10.7002 -24 -24s10.7002 -24 24 -24s24 10.7002 24 24
s-10.7002 24 -24 24zM336 -10v340c0 3.2998 -2.7002 6 -6 6h-42v-36c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v36h-42c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h276c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="arrow-alt-circle-down" unicode="&#xf358;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM224 308c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-116
h67c10.7002 0 16.0996 -12.9004 8.5 -20.5l-99 -99c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-99 99c-7.5 7.59961 -2.2002 20.5 8.5 20.5h67v116z" />
<glyph glyph-name="arrow-alt-circle-left" unicode="&#xf359;"
d="M8 192c0 137 111 248 248 248s248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248zM456 192c0 110.5 -89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200zM384 212v-40c0 -6.59961 -5.40039 -12 -12 -12h-116v-67
c0 -10.7002 -12.9004 -16 -20.5 -8.5l-99 99c-4.7002 4.7002 -4.7002 12.2998 0 17l99 99c7.59961 7.59961 20.5 2.2002 20.5 -8.5v-67h116c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="arrow-alt-circle-right" unicode="&#xf35a;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM128 172v40c0 6.59961 5.40039 12 12 12h116v67
c0 10.7002 12.9004 16 20.5 8.5l99 -99c4.7002 -4.7002 4.7002 -12.2998 0 -17l-99 -99c-7.59961 -7.59961 -20.5 -2.2002 -20.5 8.5v67h-116c-6.59961 0 -12 5.40039 -12 12z" />
<glyph glyph-name="arrow-alt-circle-up" unicode="&#xf35b;"
d="M256 -56c-137 0 -248 111 -248 248s111 248 248 248s248 -111 248 -248s-111 -248 -248 -248zM256 392c-110.5 0 -200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200zM276 64h-40c-6.59961 0 -12 5.40039 -12 12v116h-67
c-10.7002 0 -16 12.9004 -8.5 20.5l99 99c4.7002 4.7002 12.2998 4.7002 17 0l99 -99c7.59961 -7.59961 2.2002 -20.5 -8.5 -20.5h-67v-116c0 -6.59961 -5.40039 -12 -12 -12z" />
<glyph glyph-name="gem" unicode="&#xf3a5;" horiz-adv-x="576"
d="M464 448c4.09961 0 7.7998 -2 10.0996 -5.40039l99.9004 -147.199c2.90039 -4.40039 2.59961 -10.1006 -0.700195 -14.2002l-276 -340.8c-4.7998 -5.90039 -13.7998 -5.90039 -18.5996 0l-276 340.8c-3.2998 4 -3.60059 9.7998 -0.700195 14.2002l100 147.199
c2.2002 3.40039 6 5.40039 10 5.40039h352zM444.7 400h-56.7998l51.6992 -96h68.4004zM242.6 400l-51.5996 -96h194l-51.7002 96h-90.7002zM131.3 400l-63.2998 -96h68.4004l51.6992 96h-56.7998zM88.2998 256l119.7 -160l-68.2998 160h-51.4004zM191.2 256l96.7998 -243.3
l96.7998 243.3h-193.6zM368 96l119.6 160h-51.3994z" />
<glyph glyph-name="money-bill-alt" unicode="&#xf3d1;" horiz-adv-x="640"
d="M320 304c53.0195 0 96 -50.1396 96 -112c0 -61.8701 -43 -112 -96 -112c-53.0195 0 -96 50.1504 -96 112c0 61.8604 42.9805 112 96 112zM360 136v16c0 4.41992 -3.58008 8 -8 8h-16v88c0 4.41992 -3.58008 8 -8 8h-13.5801
c-4.91113 0 -9.50586 -1.49316 -13.3096 -4.03027l-15.3301 -10.2197c-2.15332 -1.43262 -3.55957 -3.88379 -3.55957 -6.66113c0 -1.6377 0.493164 -3.16113 1.33887 -4.42871l8.88086 -13.3105c1.43164 -2.15234 3.88379 -3.55957 6.66113 -3.55957
c1.6377 0 3.16016 0.494141 4.42871 1.33984l0.469727 0.310547v-55.4404h-16c-4.41992 0 -8 -3.58008 -8 -8v-16c0 -4.41992 3.58008 -8 8 -8h64c4.41992 0 8 3.58008 8 8zM608 384c17.6699 0 32 -14.3301 32 -32v-320c0 -17.6699 -14.3301 -32 -32 -32h-576
c-17.6699 0 -32 14.3301 -32 32v320c0 17.6699 14.3301 32 32 32h576zM592 112v160c-35.3496 0 -64 28.6504 -64 64h-416c0 -35.3496 -28.6504 -64 -64 -64v-160c35.3496 0 64 -28.6504 64 -64h416c0 35.3496 28.6504 64 64 64z" />
<glyph glyph-name="window-close" unicode="&#xf410;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v340c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z
M356.5 253.4l-61.4004 -61.4004l61.4004 -61.4004c4.59961 -4.59961 4.59961 -12.0996 0 -16.7998l-22.2998 -22.2998c-4.60059 -4.59961 -12.1006 -4.59961 -16.7998 0l-61.4004 61.4004l-61.4004 -61.4004c-4.59961 -4.59961 -12.0996 -4.59961 -16.7998 0
l-22.2998 22.2998c-4.59961 4.60059 -4.59961 12.1006 0 16.7998l61.4004 61.4004l-61.4004 61.4004c-4.59961 4.59961 -4.59961 12.0996 0 16.7998l22.2998 22.2998c4.60059 4.59961 12.1006 4.59961 16.7998 0l61.4004 -61.4004l61.4004 61.4004
c4.59961 4.59961 12.0996 4.59961 16.7998 0l22.2998 -22.2998c4.7002 -4.60059 4.7002 -12.1006 0 -16.7998z" />
<glyph glyph-name="comment-dots" unicode="&#xf4ad;"
d="M144 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM256 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM368 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5
c-3.80078 8.7998 -2 19 4.59961 26c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160
c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="smile-wink" unicode="&#xf4da;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM365.8 138.4c10.2002 -8.5 11.6006 -23.6006 3.10059 -33.8008
c-30 -36 -74.1006 -56.5996 -120.9 -56.5996s-90.9004 20.5996 -120.9 56.5996c-8.39941 10.2002 -7.09961 25.3008 3.10059 33.8008c10.0996 8.39941 25.2998 7.09961 33.7998 -3.10059c20.7998 -25.0996 51.5 -39.3994 84 -39.3994s63.2002 14.3994 84 39.3994
c8.5 10.2002 23.5996 11.6006 33.7998 3.10059zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 268c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008l-9.5 8.5
c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996z" />
<glyph glyph-name="angry" unicode="&#xf556;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 136c33.5996 0 65.2002 -14.7998 86.7998 -40.5996
c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008c-10.6006 -8.89941 -25.7002 -6.69922 -33.7998 3c-24.8008 29.7002 -75 29.7002 -99.8008 0c-8.5 -10.1992 -23.5996 -11.5 -33.7998 -3s-11.5996 23.6006 -3.09961 33.8008
c21.5996 25.7998 53.2002 40.5996 86.7998 40.5996zM200 208c0 -17.7002 -14.2998 -32.0996 -32 -32.0996s-32 14.2998 -32 32c0 6.19922 2.2002 11.6992 5.2998 16.5996l-28.2002 8.5c-12.6992 3.7998 -19.8994 17.2002 -16.0996 29.9004
c3.7998 12.6992 17.0996 20 29.9004 16.0996l80 -24c12.6992 -3.7998 19.8994 -17.2002 16.0996 -29.9004c-3.09961 -10.3994 -12.7002 -17.0996 -23 -17.0996zM399 262.9c3.7998 -12.7002 -3.40039 -26.1006 -16.0996 -29.8008l-28.2002 -8.5
c3.09961 -4.89941 5.2998 -10.3994 5.2998 -16.5996c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c-10.2998 0 -19.9004 6.7002 -23 17.0996c-3.7998 12.7002 3.40039 26.1006 16.0996 29.9004l80 24c12.8008 3.7998 26.1006 -3.40039 29.9004 -16.0996z" />
<glyph glyph-name="dizzy" unicode="&#xf567;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM214.2 209.9
c-7.90039 -7.90039 -20.5 -7.90039 -28.4004 -0.200195l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998
c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.9004c7.7998 7.7998 20.5 7.7998 28.2998 0s7.7998 -20.5 0 -28.2998l-17.9004 -17.9004l17.9004 -17.7998c7.7998 -7.7998 7.7998 -20.5 0 -28.2998zM374.2 302.1
c7.7002 -7.7998 7.7002 -20.3994 0 -28.1992l-17.9004 -17.9004l17.7998 -18c7.80078 -7.7998 7.80078 -20.5 0 -28.2998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0
c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.7998c7.7998 7.80078 20.5 7.80078 28.2998 0zM248 176
c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64s-64 28.7002 -64 64s28.7002 64 64 64z" />
<glyph glyph-name="flushed" unicode="&#xf579;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM344 304c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80zM344 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM344 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM232 224c0 -44.2002 -35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80s80 -35.7998 80 -80zM152 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM152 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM312 104
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="frown-open" unicode="&#xf57a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 160c35.5996 0 88.7998 -21.2998 95.7998 -61.2002c2 -11.7998 -9.09961 -21.5996 -20.5 -18.0996
c-31.2002 9.59961 -59.3994 15.2998 -75.2998 15.2998s-44.0996 -5.7002 -75.2998 -15.2998c-11.5 -3.40039 -22.5 6.2998 -20.5 18.0996c7 39.9004 60.2002 61.2002 95.7998 61.2002z" />
<glyph glyph-name="grimace" unicode="&#xf57f;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM344 192c26.5 0 48 -21.5 48 -48v-32c0 -26.5 -21.5 -48 -48 -48h-192c-26.5 0 -48 21.5 -48 48v32c0 26.5 21.5 48 48 48
h192zM176 96v24h-40v-8c0 -8.7998 7.2002 -16 16 -16h24zM176 136v24h-24c-8.7998 0 -16 -7.2002 -16 -16v-8h40zM240 96v24h-48v-24h48zM240 136v24h-48v-24h48zM304 96v24h-48v-24h48zM304 136v24h-48v-24h48zM360 112v8h-40v-24h24c8.7998 0 16 7.2002 16 16zM360 136v8
c0 8.7998 -7.2002 16 -16 16h-24v-24h40z" />
<glyph glyph-name="grin" unicode="&#xf580;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="grin-alt" unicode="&#xf581;" horiz-adv-x="496"
d="M200.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56
c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM328.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0
c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200
s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006
s79.7002 4.7998 105.6 13.1006z" />
<glyph glyph-name="grin-beam" unicode="&#xf582;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM117.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996
l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002zM277.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998
c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002z" />
<glyph glyph-name="grin-beam-sweat" unicode="&#xf583;" horiz-adv-x="496"
d="M440 288c-29.5 0 -53.2998 26.2998 -53.2998 58.7002c0 25 31.7002 75.5 46.2002 97.2998c3.5 5.2998 10.5996 5.2998 14.1992 0c14.5 -21.7998 46.2002 -72.2998 46.2002 -97.2998c0 -32.4004 -23.7998 -58.7002 -53.2998 -58.7002zM248 48
c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM378.3 216.3
c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM483.6 269.2c8 -24.2998 12.4004 -50.2002 12.4004 -77.2002c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248
c45.7002 0 88.4004 -12.5996 125.2 -34.2002c-10.9004 -21.5996 -15.5 -36.2002 -17.2002 -45.7002c-31.2002 20.1006 -68.2002 31.9004 -108 31.9004c-110.3 0 -200 -89.7002 -200 -200s89.7002 -200 200 -200s200 89.7002 200 200
c0 22.5 -3.90039 44.0996 -10.7998 64.2998c0.399414 0 21.7998 -2.7998 46.3994 12.9004zM168 258.6c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-hearts" unicode="&#xf584;" horiz-adv-x="496"
d="M353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM200.8 192.3
l-70.2002 18.1006c-20.3994 5.2998 -31.8994 27 -24.1992 47.1992c6.69922 17.7002 26.6992 26.7002 44.8994 22l7.10059 -1.89941l2 7.09961c5.09961 18.1006 22.8994 30.9004 41.5 27.9004c21.3994 -3.40039 34.3994 -24.2002 28.7998 -44.5l-19.4004 -69.9004
c-1.2998 -4.5 -6 -7.2002 -10.5 -6zM389.6 257.6c7.7002 -20.1992 -3.7998 -41.7998 -24.1992 -47.0996l-70.2002 -18.2002c-4.60059 -1.2002 -9.2998 1.5 -10.5 6l-19.4004 69.9004c-5.59961 20.2998 7.40039 41.0996 28.7998 44.5c18.7002 3 36.5 -9.7998 41.5 -27.9004
l2 -7.09961l7.10059 1.89941c18.2002 4.7002 38.2002 -4.39941 44.8994 -22zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200
s89.7002 -200 200 -200z" />
<glyph glyph-name="grin-squint" unicode="&#xf585;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM118.9 184.2c-3.80078 4.39941 -3.90039 11 -0.100586 15.5l33.6006 40.2998
l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5c3.89941 4.40039 10.1992 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5zM361.8 181.7
l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5.10059 2.90039 11.5 1.90039 15.2998 -2.5c3.80078 -4.5 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5
c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5z" />
<glyph glyph-name="grin-squint-tears" unicode="&#xf586;"
d="M117.1 63.9004c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998
c16.8994 16.9004 75.0996 26.9004 100.899 30.6006zM75.9004 105.6c-19.6006 -3.89941 -35.1006 -8.09961 -47.3008 -12.1992c-39.2998 90.5996 -22.0996 199.899 52 274c48.5 48.3994 111.9 72.5996 175.4 72.5996c38.9004 0 77.7998 -9.2002 113.2 -27.4004
c-4 -12.1992 -8.2002 -28 -12 -48.2998c-30.4004 17.9004 -65 27.7002 -101.2 27.7002c-53.4004 0 -103.6 -20.7998 -141.4 -58.5996c-61.5996 -61.5 -74.2998 -153.4 -38.6992 -227.801zM428.2 293.2c20.2998 3.89941 36.2002 8 48.5 12
c47.8994 -93.2002 32.8994 -210.5 -45.2002 -288.601c-48.5 -48.3994 -111.9 -72.5996 -175.4 -72.5996c-33.6992 0 -67.2998 7 -98.6992 20.5996c4.19922 12.2002 8.2998 27.7002 12.1992 47.2002c26.6006 -12.7998 55.9004 -19.7998 86.4004 -19.7998
c53.4004 0 103.6 20.7998 141.4 58.5996c65.6992 65.7002 75.7998 166 30.7998 242.601zM394.9 320.1c-6.30078 -0.899414 -11.7002 4.5 -10.9004 10.9004c3.7002 25.7998 13.7002 84 30.5996 100.9c22 21.8994 57.9004 21.5 80.3008 -0.900391
c22.3994 -22.4004 22.7998 -58.4004 0.899414 -80.2998c-16.8994 -16.9004 -75.0996 -26.9004 -100.899 -30.6006zM207.9 211.8c3 -3 4.19922 -7.2998 3.19922 -11.5l-22.5996 -90.5c-1.40039 -5.39941 -6.2002 -9.09961 -11.7002 -9.09961h-0.899414
c-5.80078 0.5 -10.5 5.09961 -11 10.8994l-4.80078 52.3008l-52.2998 4.7998c-5.7998 0.5 -10.3994 5.2002 -10.8994 11c-0.400391 5.89941 3.39941 11.2002 9.09961 12.5996l90.5 22.7002c4.2002 1 8.40039 -0.200195 11.4004 -3.2002zM247.6 236.9
c-0.0996094 0 -6.39941 -1.80078 -11.3994 3.19922c-3 3 -4.2002 7.30078 -3.2002 11.4004l22.5996 90.5c1.40039 5.7002 7 9.2002 12.6006 9.09961c5.7998 -0.5 10.5 -5.09961 11 -10.8994l4.7998 -52.2998l52.2998 -4.80078c5.7998 -0.5 10.4004 -5.19922 10.9004 -11
c0.399414 -5.89941 -3.40039 -11.1992 -9.10059 -12.5996zM299.6 148.4c29.1006 29.0996 53 59.5996 65.3008 83.7998c4.89941 9.2998 17.5996 9.89941 23.3994 1.7002c27.7002 -38.9004 6.10059 -106.9 -30.5996 -143.7s-104.8 -58.2998 -143.7 -30.6006
c-8.2998 5.90039 -7.5 18.6006 1.7002 23.4004c24.2002 12.5 54.7998 36.2998 83.8994 65.4004z" />
<glyph glyph-name="grin-stars" unicode="&#xf587;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM125.7 200.9l6.09961 34.8994l-25.3994 24.6006
c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006l34.9004 -5c6.19922 -1 8.7998 -8.69922 4.2998 -13.1992l-25.4004 -24.6006l6 -34.8994c1 -6.2002 -5.39941 -11 -11 -7.90039
l-31.2998 16.2998l-31.2998 -16.2998c-5.60059 -3.09961 -12 1.7002 -11 7.90039zM385.4 273.6c6.19922 -1 8.89941 -8.59961 4.39941 -13.1992l-25.3994 -24.6006l6 -34.8994c1 -6.2002 -5.40039 -11 -11 -7.90039l-31.3008 16.2998l-31.2998 -16.2998
c-5.59961 -3.09961 -12 1.7002 -11 7.90039l6 34.8994l-25.3994 24.6006c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006z" />
<glyph glyph-name="grin-tears" unicode="&#xf588;" horiz-adv-x="640"
d="M117.1 191.9c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998c16.8994 16.9004 75.0996 26.9004 100.899 30.6006
zM623.8 161.3c21.9004 -21.8994 21.5 -57.8994 -0.799805 -80.2002c-22.4004 -22.3994 -58.4004 -22.7998 -80.2998 -0.899414c-16.9004 16.8994 -26.9004 75.0996 -30.6006 100.899c-0.899414 6.30078 4.5 11.7002 10.8008 10.8008
c25.7998 -3.7002 84 -13.7002 100.899 -30.6006zM497.2 99.5996c12.3994 -37.2998 25.0996 -43.7998 28.2998 -46.5c-44.5996 -65.7998 -120 -109.1 -205.5 -109.1s-160.9 43.2998 -205.5 109.1c3.09961 2.60059 15.7998 9.10059 28.2998 46.5
c33.4004 -63.8994 100.3 -107.6 177.2 -107.6s143.8 43.7002 177.2 107.6zM122.7 223.5c-2.40039 0.299805 -5 2.5 -49.5 -6.90039c12.3994 125.4 118.1 223.4 246.8 223.4s234.4 -98 246.8 -223.5c-44.2998 9.40039 -47.3994 7.2002 -49.5 7
c-15.2002 95.2998 -97.7998 168.5 -197.3 168.5s-182.1 -73.2002 -197.3 -168.5zM320 48c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996
c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM450.3 216.3c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM240 258.6
c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-tongue" unicode="&#xf589;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="grin-tongue-squint" unicode="&#xf58a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998
l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 3 11.5 1.90039 15.2998 -2.5zM214.2 250.3
c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5
c3.89941 4.5 10.2998 5.5 15.2998 2.5z" />
<glyph glyph-name="grin-tongue-wink" unicode="&#xf58b;" horiz-adv-x="496"
d="M152 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2998 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM328 320c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80s-80 35.7998 -80 80s35.7998 80 80 80zM328 192
c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM328 264c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248z
M312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998
c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3
c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998
c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998z" />
<glyph glyph-name="grin-wink" unicode="&#xf58c;" horiz-adv-x="496"
d="M328 268c25.6904 0 55.8799 -16.9199 59.8701 -42.1201c1.72949 -11.0898 -11.3506 -18.2695 -19.8301 -10.8398l-9.5498 8.47949c-14.8105 13.1904 -46.1602 13.1904 -60.9707 0l-9.5498 -8.47949c-8.33008 -7.40039 -21.5801 -0.379883 -19.8301 10.8398
c3.98047 25.2002 34.1699 42.1201 59.8604 42.1201zM168 208c-17.6699 0 -32 14.3301 -32 32s14.3301 32 32 32s32 -14.3301 32 -32s-14.3301 -32 -32 -32zM353.55 143.36c10.04 3.13965 19.3906 -5.4502 17.71 -15.3408
c-7.92969 -47.1494 -71.3193 -80.0195 -123.26 -80.0195s-115.33 32.8701 -123.26 80.0195c-1.69043 9.9707 7.76953 18.4707 17.71 15.3408c25.9297 -8.31055 64.3994 -13.0605 105.55 -13.0605s79.6201 4.75977 105.55 13.0605zM248 440c136.97 0 248 -111.03 248 -248
s-111.03 -248 -248 -248s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="kiss" unicode="&#xf596;" horiz-adv-x="496"
d="M168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM248 440c137 0 248 -111 248 -248
s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z
" />
<glyph glyph-name="kiss-beam" unicode="&#xf597;" horiz-adv-x="496"
d="M168 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM328 296
c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="kiss-wink-heart" unicode="&#xf598;" horiz-adv-x="504"
d="M304 139.5c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002
c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM374.5 223c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-2.5 -2.2998 -7.90039 -4.7002 -13.7002 -1.59961c-4.39941 2.39941 -6.89941 7.39941 -6.09961 12.3994
c3.89941 25.2002 34.2002 42.1006 59.7998 42.1006s55.7998 -16.9004 59.7998 -42.1006c0.799805 -5 -1.7002 -10 -6.09961 -12.3994c-4.40039 -2.40039 -9.90039 -1.7002 -13.7002 1.59961zM136 239.5c0 17.7002 14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32zM501.1 45.5c9.2002 -23.9004 -4.39941 -49.4004 -28.5 -55.7002l-83 -21.5c-5.39941 -1.39941 -10.8994 1.7998 -12.3994 7.10059l-22.9004 82.5996c-6.59961 24 8.7998 48.5996 34 52.5996c22 3.5 43.1006 -11.5996 49 -33l2.2998 -8.39941
l8.40039 2.2002c21.5996 5.59961 45.0996 -5.10059 53.0996 -25.9004zM334 11.7002c17.7002 -64 10.9004 -39.5 13.4004 -46.7998c-30.5 -13.4004 -64 -20.9004 -99.4004 -20.9004c-137 0 -248 111 -248 248s111 248 248 248s248 -111 247.9 -248
c0 -31.7998 -6.2002 -62.0996 -17.1006 -90c-6 1.5 -12.2002 2.7998 -18.5996 2.90039c-5.60059 9.69922 -13.6006 17.5 -22.6006 23.8994c6.7002 19.9004 10.4004 41.1006 10.4004 63.2002c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200
c30.7998 0 59.9004 7.2002 86 19.7002z" />
<glyph glyph-name="laugh" unicode="&#xf599;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM168 224
c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-beam" unicode="&#xf59a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.700195 -8.5 -10.7998 -11.8994 -14.8994 -4.5
l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006c-12.3008 0 -23.8008 -7.90039 -31.5 -21.6006l-9.5 -17c-4.10059 -7.39941 -15.6006 -4.09961 -14.9004 4.5c3.2998 42.1006 32.2002 71.4004 56 71.4004zM127 220.1c-4.2002 -7.39941 -15.7002 -4 -15.0996 4.5
c3.2998 42.1006 32.1992 71.4004 56 71.4004c23.7998 0 52.6992 -29.2998 56 -71.4004c0.699219 -8.5 -10.8008 -11.8994 -14.9004 -4.5l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006s-23.7998 -7.90039 -31.5 -21.6006zM362.4 160c8.19922 0 14.5 -7 13.5 -15
c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-squint" unicode="&#xf59b;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM343.6 252l33.6006 -40.2998c8.59961 -10.4004 -3.90039 -24.7998 -15.4004 -18l-80 48
c-7.7998 4.7002 -7.7998 15.8994 0 20.5996l80 48c11.6006 6.7998 24 -7.7002 15.4004 -18zM134.2 193.7c-11.6006 -6.7998 -24.1006 7.59961 -15.4004 18l33.6006 40.2998l-33.6006 40.2998c-8.59961 10.2998 3.7998 24.9004 15.4004 18l80 -48
c7.7998 -4.7002 7.7998 -15.8994 0 -20.5996zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-wink" unicode="&#xf59c;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 284c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008
l-9.5 8.5c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996zM168 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z
M362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="meh-blank" unicode="&#xf5a4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="meh-rolling-eyes" unicode="&#xf5a5;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM336 296c39.7998 0 72 -32.2002 72 -72s-32.2002 -72 -72 -72
s-72 32.2002 -72 72s32.2002 72 72 72zM336 184c22.0996 0 40 17.9004 40 40c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998
c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998c0 -22.0996 17.9004 -40 40 -40zM232 224c0 -39.7998 -32.2002 -72 -72 -72s-72 32.2002 -72 72s32.2002 72 72 72s72 -32.2002 72 -72zM120 224c0 -22.0996 17.9004 -40 40 -40s40 17.9004 40 40
c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998zM312 96
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="sad-cry" unicode="&#xf5b3;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM392 53.5996c34.5996 35.9004 56 84.7002 56 138.4c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -53.7002 21.4004 -102.4 56 -138.4v114.4
c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-151.4c28.5 -15.5996 61.2002 -24.5996 96 -24.5996s67.5 9 96 24.5996v151.4c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-114.4zM205.8 213.5c-5.7998 -3.2002 -11.2002 -0.700195 -13.7002 1.59961l-9.5 8.5
c-14.7998 13.2002 -46.1992 13.2002 -61 0l-9.5 -8.5c-3.7998 -3.39941 -9.2998 -4 -13.6992 -1.59961c-4.40039 2.40039 -6.90039 7.40039 -6.10059 12.4004c3.90039 25.1992 34.2002 42.0996 59.7998 42.0996c25.6006 0 55.8008 -16.9004 59.8008 -42.0996
c0.799805 -5 -1.7002 -10 -6.10059 -12.4004zM344 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2002 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM248 176c30.9004 0 56 -28.7002 56 -64s-25.0996 -64 -56 -64s-56 28.7002 -56 64s25.0996 64 56 64z" />
<glyph glyph-name="sad-tear" unicode="&#xf5b4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM256 144c38.0996 0 74 -16.7998 98.5 -46.0996
c8.5 -10.2002 7.09961 -25.3008 -3.09961 -33.8008c-10.6006 -8.7998 -25.7002 -6.69922 -33.8008 3.10059c-15.2998 18.2998 -37.7998 28.7998 -61.5996 28.7998c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM162.4 173.2c2.7998 3.7002 8.39941 3.7002 11.1992 0c11.4004 -15.2998 36.4004 -50.6006 36.4004 -68.1006
c0 -22.6992 -18.7998 -41.0996 -42 -41.0996s-42 18.4004 -42 41.0996c0 17.5 25 52.8008 36.4004 68.1006z" />
<glyph glyph-name="smile-beam" unicode="&#xf5b8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM332 135.4c8.5 10.1992 23.5996 11.5 33.7998 3.09961
c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.2002 8.40039 25.2998 7.09961 33.7998 -3.09961
c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004zM136.5 237l-9.5 -17c-1.90039 -3.2002 -5.90039 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996zM328 296c23.7998 0 52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002
c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="surprise" unicode="&#xf5c2;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 168c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64
s-64 28.7002 -64 64s28.7002 64 64 64zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="tired" unicode="&#xf5c8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5
l-33.6006 -40.2998l33.6006 -40.2998c3.7998 -4.5 3.7002 -11 -0.100586 -15.5c-3.5 -4.10059 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 2.90039 11.5 1.90039 15.2998 -2.5z
M220 240c0 -4.2002 -2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7998 -1.60059 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7998 4.5 -3.7002 11 0.100586 15.5
c3.7998 4.40039 10.2998 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998zM248 176c45.4004 0 100.9 -38.2998 107.8 -93.2998c1.5 -11.9004 -7 -21.6006 -15.5 -17.9004c-22.7002 9.7002 -56.2998 15.2002 -92.2998 15.2002
s-69.5996 -5.5 -92.2998 -15.2002c-8.60059 -3.7002 -17 6.10059 -15.5 17.9004c6.89941 55 62.3994 93.2998 107.8 93.2998z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "TSUN-Proxy",
"short_name": "Proxy",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Some files were not shown because too many files have changed in this diff Show More