diff --git a/.github/ISSUE_TEMPLATE/report-ahoy.md b/.github/ISSUE_TEMPLATE/report-ahoy.md index cae74fcd..1f361271 100644 --- a/.github/ISSUE_TEMPLATE/report-ahoy.md +++ b/.github/ISSUE_TEMPLATE/report-ahoy.md @@ -33,18 +33,29 @@ connected between +3.3V and GND (Pin 1 & 2) of the NRF Module * [ ] Image of the your wiring attached ### Connection diagram I used: -| nRF24L01+ Pin | ESP8266/32 GPIO | +| nRF24L01+ Pin | ESP8266 GPIO | +| ------------- | -------------- | +| Pin 1 GND [*] | GND | +| Pin 2 +3.3V | +3.3V | +| Pin 3 CE | GPIO2 CE D4 | +| Pin 4 CSN | GPIO15 CS D8 | +| Pin 5 SCK | GPIO14 SCLK D5 | +| Pin 6 MOSI | GPIO13 MOSI D7 | +| Pin 7 MISO | GPIO12 MISO D6 | +| Pin 8 IRQ | GPIO0 IRQ D3 | + +| nRF24L01+ Pin | ESP32 GPIO | | ------------- | --------------- | -| Pin 1 GND [] | GND | -| Pin 2 +3.3V | +3.3V | -| Pin 3 CE | GPIO_2/_4 CE | -| Pin 4 CSN | GPIO15/_5 CS | -| Pin 5 SCK | GPIO14/18 SCLK | -| Pin 6 MOSI | GPIO13/23 MOSI | -| Pin 7 MISO | GPIO12/19 MISO | -| Pin 8 IRQ | GPIO_0/0 IRQ | +| Pin 1 GND [*] | GND | +| Pin 2 +3.3V | +3.3V | +| Pin 3 CE | GPIO4 CE D4 | +| Pin 4 CSN | GPIO5 CS D5 | +| Pin 5 SCK | GPIO18 SCLK D18 | +| Pin 6 MOSI | GPIO23 MOSI D23 | +| Pin 7 MISO | GPIO19 MISO D19 | +| Pin 8 IRQ | GPIO0 IRQ D0 | -Note: [] GND Pin 1 has a square mark on the nRF24L01+ module +Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module ## Software * [ ] AhoyDTU diff --git a/.github/ISSUE_TEMPLATE/report.yaml b/.github/ISSUE_TEMPLATE/report.yaml index 9f81e554..6c834480 100644 --- a/.github/ISSUE_TEMPLATE/report.yaml +++ b/.github/ISSUE_TEMPLATE/report.yaml @@ -81,18 +81,29 @@ body: description: Tell us which connection diagram you used? value: | ## Connection diagram I used: - | nRF24L01+ Pin | ESP8266/32 GPIO | + | nRF24L01+ Pin | ESP8266 GPIO | + | ------------- | -------------- | + | Pin 1 GND [*] | GND | + | Pin 2 +3.3V | +3.3V | + | Pin 3 CE | GPIO2 CE D4 | + | Pin 4 CSN | GPIO15 CS D8 | + | Pin 5 SCK | GPIO14 SCLK D5 | + | Pin 6 MOSI | GPIO13 MOSI D7 | + | Pin 7 MISO | GPIO12 MISO D6 | + | Pin 8 IRQ | GPIO0 IRQ D3 | + + | nRF24L01+ Pin | ESP32 GPIO | | ------------- | --------------- | - | Pin 1 GND [] | GND | - | Pin 2 +3.3V | +3.3V | - | Pin 3 CE | GPIO_2/_4 CE | - | Pin 4 CSN | GPIO15/_5 CS | - | Pin 5 SCK | GPIO14/18 SCLK | - | Pin 6 MOSI | GPIO13/23 MOSI | - | Pin 7 MISO | GPIO12/19 MISO | - | Pin 8 IRQ | GPIO_0/0 IRQ | - - Note: [] GND Pin 1 has a square mark on the nRF24L01+ module + | Pin 1 GND [*] | GND | + | Pin 2 +3.3V | +3.3V | + | Pin 3 CE | GPIO4 CE D4 | + | Pin 4 CSN | GPIO5 CS D5 | + | Pin 5 SCK | GPIO18 SCLK D18 | + | Pin 6 MOSI | GPIO23 MOSI D23 | + | Pin 7 MISO | GPIO19 MISO D19 | + | Pin 8 IRQ | GPIO0 IRQ D0 | + + Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module validations: required: true - type: checkboxes diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 218996f2..ffde2d21 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 with: ref: development03 - - uses: benjlevesque/short-sha@v2.0 + - uses: benjlevesque/short-sha@v2.1 id: short-sha with: length: 7 @@ -43,16 +43,16 @@ jobs: pip install --upgrade platformio - name: Convert HTML files - working-directory: tools/esp8266/html + working-directory: src/web/html run: python convert.py - name: Run PlatformIO - run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8266-1m-release --environment esp32-wroom32-release + run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release - name: Rename Binary files id: rename-binary-files - working-directory: tools/esp8266/scripts - run: python getVersion.py + working-directory: src + run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT - name: Set Version uses: cschleiden/replace-tokens@v1 @@ -61,9 +61,16 @@ jobs: env: VERSION: ${{ steps.rename-binary-files.outputs.name }} + - name: Create Manifest + working-directory: src + run: python ../scripts/buildManifest.py + - name: Create Artifact - run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip tools/esp8266/.pio/build/out/* tools/esp8266/User_Manual.md - - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3 with: - name: ${{ steps.rename-binary-files.outputs.name }}_dev_build - path: ./${{ steps.rename-binary-files.outputs.name }}.zip + name: ahoydtu_dev + path: | + src/firmware/* + src/User_Manual.md + src/install.html + diff --git a/.github/workflows/compile_esp8266.yml b/.github/workflows/compile_release.yml similarity index 70% rename from .github/workflows/compile_esp8266.yml rename to .github/workflows/compile_release.yml index e989235c..84ad5111 100644 --- a/.github/workflows/compile_esp8266.yml +++ b/.github/workflows/compile_release.yml @@ -4,10 +4,11 @@ on: push: branches: main paths: - - 'tools/esp8266/**' # build only when changes occur here - - '!tools/esp8266/README.md' - - '!tools/esp8266/CHANGES.md' - - '!tools/esp8266/User_Manual.md' + - 'src/**' # build only when changes occur here + - '.github/workflows/compile_release.yml' + - '!README.md' + - '!CHANGES.md' + - '!User_Manual.md' jobs: build: runs-on: ubuntu-latest @@ -16,7 +17,7 @@ jobs: - uses: actions/checkout@v3 with: ref: main - - uses: benjlevesque/short-sha@v2.0 + - uses: benjlevesque/short-sha@v2.1 id: short-sha with: length: 7 @@ -46,16 +47,18 @@ jobs: pip install --upgrade platformio - name: Convert HTML files - working-directory: tools/esp8266/html + working-directory: src/web/html run: python convert.py + - name: Run PlatformIO - run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8266-1m-release --environment esp32-wroom32-release + run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release - name: Rename Binary files id: rename-binary-files - working-directory: tools/esp8266/scripts - run: python getVersion.py - - name: create-release + working-directory: src + run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT + + - name: Create Release id: create-release uses: actions/create-release@v1 with: @@ -63,18 +66,21 @@ jobs: prerelease: false release_name: ${{ steps.rename-binary-files.outputs.name }} tag_name: ${{ steps.rename-binary-files.outputs.name }} - body_path: tools/esp8266/CHANGES.md + body_path: src/CHANGES.md env: GITHUB_TOKEN: ${{ github.token }} - - name: set-version + + - name: Set Version uses: cschleiden/replace-tokens@v1 with: - files: tools/esp8266/User_Manual.md + files: User_Manual.md env: VERSION: ${{ steps.rename-binary-files.outputs.name }} - - name: create-artifact - run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip tools/esp8266/.pio/build/out/* tools/esp8266/User_Manual.md - - name: upload-release + + - name: Create Artifact + run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md + + - name: Upload Release id: upload-release uses: actions/upload-release-asset@v1 env: diff --git a/.gitignore b/.gitignore index 9af41f40..2ee4b679 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,15 @@ -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps -build -tools/esp8266/tmp -tools/esp8266/binaries +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +.vscode/extensions.json +src/config/config_override.h +src/web/html/h/* +src/web/html/tmp/* /**/Debug /**/v16/* *.db *.suo *.ipch -tools/esp8266/.vscode/extensions.json -.DS_Store -.vscode -tools/esp8266/platformio-device-monitor-*.log -tools/esp8266/html/h/* \ No newline at end of file +src/output.map diff --git a/tools/esp8266/README.md b/Getting_Started.md similarity index 71% rename from tools/esp8266/README.md rename to Getting_Started.md index 18f00c17..e73277f0 100644 --- a/tools/esp8266/README.md +++ b/Getting_Started.md @@ -1,3 +1,22 @@ +## Overview + +This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.
+Further information will help you to communicate to the compatible inverters. + +You find the full [User_Manual here](User_Manual.md) + +## Compatiblity + +For now the following Inverters should work out of the box: + +Hoymiles Inverters + +| Status | Serie | Model | comment | +| ----- | ----- | ------ | ------- | +| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet | +| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | | +| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). | + ## Table of Contents - [Table of Contents](#table-of-contents) @@ -6,9 +25,12 @@ - [Things needed](#things-needed) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there) - [Wiring things up](#wiring-things-up) - - [ESP8266 wiring example](#esp8266-wiring-example) + - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example) - [Schematic](#schematic) - [Symbolic view](#symbolic-view) + - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2) + - [Schematic](#schematic-2) + - [Symbolic view](#symbolic-view-2) - [ESP32 wiring example](#esp32-wiring-example) - [Schematic](#schematic-1) - [Symbolic view](#symbolic-view-1) @@ -23,39 +45,13 @@ - [HTTP based Pages](#http-based-pages) - [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface) - [Used Libraries](#used-libraries) -- [Contact](#contact) - [ToDo](#todo) *** -## Overview - -This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.
-Further information will help you to communicate to the compatible inverters. - -You find the full [User_Manual here](User_Manual.md) - -## Compatiblity +Solenso Inverters: -For now the following Inverters should work out of the box: - -Hoymiles Inverters - -- HM300 -- HM350 -- HM400 -- HM600 -- HM700 -- HM800 -- HM1000? -- HM1200 -- HM1500 - -TSun Inverters: - -- TSOL-350 -- TSOL-400 -- others may work as well (need to be verified). +- SOL-H350 ## Things needed @@ -68,8 +64,9 @@ Make sure the NRF24L01+ module has the "+" in its name as we depend on the 250kb | **Parts** | **Price** | | --- | --- | -| D1 ESP8266 Mini WLAN Board Mikrokontroller | 4,40 Euro | +| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro | | NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro | +| 100µF / 10V Capacitor Kondensator | 0,15 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | **Total costs** | **10,34 Euro** | @@ -79,15 +76,28 @@ To also run our sister project OpenDTU and be upwards compatible for the future | --- | --- | | ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro | | NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro | +| 100µF / 10V Capacitor Kondensator | 0,15 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | **Total costs** | **14,89 Euro** | #### There are fake NRF24L01+ Modules out there Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).
-An example can be found in [Issue #230](https://github.com/grindylow/ahoy/issues/230).
+An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).
You are welcome to add more examples of faked chips. We will add that information here.
+Some users reported better connection or longer range through more walls when using the +"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector, +which has an eye-catching HF cover. But beware: It comes without the antenna! + +In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW". +Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN". +It is not always the bigger the better... + +Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator. +The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously. +If you operate additional interfaces like a display, the reserve is again reduced. + ## Wiring things up The NRF24L01+ radio module is connected to the standard SPI pins: @@ -106,17 +116,29 @@ Additional, there are 3 pins, which can be set individual: *These pins can be changed from the /setup URL.* -#### ESP8266 wiring example +#### ESP8266 wiring example on WEMOS D1 This is an example wiring using a Wemos D1 mini.
##### Schematic -![Schematic](../../doc/AhoyWemos_Schaltplan.jpg) +![Schematic](doc/AhoyWemos_Schaltplan.jpg) ##### Symbolic view -![Symbolic](../../doc/AhoyWemos_Steckplatine.jpg) +![Symbolic](doc/AhoyWemos_Steckplatine.jpg) + +#### ESP8266 wiring example on 30pin Lolin NodeMCU v3 + +This is an example wiring using a NodeMCU V3.
+ +##### Schematic + +![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg) + +##### Symbolic view + +![Symbolic](doc/ESP8266_nRF24L01+_bb.png) #### ESP32 wiring example @@ -124,11 +146,11 @@ Example wiring for a 38pin ESP32 module ##### Schematic -![Schematic](../../doc/Wiring_ESP32_Schematic.png) +![Schematic](doc/Wiring_ESP32_Schematic.png) ##### Symbolic view -![Symbolic](../../doc/Wiring_ESP32_Symbol.png) +![Symbolic](doc/Wiring_ESP32_Symbol.png) ##### ESP32 GPIO settings @@ -140,12 +162,27 @@ CE D2 (GPIO4) IRQ D0 (GPIO16 - no IRQ!) ``` +ATTENTION: From development version 108 onwards, also MISO, MOSI and SCLK +are configurable. Their defaults are correct for 'standard' ESP32 boards +and non-settable for ESP8266 (as this chip cannot move them elsewhere). +If you have an existing install though, you might see '0' in the web GUI. + +Set MISO=19, MOSI=23, SCLK=18 in GUI and save for existing installs, this is the old +correct default for most ESP32 boards, for ESP82xx, a simple settings save should suffice. +Reboot afterwards. + + ## Flash the Firmware on your Ahoy DTU Hardware Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board. You can either build your own using your own configuration or use one of our pre-compiled generic builds. -#### Compiling your own Version +### Flash from your browser (easy) + +The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required. +[Here you go](https://ahoydtu.de/web_install/) + +### Compiling your own Version This information suits you if you want to configure and build your own firmware. @@ -204,7 +241,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.

If connected to your local Network, you just have to find out the used IP Address or try the default name [http://ahoy-dtu/](http://ahoy-dtu/). In most cases your Router will give you a hint.
- If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.1.1/](http://192.168.1.1/).
+ If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.4.1/](http://192.168.4.1/).
Just open the IP-Address in your browser.

The webinterface has the following abilities: @@ -216,25 +253,26 @@ When everything is wired up and the firmware is flashed, it is time to connect t ##### HTTP based Pages - To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.1.1/setup](http://192.168.1.1/setup) ).
- -| page | use | output | -| ---- | ------ | ------ | -| /uptime | displays the uptime of your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | -| /reboot | reboots the Ahoy DTU | | -| /erase | erases the EEPROM | | -| /factory | resets to the factory defaults configured in config.h | | -| /setup | opens the setup page | | -| /save | | | -| /cmdstat | show stat from the home page | | -| /visualization | displays the information from your converter | | -| /livedata | displays the live data | | -| /json | gets live-data in JSON format | json output from the livedata | -| /api | | | + To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.4.1/setup](http://192.168.4.1/setup) ).
+ +| page | use | output | default availability | +| ---- | ------ | ------ | ------ | +| /uptime | displays the uptime uf your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | yes | +| /reboot | reboots the Ahoy DTU | | yes | +| /erase | erases the EEPROM | | yes | +| /factory | resets to the factory defaults configured in config.h | | yes | +| /setup | opens the setup page | | yes | +| /save | | | yes | +| /cmdstat | show stat from the home page | | yes | +| /visualization | displays the information from your converter | | yes | +| /livedata | displays the live data | | yes | +| /json | gets live-data in JSON format | json output from the livedata | no - enable via config_override.h | +| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h | +| /api | | | yes | ## MQTT command to set the DTU without webinterface -[Read here](tools/esp8266/User_Manual.md) +[Read here](User_Manual.md) ## Used Libraries @@ -251,12 +289,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t | `RF24` | 1.4.5 | GPL-2.0 | | `PubSubClient` | 2.8 | MIT | | `ArduinoJson` | 6.19.4 | MIT | - -## Contact - -We run a Discord Server that can be used to get in touch with the Developers and Users. - - +| `ESP Async WebServer` | 4.3.0 | ? | ## ToDo diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cbe5ad16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,437 @@ +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 45066d88..78094c78 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,45 @@ -![actions/workflows/compile_esp8266.yml](../../actions/workflows/compile_esp8266.yml/badge.svg) ![actions/workflows/compile_development.yml](../../actions/workflows/compile_development.yml/badge.svg) +[![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] [![Ahoy Dev Build][dev-action-badge]][dev-action-link] + +This work is licensed under a +[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa]. + +[![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa] + +[cc-by-nc-sa]: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.de +[cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png +[cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg + +[dev-action-badge]: https://github.com/lumapu/ahoy/actions/workflows/compile_development.yml/badge.svg +[dev-action-link]: https://github.com/lumapu/ahoy/actions/workflows/compile_development.yml + # 🖐 Ahoy! ![Logo](https://github.com/grindylow/ahoy/blob/main/doc/logo1_small.png?raw=true) **Communicate with Hoymiles inverters via radio**. Get actual values like power, current, daily energy and set parameters like the power limit via web interface or MQTT. In this repository you will find different approaches means Hardware / Software to realize the described functionalities. -List of approaches +Table of approaches: + +| Board | MI | HM | HMS/HMT | comment | HowTo start | +| ------ | -- | -- | ------- | ------- | ---------- | +| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | coming soon✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | +| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | +| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | +| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | -- [ESP8266/ESP32, C++](tools/esp8266/) 👈 the most effort is spent here -- [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) -- [Raspberry Pi, Python](tools/rpi/) -- [Others, C/C++](tools/nano/NRF24_SendRcv/) +## Getting Started +[Guide how to start with a ESP module](Getting_Started.md) -## Quick Start with ESP8266 -- [Go here ✨](tools/esp8266/README.md#things-needed) +[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install) +## Our Website +[https://ahoydtu.de](https://ahoydtu.de) ## Success Stories - [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl) ## Support, Feedback, Information and Discussion -- [Discord Server (~ 300 Users)](https://discord.gg/WzhxEY62mB) +- [Discord Server (~ 3.800 Users)](https://discord.gg/WzhxEY62mB) - [The root of development](https://www.mikrocontroller.net/topic/525778) ### Development @@ -32,4 +51,6 @@ Please try to describe your issues as precise as possible and think about if thi ### Related Projects - [OpenDTU](https://github.com/tbnobody/OpenDTU) -- [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) + <- Our sister project ✨ for Hoymiles HM-300, HM-600, HM-1200 (for ESP32 only!) +- [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) + <- Go here ✨ for Hoymiles MI-300, MI-600, MI-1200 Software (single inverter only) diff --git a/tools/esp8266/User_Manual.md b/User_Manual.md similarity index 55% rename from tools/esp8266/User_Manual.md rename to User_Manual.md index 1c882f48..242a2809 100644 --- a/tools/esp8266/User_Manual.md +++ b/User_Manual.md @@ -1,7 +1,7 @@ -# User Manual Ahoy DTU (on ESP8266) +# User Manual AhoyDTU (on ESP8266) Version #{VERSION}# ## Introduction -See the repository [README.md](README.md) +See the repository [README.md](Getting_Started.md) ## Setup Assuming you have a running ahoy-dtu and you can access the setup page. @@ -9,9 +9,51 @@ In the initial case or after click "erase settings" the fields for the inverter Set at least the serial number and a name for each inverter, check "reboot after save" and click the "Save" button. -## MQTT Output -The ahoy dtu will publish on the following topics -`//ch0/#` +## MQTT Publish + +The AhoyDTU will publish on the following topics + +### `/#` + +| Topic | Example Value | Remarks | Retained | +|---|---|---|---| +| `comm_start` | 1672123767 | inverter communication start, based on sunrise, UTC timestamp | true | +| `comm_stop` | 1672155709 | inverter communication stop, based on sunset, UTC timestamp | true | +| `device` | AHOY-DTU | configured device name | true | +| `dis_night_comm` | true | setting if night communication is disabled | true | +| `free_heap` | 17784 | free heap of ESP in bytes | false | +| `mqtt` | connected | shows MQTT status | true | +| `status` | 1 | see table below | true | +| `sunrise` | 1672124667 | sunrise, UTC timestamp | true | +| `sunset` | 1672154809 | sunset, UTC timestamp | true | +| `uptime` | 73630 | uptime in seconds | false | +| `version` | 0.5.61 | current installed verison of AhoyDTU | true | +| `wifi_rssi` | -75 | WiFi signal strength | false | +| `ip_addr` | 192.168.178.25 | WiFi Station IP Address | true | + +| status code | Remarks | +|---|---| +| 0 | offline | +| 1 | partial | +| 2 | online | + + +### `//#` + +| Topic | Example Value | Remarks | Retained | +|---|---|---|---| +| `available` | 2 | see table below | true | +| `last_success` | 1672155690 | UTC Timestamp | true | +| `ack_pwr_limit` | true | fast information if inverter has accepted power limit | false | + +| status code | Remarks | +|---|---| +| 0 | not available and not producing | +| 1 | available but not producing | +| 2 | available and producing | + + +### `//ch0/#` | Topic | Example Value | Remarks | |---|---|---| @@ -34,7 +76,7 @@ The ahoy dtu will publish on the following topics |PowerLimit | 80.000|actual set point for power limit control AC active power in percent| |LastAlarmCode | 1.000| Last Alarm Code eg. "inverter start"| -`//ch/#` +### `//ch/#` `` is in the range 1 to 4 depending on the inverter type @@ -47,10 +89,8 @@ The ahoy dtu will publish on the following topics |YieldTotal | 110.819 | Energy converted to AC since reset Watt hours per module/channel (measured on DC) | |Irradiation |5.65 | ratio DC Power over set maximum power per module/channel in percent | -## Active Power Limit via Setup Page -If you leave the field "Active Power Limit" empty during the setup and reboot the ahoy-dtu will set a value of 65535 in the setup. -That is the value you have to fill in case you want to operate the inverter without a active power limit. -If the value is 65535 or -1 after another reboot the value will be set automatically to "100" and in the drop-down menu "relative in percent persistent" will be set. Of course you can do this also by your self. +## Active Power Limit via Serial / Control Page +URL: `/serial` You can change the setting in the following manner. Decide if you want to set @@ -68,160 +108,160 @@ after a power cycle of the inverter (P_DC=0 and P_AC=0 for at least 10 seconds) The user has to ensure correct settings. Remember that for the inverters of 3rd generation the relative active power limit is in the range of 2% up to 100%. Also an absolute active power limit below approx. 30 Watt seems to be not meanful because of the control capabilities and reactive power load. -## Active Power Limit via MQTT -The ahoy-dtu subscribes on the topic `/devcontrol/#` if the mqtt broker is set-up correctly. The default topic is `inverter/devcontrol/#`. +## Control via MQTT + +### Generic Information -To set the active power limit (controled value is the AC Power of the inverter) you have four options. (Only single phase inverters are actually in focus). +The AhoyDTU subscribes on following topics: -| topic | payload | active power limit in | Condition | -| --------------------------------------------------------------- | ----------- | -------------------------------------------- | -------------- | -| /devcontrol//11 OR /devcontrol//11/0 | [0..65535] | Watt | not persistent | -| /devcontrol//11/256 | [0..65535] | Watt | persistent | -| /devcontrol//11/1 | [2..100] | % | not persistent | -| /devcontrol//11/257 | [2..100] | % | persistent | +- `/ctrl/limit/` +- `/ctrl/restart/` +- `/setup/set_time` + +👆 `` can be set on setup page, default is `inverter`. 👆 `` is the number of the specific inverter in the setup page. -* First inverter --> `` = 0 -* Second inverter --> `` = 1 -* ... - -### Developer Information MQTT Interface -`/devcontrol///` - -The implementation allows to set any of the available `` Commands: -```C -typedef enum { - TurnOn = 0, // 0x00 - TurnOff = 1, // 0x01 - Restart = 2, // 0x02 - Lock = 3, // 0x03 - Unlock = 4, // 0x04 - ActivePowerContr = 11, // 0x0b - ReactivePowerContr = 12, // 0x0c - PFSet = 13, // 0x0d - CleanState_LockAndAlarm = 20, // 0x14 - SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files - Init = 0xff -} DevControlCmdType; + +### Inverter restart +```mqtt +/ctrl/restart/ ``` -The MQTT payload will be set on first to bytes and ``, which is taken from the topic path will be set on the second two bytes if the corresponding DevControlCmdType supports 4 byte data. -See here the actual implementation to set the send buffer bytes. -```C -void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { - sendCmdPacket(invId, TX_REQ_DEVCONTROL, ALL_FRAMES, false); - int cnt = 0; - // cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor - mTxBuf[10] = cmd; - mTxBuf[10 + (++cnt)] = 0x00; - if (cmd >= ActivePowerContr && cmd <= PFSet){ - mTxBuf[10 + (++cnt)] = ((data[0] * 10) >> 8) & 0xff; // power limit || high byte from MQTT payload - mTxBuf[10 + (++cnt)] = ((data[0] * 10) ) & 0xff; // power limit || low byte from MQTT payload - mTxBuf[10 + (++cnt)] = ((data[1] ) >> 8) & 0xff; // high byte from MQTT topic value - mTxBuf[10 + (++cnt)] = ((data[1] ) ) & 0xff; // low byte from MQTT topic value - } - // crc control data - uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt+1); - mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff; - mTxBuf[10 + (++cnt)] = (crc ) & 0xff; - // crc over all - cnt +=1; - mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt); - - sendPacket(invId, mTxBuf, 10 + (++cnt), true); -} +Example: +```mqtt +inverter/ctrl/restart/0 +``` + +### Power Limit relative (non persistent) [%] + +```mqtt +/ctrl/limit/ ``` +with a payload `[2 .. 100]` -So as example sending any payload on `inverter/devcontrol/0/1` will switch off the inverter. +**NOTE: optional a `%` can be sent as last character** + +Example: +```mqtt +inverter/ctrl/limit/0 70 +``` + +### Power Limit absolute (non persistent) [Watts] +```mqtt +/ctrl/limit/ +``` +with a payload `[0 .. 65535]` + +**NOTE: the unit `W` is necessary to determine an absolute limit** + +Example: +```mqtt +inverter/ctrl/limit/0 600W +``` + +### Power Limit persistent +This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter. + +## Control via REST API + +### Generic Information + +The rest API works with *JSON* POST requests. All the following instructions must be sent to the `/api` endpoint of the AhoyDTU. + +👆 `` is the number of the specific inverter in the setup page. + +### Inverter Power (On / Off) -## Active Power Limit via REST API -It is also implemented to set the power limit via REST API call. Therefore send a POST request to the endpoint /api. -The response will always be a json with {success:true} -The payload shall be a json formated string in the following manner ```json { - "inverter":, - "tx_request": , - "cmd": , - "payload": , - "payload2": + "id": , + "cmd": "power", + "val": } ``` -With the following value ranges +The `` should be set to `1` = `ON` and `0` = `OFF` -| Value | range | note | -| --------------------------- | ----------- | ------------------------------- | -| | 81 or 21 | integer uint8, (0x15 or 0x51) | -| | [0...255] | integer uint8, subcmds eg. 0x0b | -| | [0...65535] | uint16 | -| | [0...3] | integer uint8 | +### Inverter restart -Example to set the active power limit non persistent to 10% ```json { - "inverter":0, - "tx_request": 81, - "cmd": 11, - "payload": 10, - "payload2": 1 + "id": , + "cmd": "restart" } ``` -Example to set the active power limit persistent to 600Watt + + +### Power Limit relative persistent [%] + ```json { - "inverter":0, - "tx_request": 81, - "cmd": 11, - "payload": 600, - "payload2": 256 + "id": , + "cmd": "limit_persistent_relative", + "val": } ``` +The `VALUE` represents a percent number in a range of `[2 .. 100]` + + +### Power Limit absolute persistent [Watts] -### Developer Information REST API -In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this: ```json { - "inverter":0, - "tx_request": 21, - "cmd": 17, - "payload": 5, - "payload2": 0 + "id": , + "cmd": "limit_persistent_absolute", + "val": } ``` +The `VALUE` represents watts in a range of `[0 .. 65535]` -## Zero Export Control -* You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet) -* You can check the inverter set point for the power limit control on the topic `//ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall) -* You can monitor the actual AC power by subscribing to the topic `//ch0/P_AC` 👆 This value is ALWAYS in Watt -## Issues and Debuging for active power limit settings +### Power Limit relative non persistent [%] -Turn on the serial debugging in the setup. Try to have find out if the behavior is deterministic. That means can you reproduce the behavior. Be patient and wait on inverter reactions at least some minutes and beware that the DC-Power is sufficient. +```json +{ + "id": , + "cmd": "limit_nonpersistent_relative", + "val": +} +``` +The `VALUE` represents a percent number in a range of `[2 .. 100]` -In case of issues please report: -1. Version of firmware -2. The output of the serial debug esp. the TX messages starting with "0x51" and the RX messages starting with "0xD1" or "0xF1" -3. Which case you have tried: Setup-Page, MQTT, REST API and at what was shown on the "Visualization Page" at the Location "Limit" -4. The setting means payload, relative, absolute, persistent, not persistent (see tables above) +### Power Limit absolute non persistent [Watts] + +```json +{ + "id": , + "cmd": "limit_nonpersistent_absolute", + "val": +} +``` +The `VALUE` represents watts in a range of `[0 .. 65535]` -**Developer Information General for Active Power Limit** -⚡The following was verified by field tests and feedback from users -Internally this values will be set for the second two bytes for MainCmd: 0x51 SubCmd: 0x0b --> DevControl set ActivePowerLimit -```C -typedef enum { - AbsolutNonPersistent = 0x0000, // 0 - RelativNonPersistent = 0x0001, // 1 - AbsolutPersistent = 0x0100, // 256 - RelativPersistent = 0x0101 // 257 -} PowerLimitControlType; +### Developer Information REST API (obsolete) +In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this: +```json +{ + "inverter":0, + "tx_request": 21, + "cmd": 17, + "payload": 5, + "payload2": 0 +} ``` +## Zero Export Control (needs rework) +* You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet) +* You can check the inverter set point for the power limit control on the topic `//ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall) +* You can monitor the actual AC power by subscribing to the topic `//ch0/P_AC` 👆 This value is ALWAYS in Watt + + ## Firmware Version collection Gather user inverter information here to understand what differs between some inverters. +To get the information open the URL `/api/record/info` on your AhoyDTU. The information will only be present once the AhoyDTU was able to communicate with an inverter. | Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | | | | ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- | @@ -241,7 +281,12 @@ Gather user inverter information here to understand what differs between some in | chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | | | chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | | | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | -| | | | | | | | | | +| B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | +| tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | | +| rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | | +| dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | | | | | | | | | | | | ## Developer Information about Command Queue @@ -276,3 +321,11 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. + +## Additional Notes +### MI Inverters +- AhoyDTU supports MI type inverters as well, since dev. version 0.5.70. +- MI inverters are known to be delivered with two different generations of firmwares: inverters with serial numbers 10x2 already use the 3rd generation protocol and behave just like the newer HM models, *the follwoing remarks do not apply to these*. +- Older MI inverters (#sn 10x1) use a different rf protocol and thus do not deliver exactly the same data. E.g. the AC power value will therefore be calculated by AhoyDTU itself, while other values might not be available at all. +- Single and dual channel 2nd gen. devices seem not to accept power limiting commands at all, the lower limit for 4-channel MI is 10% (instead of 2% for newer models) +- 4-channel MI type inverters might work, but code still is untested. diff --git a/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png new file mode 100644 index 00000000..2b37a3b0 Binary files /dev/null and b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png differ diff --git a/doc/ESP8266_nRF24L01+_Schaltplan.jpg b/doc/ESP8266_nRF24L01+_Schaltplan.jpg new file mode 100644 index 00000000..749e3fa3 Binary files /dev/null and b/doc/ESP8266_nRF24L01+_Schaltplan.jpg differ diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md new file mode 100644 index 00000000..8fb9e002 --- /dev/null +++ b/doc/prometheus_ep_description.md @@ -0,0 +1,51 @@ +# Prometheus Endpoint +Metrics available for AhoyDTU device, inverters and channels. + +Prometheus metrics provided at `/metrics`. + +## Labels +| Label name | Description | +|:-------------|:--------------------------------------| +| version | current installed version of AhoyDTU | +| image | currently not used | +| devicename | Device name from setup | +| name | Inverter name from setup | +| serial | Serial number of inverter | +| inverter | Inverter name from setup | +| channel | Channel name from setup | + +## Exported Metrics +| Metric name | Type | Description | Labels | +|----------------------------------------|---------|--------------------------------------------------------|--------------| +| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | +| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | +| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | +| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | +| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | +| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | +| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | +| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | +| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | +| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | +| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter | +| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter | +| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter | +| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | +| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index of inverter | inverter | +| `ahoy_solar_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter | +| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter | +| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter | +| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | inverter | +| `ahoy_solar_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter | +| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel | +| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel | +| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel | +| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | +| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | +| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | +| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | + diff --git a/tools/esp8266/scripts/auto_firmware_version.py b/scripts/auto_firmware_version.py similarity index 88% rename from tools/esp8266/scripts/auto_firmware_version.py rename to scripts/auto_firmware_version.py index a9c74371..c4ab270d 100644 --- a/tools/esp8266/scripts/auto_firmware_version.py +++ b/scripts/auto_firmware_version.py @@ -17,7 +17,7 @@ from dulwich import porcelain def get_firmware_specifier_build_flag(): try: - build_version = porcelain.describe('../../') # refers to the repository root dir + build_version = porcelain.describe('../') # refers to the repository root dir except: build_version = "g0000000" diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py new file mode 100644 index 00000000..db29b352 --- /dev/null +++ b/scripts/buildManifest.py @@ -0,0 +1,55 @@ +import os +from datetime import date +import json + +def readVersion(path, infile): + f = open(path + infile, "r") + lines = f.readlines() + f.close() + + today = date.today() + search = ["_MAJOR", "_MINOR", "_PATCH"] + version = today.strftime("%y%m%d") + "_ahoy_" + versionnumber = ""# "ahoy_v" + for line in lines: + if(line.find("VERSION_") != -1): + for s in search: + p = line.find(s) + if(p != -1): + version += line[p+13:].rstrip() + "." + versionnumber += line[p+13:].rstrip() + "." + + return [versionnumber[:-1], version[:-1]] + +def buildManifest(path, infile, outfile): + version = readVersion(path, infile) + sha = os.getenv("SHA",default="sha") + data = {} + data["name"] = "AhoyDTU - Development" + data["version"] = version[0] + data["new_install_prompt_erase"] = 1 + data["builds"] = [] + + esp32 = {} + esp32["chipFamily"] = "ESP32" + esp32["parts"] = [] + esp32["parts"].append({"path": "bootloader.bin", "offset": 4096}) + esp32["parts"].append({"path": "partitions.bin", "offset": 32768}) + esp32["parts"].append({"path": "ota.bin", "offset": 57344}) + esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) + data["builds"].append(esp32) + + esp8266 = {} + esp8266["chipFamily"] = "ESP8266" + esp8266["parts"] = [] + esp8266["parts"].append({"path": version[1] + "_" + sha + "_esp8266.bin", "offset": 0}) + data["builds"].append(esp8266) + + jsonString = json.dumps(data, indent=2) + + fp = open(path + "firmware/" + outfile, "w") + fp.write(jsonString) + fp.close() + + +buildManifest("", "defines.h", "manifest.json") diff --git a/scripts/getVersion.py b/scripts/getVersion.py new file mode 100644 index 00000000..0ebe1ec2 --- /dev/null +++ b/scripts/getVersion.py @@ -0,0 +1,97 @@ +import os +import shutil +import gzip +from datetime import date + +def genOtaBin(path): + arr = [] + arr.append(1) + arr.append(0) + arr.append(0) + arr.append(0) + for x in range(24): + arr.append(255) + arr.append(154) + arr.append(152) + arr.append(67) + arr.append(71) + for x in range(4064): + arr.append(255) + arr.append(0) + arr.append(0) + arr.append(0) + arr.append(0) + for x in range(4092): + arr.append(255) + with open(path + "ota.bin", "wb") as f: + f.write(bytearray(arr)) + +# write gzip firmware file +def gzip_bin(bin_file, gzip_file): + with open(bin_file,"rb") as fp: + with gzip.open(gzip_file, "wb", compresslevel = 9) as f: + shutil.copyfileobj(fp, f) + +def readVersion(path, infile): + f = open(path + infile, "r") + lines = f.readlines() + f.close() + + today = date.today() + search = ["_MAJOR", "_MINOR", "_PATCH"] + version = today.strftime("%y%m%d") + "_ahoy_" + versionnumber = "ahoy_v" + for line in lines: + if(line.find("VERSION_") != -1): + for s in search: + p = line.find(s) + if(p != -1): + version += line[p+13:].rstrip() + "." + versionnumber += line[p+13:].rstrip() + "." + + os.mkdir(path + "firmware/") + sha = os.getenv("SHA",default="sha") + + versionout = version[:-1] + "_" + sha + "_esp8266.bin" + src = path + ".pio/build/esp8266-release/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" + src = path + ".pio/build/esp8266-release-prometheus/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp8285.bin" + src = path + ".pio/build/esp8285-release/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + gzip_bin(dst, dst + ".gz") + + versionout = version[:-1] + "_" + sha + "_esp32.bin" + src = path + ".pio/build/esp32-wroom32-release/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" + src = path + ".pio/build/esp32-wroom32-release-prometheus/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32s3.bin" + src = path + ".pio/build/opendtufusionv1-release/firmware.bin" + dst = path + "firmware/" + versionout + os.rename(src, dst) + + # other ESP32 bin files + src = path + ".pio/build/esp32-wroom32-release/" + dst = path + "firmware/" + os.rename(src + "bootloader.bin", dst + "bootloader.bin") + os.rename(src + "partitions.bin", dst + "partitions.bin") + genOtaBin(path + "firmware/") + os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html") + + print("name=" + versionnumber[:-1] ) + + +readVersion("", "defines.h") diff --git a/scripts/gh-action-dev-build-flash.html b/scripts/gh-action-dev-build-flash.html new file mode 100644 index 00000000..ea6146db --- /dev/null +++ b/scripts/gh-action-dev-build-flash.html @@ -0,0 +1,93 @@ + + + + + + + + + + Flash | AhoyDTU + +
+ +
+

Development Build (ESP8266 / ESP32)

+

+ +

+

+ Hierzu die Ahoy-Hardware per USB Kabel an den PC stecken und evtl. warten, bis die Treiber installiert sind. Anschließend auf den ensprechenden connect Button klicken. +

+ + + + + + + + + + + + +

Release Build

+

+ Die Release Builds werden auf ahoyDtu.de veröffentlicht. +

+
+ +
+

Vorbereitungen Google Chrome

+

+ Bekommt man nach der Auswahl des COM-Ports einen Fehler Failed to download manifest muss man Chrome mit einem Parameter starten: +

+

+

+
+ Windows +
+
+ start chrome --allow-file-access-from-files +
+
+
+
+ Linux +
+
+ google-chrome --allow-file-access-from-files +
+
+
+
+ OS X +
+
+ open -a 'Google Chrome' --args -allow-file-access-from-files +
+
+

+ +
+
+ + + + + diff --git a/tools/esp8266/.gitignore b/src/.gitignore similarity index 83% rename from tools/esp8266/.gitignore rename to src/.gitignore index 3e881135..89cc49cb 100644 --- a/tools/esp8266/.gitignore +++ b/src/.gitignore @@ -3,4 +3,3 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch -config_override.h diff --git a/src/.vscode/extensions.json b/src/.vscode/extensions.json new file mode 100644 index 00000000..080e70d0 --- /dev/null +++ b/src/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 00000000..58a2c3c7 --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,86 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // identify that settings is loaded + "workbench.colorCustomizations": { + "editorLineNumber.foreground": "#00ff00" + }, + "editor.wordWrap": "off", + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "diffEditor.ignoreTrimWhitespace": true, + "files.autoSave": "afterDelay", + "editor.tabSize": 4, + "editor.insertSpaces": true, + // `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. + // Set to false to keep the values you've explicitly set, above. + "editor.detectIndentation": false, + // https://clang.llvm.org/docs/ClangFormatStyleOptions.html + "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}", + "files.associations": { + "typeinfo": "cpp", + "string": "cpp", + "istream": "cpp", + "ostream": "cpp", + "array": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "bit": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "set": "cpp", + "iostream": "cpp", + "mutex": "cpp", + "ranges": "cpp", + "stop_token": "cpp", + "thread": "cpp" + }, + "cmake.configureOnOpen": false, + "editor.formatOnSave": false, +} \ No newline at end of file diff --git a/src/CHANGES.md b/src/CHANGES.md new file mode 100644 index 00000000..e0591a00 --- /dev/null +++ b/src/CHANGES.md @@ -0,0 +1,33 @@ +Changelog v0.6.0 + +## General +* improved night time calculation time to 1 minute after last communication pause #515 +* refactored code for better readability +* improved Hoymiles communication (retransmits, immediate power limit transmission, timing at all) +* renamed firmware binaries +* add login / logout to menu +* add display support for `SH1106`, `SSD1306`, `Nokia` and `ePaper 1.54"` (ESP32 only) +* add yield total correction - move your yield to a new inverter or correct an already used inverter +* added import / export feature +* added `Prometheus` endpoints +* improved wifi connection and stability (connect to strongest AP) +* addded Hoymiles alarm IDs to log +* improved `System` information page (eg. radio statitistics) +* improved UI (responsive design, (optional) dark mode) +* improved system stability (reduced `heap-fragmentation`, don't break settings on failure) #644, #645 +* added support for 2nd generation of Hoymiles inverters, MI series +* improved JSON API for more stable WebUI +* added option to disable input display in `/live` (`max-power` has to be set to `0`) +* updated documentation +* improved settings on ESP32 devices while setting SPI pins (for `NRF24` radio) + +## MqTT +* added `comm_disabled` #529 +* added fixed interval option #542, #523 +* improved communication, only required publishes +* improved retained flags +* added `set_power_limit` acknowledge MQTT publish #553 +* added feature to reset values on midnight, communication pause or if the inverters are not available +* partially added Hoymiles alarm ID +* improved autodiscover (added total values on multi-inverter setup) +* improved `clientID` a part of the MAC address is added to have an unique name diff --git a/src/app.cpp b/src/app.cpp new file mode 100644 index 00000000..d3fba12a --- /dev/null +++ b/src/app.cpp @@ -0,0 +1,416 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#include "app.h" +#include +#include "utils/sun.h" + +//----------------------------------------------------------------------------- +app::app() : ah::Scheduler() {} + + +//----------------------------------------------------------------------------- +void app::setup() { + Serial.begin(115200); + while (!Serial) + yield(); + + ah::Scheduler::setup(); + + resetSystem(); + + mSettings.setup(); + mSettings.getPtr(mConfig); + DPRINT(DBG_INFO, F("Settings valid: ")); + if (mSettings.getValid()) + DBGPRINTLN(F("true")); + else + DBGPRINTLN(F("false")); + + mSys.enableDebug(); + mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); + +#if defined(AP_ONLY) + mInnerLoopCb = std::bind(&app::loopStandard, this); + #else + mInnerLoopCb = std::bind(&app::loopWifi, this); + #endif + + mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); + #if !defined(AP_ONLY) + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + #endif + + mSys.addInverters(&mConfig->inst); + + mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mPayload.enableSerialDebug(mConfig->serial.debug); + mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + + mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mMiPayload.enableSerialDebug(mConfig->serial.debug); + + // DBGPRINTLN("--- after payload"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); + + if (!mSys.Radio.isChipConnected()) + DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); + + // when WiFi is in client mode, then enable mqtt broker + #if !defined(AP_ONLY) + mMqttEnabled = (mConfig->mqtt.broker[0] > 0); + if (mMqttEnabled) { + mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); + mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); + mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + } + #endif + setupLed(); + + mWeb.setup(this, &mSys, mConfig); + mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); + + mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); + + // Plugins + if (mConfig->plugin.display.type != 0) + mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); + + mPubSerial.setup(mConfig, &mSys, &mTimestamp); + + regularTickers(); + + + // DBGPRINTLN("--- end setup"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); +} + +//----------------------------------------------------------------------------- +void app::loop(void) { + mInnerLoopCb(); +} + +//----------------------------------------------------------------------------- +void app::loopStandard(void) { + ah::Scheduler::loop(); + + if (mSys.Radio.loop()) { + while (!mSys.Radio.mBufCtrl.empty()) { + packet_t *p = &mSys.Radio.mBufCtrl.front(); + + if (mConfig->serial.debug) { + DPRINT(DBG_INFO, F("RX ")); + DBGPRINT(String(p->len)); + DBGPRINT(F("B Ch")); + DBGPRINT(String(p->ch)); + DBGPRINT(F(" | ")); + mSys.Radio.dumpBuf(p->packet, p->len); + } + mStat.frmCnt++; + + Inverter<> *iv = mSys.findInverter(&p->packet[1]); + if (NULL != iv) { + if (IV_HM == iv->ivGen) + mPayload.add(iv, p); + else + mMiPayload.add(iv, p); + } + mSys.Radio.mBufCtrl.pop(); + yield(); + } + mPayload.process(true); + mMiPayload.process(true); + } + mPayload.loop(); + mMiPayload.loop(); + + if (mMqttEnabled) + mMqtt.loop(); +} + +//----------------------------------------------------------------------------- +void app::loopWifi(void) { + ah::Scheduler::loop(); + yield(); +} + +//----------------------------------------------------------------------------- +void app::onWifi(bool gotIp) { + DPRINTLN(DBG_DEBUG, F("onWifi")); + ah::Scheduler::resetTicker(); + regularTickers(); // reinstall regular tickers + if (gotIp) { + mInnerLoopCb = std::bind(&app::loopStandard, this); + every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); + mMqttReconnect = true; + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); + if (WIFI_AP == WiFi.getMode()) { + mMqttEnabled = false; + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + } + } else { + mInnerLoopCb = std::bind(&app::loopWifi, this); + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + } +} + +//----------------------------------------------------------------------------- +void app::regularTickers(void) { + DPRINTLN(DBG_DEBUG, F("regularTickers")); + everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); + // Plugins + if (mConfig->plugin.display.type != 0) + everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); + every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); +} + +//----------------------------------------------------------------------------- +void app::tickNtpUpdate(void) { + uint32_t nxtTrig = 5; // default: check again in 5 sec + bool isOK = mWifi.getNtpTime(); + if (isOK || mTimestamp != 0) { + if (mMqttReconnect && mMqttEnabled) { + mMqtt.tickerSecond(); + everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); + everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + } + + // only install schedulers once even if NTP wasn't successful in first loop + if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed + if (mConfig->inst.rstValsNotAvail) + everyMin(std::bind(&app::tickMinute, this), "tMin"); + if (mConfig->inst.rstYieldMidNight) { + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); + } + } + + nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min + + if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { + mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; + tickCalcSunrise(); + } + + // immediately start communicating + // @TODO: leads to reboot loops? not sure #674 + if (isOK && mSendFirst) { + mSendFirst = false; + once(std::bind(&app::tickSend, this), 2, "senOn"); + } + + mMqttReconnect = false; + } + once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); +} + +//----------------------------------------------------------------------------- +void app::tickCalcSunrise(void) { + if (mSunrise == 0) // on boot/reboot calc sun values for current time + ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + + if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day + ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + + tickIVCommunication(); + + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop + onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); + if (mMqttEnabled) + tickSun(); +} + +//----------------------------------------------------------------------------- +void app::tickIVCommunication(void) { + mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on + if (!mIVCommunicationOn) { // inverter communication only during the day + uint32_t nxtTrig; + if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start + nxtTrig = mSunrise - mConfig->sun.offsetSec; + } else { + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + nxtTrig = 0; + } else { // current time lies within communication start/stop time, set next trigger to communication stop + mIVCommunicationOn = true; + nxtTrig = mSunset + mConfig->sun.offsetSec; + } + } + if (nxtTrig != 0) + onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + } + tickComm(); +} + +//----------------------------------------------------------------------------- +void app::tickSun(void) { + // only used and enabled by MQTT (see setup()) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) + once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry +} + +//----------------------------------------------------------------------------- +void app::tickComm(void) { + if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) + once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); + + if (mMqttEnabled) { + if (!mMqtt.tickerComm(!mIVCommunicationOn)) + once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s + } +} + +//----------------------------------------------------------------------------- +void app::tickZeroValues(void) { + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMinute(void) { + // only triggered if 'reset values on no avail is enabled' + + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMidnight(void) { + // only triggered if 'reset values at midnight is enabled' + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); + + Inverter<> *iv; + // set values to zero, except yield total + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + mPayload.zeroYieldDay(iv); + } + + if (mMqttEnabled) + mMqtt.tickerMidnight(); +} + +//----------------------------------------------------------------------------- +void app::tickSend(void) { + if (!mSys.Radio.isChipConnected()) { + DPRINTLN(DBG_WARN, F("NRF24 not connected!")); + return; + } + if (mIVCommunicationOn) { + if (!mSys.Radio.mBufCtrl.empty()) { + if (mConfig->serial.debug) { + DPRINT(DBG_DEBUG, F("recbuf not empty! #")); + DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); + } + } + + int8_t maxLoop = MAX_NUM_INVERTERS; + Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); + do { + mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; + iv = mSys.getInverterByPos(mSendLastIvId); + } while ((NULL == iv) && ((maxLoop--) > 0)); + + if (NULL != iv) { + if (iv->config->enabled) { + if (iv->ivGen == IV_HM) + mPayload.ivSend(iv); + else + mMiPayload.ivSend(iv); + } + } + } else { + if (mConfig->serial.debug) + DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!")); + } + yield(); + + updateLed(); +} + +//----------------------------------------------------------------------------- +void app::resetSystem(void) { + snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + +#ifdef AP_ONLY + mTimestamp = 1; +#endif + + mSendFirst = true; + + mSunrise = 0; + mSunset = 0; + + mMqttEnabled = false; + + mSendLastIvId = 0; + mShowRebootRequest = false; + mIVCommunicationOn = true; + mSavePending = false; + mSaveReboot = false; + + memset(&mStat, 0, sizeof(statistics_t)); +} + +//----------------------------------------------------------------------------- +void app::mqttSubRxCb(JsonObject obj) { + mApi.ctrlRequest(obj); +} + +//----------------------------------------------------------------------------- +void app::setupLed(void) { + /** LED connection diagram + * \\ + * PIN ---- |<----- 3.3V + * + * */ + if (mConfig->led.led0 != 0xff) { + pinMode(mConfig->led.led0, OUTPUT); + digitalWrite(mConfig->led.led0, HIGH); // LED off + } + if (mConfig->led.led1 != 0xff) { + pinMode(mConfig->led.led1, OUTPUT); + digitalWrite(mConfig->led.led1, HIGH); // LED off + } +} + +//----------------------------------------------------------------------------- +void app::updateLed(void) { + if (mConfig->led.led0 != 0xff) { + Inverter<> *iv = mSys.getInverterByPos(0); + if (NULL != iv) { + if (iv->isProducing(mTimestamp)) + digitalWrite(mConfig->led.led0, LOW); // LED on + else + digitalWrite(mConfig->led.led0, HIGH); // LED off + } + } +} diff --git a/src/app.h b/src/app.h new file mode 100644 index 00000000..cbf7e6a1 --- /dev/null +++ b/src/app.h @@ -0,0 +1,297 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __APP_H__ +#define __APP_H__ + +#include +#include +#include +#include + +#include "appInterface.h" +#include "config/settings.h" +#include "defines.h" +#include "hm/hmPayload.h" +#include "hm/hmSystem.h" +#include "hm/miPayload.h" +#include "publisher/pubMqtt.h" +#include "publisher/pubSerial.h" +#include "utils/crc.h" +#include "utils/dbg.h" +#include "utils/scheduler.h" +#include "web/RestApi.h" +#include "web/web.h" +#include "wifi/ahoywifi.h" + +// convert degrees and radians for sun calculation +#define SIN(x) (sin(radians(x))) +#define COS(x) (cos(radians(x))) +#define ASIN(x) (degrees(asin(x))) +#define ACOS(x) (degrees(acos(x))) + +typedef HmSystem HmSystemType; +typedef HmPayload PayloadType; +typedef MiPayload MiPayloadType; +typedef Web WebType; +typedef RestApi RestApiType; +typedef PubMqtt PubMqttType; +typedef PubSerial PubSerialType; + +// PLUGINS +#include "plugins/Display/Display.h" +typedef Display DisplayType; + +class app : public IApp, public ah::Scheduler { + public: + app(); + ~app() {} + + void setup(void); + void loop(void); + void loopStandard(void); + void loopWifi(void); + void onWifi(bool gotIp); + void regularTickers(void); + + void handleIntr(void) { + mSys.Radio.handleIntr(); + } + + uint32_t getUptime() { + return Scheduler::getUptime(); + } + + uint32_t getTimestamp() { + return Scheduler::getTimestamp(); + } + + bool saveSettings(bool reboot) { + mShowRebootRequest = true; // only message on index, no reboot + mSavePending = true; + mSaveReboot = reboot; + once(std::bind(&app::tickSave, this), 3, "save"); + return true; + } + + bool readSettings(const char *path) { + return mSettings.readSettings(path); + } + + bool eraseSettings(bool eraseWifi = false) { + return mSettings.eraseSettings(eraseWifi); + } + + bool getSavePending() { + return mSavePending; + } + + bool getLastSaveSucceed() { + return mSettings.getLastSaveSucceed(); + } + + statistics_t *getStatistics() { + return &mStat; + } + + void scanAvailNetworks() { + mWifi.scanAvailNetworks(); + } + + void getAvailNetworks(JsonObject obj) { + mWifi.getAvailNetworks(obj); + } + + void setOnUpdate() { + onWifi(false); + } + + void setRebootFlag() { + once(std::bind(&app::tickReboot, this), 3, "rboot"); + } + + const char *getVersion() { + return mVersion; + } + + uint32_t getSunrise() { + return mSunrise; + } + + uint32_t getSunset() { + return mSunset; + } + + bool getSettingsValid() { + return mSettings.getValid(); + } + + bool getRebootRequestState() { + return mShowRebootRequest; + } + + void setMqttDiscoveryFlag() { + once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); + } + + void setMqttPowerLimitAck(Inverter<> *iv) { + mMqtt.setPowerLimitAck(iv); + } + + void ivSendHighPrio(Inverter<> *iv) { + if(mIVCommunicationOn) // only send commands if communcation is enabled + mPayload.ivSendHighPrio(iv); + } + + bool getMqttIsConnected() { + return mMqtt.isConnected(); + } + + uint32_t getMqttTxCnt() { + return mMqtt.getTxCnt(); + } + + uint32_t getMqttRxCnt() { + return mMqtt.getRxCnt(); + } + + bool getProtection() { + return mWeb.getProtection(); + } + + uint8_t getIrqPin(void) { + return mConfig->nrf.pinIrq; + } + + String getTimeStr(uint32_t offset = 0) { + char str[10]; + if(0 == mTimestamp) + sprintf(str, "n/a"); + else + sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset)); + return String(str); + } + + uint32_t getTimezoneOffset() { + return mApi.getTimezoneOffset(); + } + + void getSchedulerInfo(uint8_t *max) { + getStat(max); + } + + void getSchedulerNames(void) { + printSchedulers(); + } + + void setTimestamp(uint32_t newTime) { + DPRINT(DBG_DEBUG, F("setTimestamp: ")); + DBGPRINTLN(String(newTime)); + if(0 == newTime) + mWifi.getNtpTime(); + else + Scheduler::setTimestamp(newTime); + } + + HmSystemType mSys; + + private: + typedef std::function innerLoopCb; + + void resetSystem(void); + + void payloadEventListener(uint8_t cmd) { + #if !defined(AP_ONLY) + if (mMqttEnabled) + mMqtt.payloadEventListener(cmd); + #endif + if(mConfig->plugin.display.type != 0) + mDisplay.payloadEventListener(cmd); + } + + void mqttSubRxCb(JsonObject obj); + + void setupLed(void); + void updateLed(void); + + void tickReboot(void) { + DPRINTLN(DBG_INFO, F("Rebooting...")); + onWifi(false); + ah::Scheduler::resetTicker(); + WiFi.disconnect(); + delay(200); + ESP.restart(); + } + + void tickSave(void) { + if(!mSettings.saveSettings()) + mSaveReboot = false; + mSavePending = false; + + if(mSaveReboot) + setRebootFlag(); + } + + void tickNtpUpdate(void); + void tickCalcSunrise(void); + void tickIVCommunication(void); + void tickSun(void); + void tickComm(void); + void tickSend(void); + void tickMinute(void); + void tickZeroValues(void); + void tickMidnight(void); + /*void tickSerial(void) { + if(Serial.available() == 0) + return; + + uint8_t buf[80]; + uint8_t len = Serial.readBytes(buf, 80); + DPRINTLN(DBG_INFO, "got serial data, len: " + String(len)); + for(uint8_t i = 0; i < len; i++) { + if((0 != i) && (i % 8 == 0)) + DBGPRINTLN(""); + DBGPRINT(String(buf[i], HEX) + " "); + } + DBGPRINTLN(""); + }*/ + + innerLoopCb mInnerLoopCb; + + bool mShowRebootRequest; + bool mIVCommunicationOn; + + ahoywifi mWifi; + WebType mWeb; + RestApiType mApi; + PayloadType mPayload; + MiPayloadType mMiPayload; + PubSerialType mPubSerial; + + char mVersion[12]; + settings mSettings; + settings_t *mConfig; + bool mSavePending; + bool mSaveReboot; + + uint8_t mSendLastIvId; + bool mSendFirst; + + statistics_t mStat; + + // mqtt + PubMqttType mMqtt; + bool mMqttReconnect; + bool mMqttEnabled; + + // sun + int32_t mCalculatedTimezoneOffset; + uint32_t mSunrise, mSunset; + + // plugins + DisplayType mDisplay; +}; + +#endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h new file mode 100644 index 00000000..a79dcdb1 --- /dev/null +++ b/src/appInterface.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __IAPP_H__ +#define __IAPP_H__ + +#include "defines.h" +#include "hm/hmSystem.h" + +// abstract interface to App. Make members of App accessible from child class +// like web or API without forward declaration +class IApp { + public: + virtual ~IApp() {} + virtual bool saveSettings(bool stopFs) = 0; + virtual bool readSettings(const char *path) = 0; + virtual bool eraseSettings(bool eraseWifi) = 0; + virtual bool getSavePending() = 0; + virtual bool getLastSaveSucceed() = 0; + virtual void setOnUpdate() = 0; + virtual void setRebootFlag() = 0; + virtual const char *getVersion() = 0; + virtual statistics_t *getStatistics() = 0; + virtual void scanAvailNetworks() = 0; + virtual void getAvailNetworks(JsonObject obj) = 0; + + virtual uint32_t getUptime() = 0; + virtual uint32_t getTimestamp() = 0; + virtual uint32_t getSunrise() = 0; + virtual uint32_t getSunset() = 0; + virtual void setTimestamp(uint32_t newTime) = 0; + virtual String getTimeStr(uint32_t offset) = 0; + virtual uint32_t getTimezoneOffset() = 0; + virtual void getSchedulerInfo(uint8_t *max) = 0; + virtual void getSchedulerNames() = 0; + + virtual bool getRebootRequestState() = 0; + virtual bool getSettingsValid() = 0; + virtual void setMqttDiscoveryFlag() = 0; + virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; + + virtual void ivSendHighPrio(Inverter<> *iv) = 0; + + virtual bool getMqttIsConnected() = 0; + virtual uint32_t getMqttRxCnt() = 0; + virtual uint32_t getMqttTxCnt() = 0; + + virtual bool getProtection() = 0; +}; + +#endif /*__IAPP_H__*/ diff --git a/tools/esp8266/config.h b/src/config/config.h similarity index 67% rename from tools/esp8266/config.h rename to src/config/config.h index 1a1555ce..ac28c1a2 100644 --- a/tools/esp8266/config.h +++ b/src/config/config.h @@ -25,6 +25,8 @@ // If the next line is uncommented, Ahoy will stay in access point mode all the time //#define AP_ONLY +// timeout for automatic logoff (20 minutes) +#define LOGOUT_TIMEOUT (20 * 60) //------------------------------------- // CONFIGURATION - COMPILE TIME @@ -41,9 +43,27 @@ #define DEF_DEVICE_NAME "AHOY-DTU" // default pinout (GPIO Number) -#define DEF_CS_PIN 15 -#define DEF_CE_PIN 2 -#define DEF_IRQ_PIN 0 +#if defined(ESP32) + // this is the default ESP32 (son-S) pinout on the WROOM modules for VSPI, + // for the ESP32-S3 there is no sane 'default', as it has full flexibility + // to map its two HW SPIs anywhere and PCBs differ materially, + // so it has to be selected in the Web UI + #define DEF_CS_PIN 5 + #define DEF_CE_PIN 4 + #define DEF_IRQ_PIN 16 + #define DEF_MISO_PIN 19 + #define DEF_MOSI_PIN 23 + #define DEF_SCLK_PIN 18 +#else + #define DEF_CS_PIN 15 + #define DEF_CE_PIN 2 + #define DEF_IRQ_PIN 0 + // these are given to relay the correct values via API + // they cannot actually be moved for ESP82xx models + #define DEF_MISO_PIN 12 + #define DEF_MOSI_PIN 13 + #define DEF_SCLK_PIN 14 +#endif // default NRF24 power, possible values (0 - 3) #define DEF_AMPLIFIERPOWER 1 @@ -52,7 +72,7 @@ #define PACKET_BUFFER_SIZE 30 // number of configurable inverters -#define MAX_NUM_INVERTERS 4 +#define MAX_NUM_INVERTERS 10 // default serial interval #define SERIAL_INTERVAL 5 @@ -91,7 +111,7 @@ #define NTP_REFRESH_INTERVAL 12 * 3600 * 1000 // default mqtt interval -#define MQTT_INTERVAL 60 +#define MQTT_INTERVAL 90 // default MQTT broker uri #define DEF_MQTT_BROKER "\0" @@ -108,6 +128,18 @@ // default MQTT topic #define DEF_MQTT_TOPIC "inverter" +// discovery prefix +#define MQTT_DISCOVERY_PREFIX "homeassistant" + +// reconnect delay +#define MQTT_RECONNECT_DELAY 5000 + +// Offset for midnight Ticker +// relative to UTC +// may be negative for later in the next day or positive for earlier in previous day +// may contain variable like mCalculatedTimezoneOffset +// must be in parentheses +#define MIDNIGHTTICKER_OFFSET (-1) #if __has_include("config_override.h") #include "config_override.h" diff --git a/tools/esp8266/config_override_example.h b/src/config/config_override_example.h similarity index 52% rename from tools/esp8266/config_override_example.h rename to src/config/config_override_example.h index 5fa73f3d..e7c06b77 100644 --- a/tools/esp8266/config_override_example.h +++ b/src/config/config_override_example.h @@ -7,6 +7,7 @@ #define __CONFIG_OVERRIDE_H__ // override fallback WiFi info +#define FB_WIFI_OVERRIDDEN // each ovveride must be preceeded with an #undef statement #undef FB_WIFI_SSID @@ -16,12 +17,26 @@ #undef FB_WIFI_PWD #define FB_WIFI_PWD "MY_WIFI_KEY" -// ESP32 default pinout -#undef DEF_RF24_CS_PIN -#define DEF_RF24_CS_PIN 5 -#undef DEF_RF24_CE_PIN -#define DEF_RF24_CE_PIN 4 -#undef DEF_RF24_IRQ_PIN -#define DEF_RF24_IRQ_PIN 16 +// ESP32-S3 example pinout +#undef DEF_CS_PIN +#define DEF_CS_PIN 37 +#undef DEF_CE_PIN +#define DEF_CE_PIN 38 +#undef DEF_IRQ_PIN +#define DEF_IRQ_PIN 47 +#undef DEF_MISO_PIN +#define DEF_MISO_PIN 48 +#undef DEF_MOSI_PIN +#define DEF_MOSI_PIN 35 +#undef DEF_SCLK_PIN +#define DEF_SCLK_PIN 36 + +// Offset for midnight Ticker Example: 1 second before midnight (local time) +#undef MIDNIGHTTICKER_OFFSET +#define MIDNIGHTTICKER_OFFSET (mCalculatedTimezoneOffset + 1) + +// To enable the endpoint for prometheus to scrape data from at /metrics +// #define ENABLE_PROMETHEUS_EP + #endif /*__CONFIG_OVERRIDE_H__*/ diff --git a/src/config/settings.h b/src/config/settings.h new file mode 100644 index 00000000..6d58b406 --- /dev/null +++ b/src/config/settings.h @@ -0,0 +1,615 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __SETTINGS_H__ +#define __SETTINGS_H__ + +#include +#include +#include + +#include "../defines.h" +#include "../utils/dbg.h" +#include "../utils/helper.h" + +#if defined(ESP32) + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxAllocHeap() - 1024 +#else + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxFreeBlockSize() - 1024 +#endif + +/** + * More info: + * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout + * */ +#define DEF_PIN_OFF 255 + + +#define PROT_MASK_INDEX 0x0001 +#define PROT_MASK_LIVE 0x0002 +#define PROT_MASK_SERIAL 0x0004 +#define PROT_MASK_SETUP 0x0008 +#define PROT_MASK_UPDATE 0x0010 +#define PROT_MASK_SYSTEM 0x0020 +#define PROT_MASK_API 0x0040 +#define PROT_MASK_MQTT 0x0080 + +#define DEF_PROT_INDEX 0x0001 +#define DEF_PROT_LIVE 0x0000 +#define DEF_PROT_SERIAL 0x0004 +#define DEF_PROT_SETUP 0x0008 +#define DEF_PROT_UPDATE 0x0010 +#define DEF_PROT_SYSTEM 0x0020 +#define DEF_PROT_API 0x0000 +#define DEF_PROT_MQTT 0x0000 + + +typedef struct { + uint8_t ip[4]; // ip address + uint8_t mask[4]; // sub mask + uint8_t dns1[4]; // dns 1 + uint8_t dns2[4]; // dns 2 + uint8_t gateway[4]; // standard gateway +} cfgIp_t; + +typedef struct { + char deviceName[DEVNAME_LEN]; + char adminPwd[PWD_LEN]; + uint16_t protectionMask; + bool darkMode; + + // wifi + char stationSsid[SSID_LEN]; + char stationPwd[PWD_LEN]; + + cfgIp_t ip; +} cfgSys_t; + +typedef struct { + uint16_t sendInterval; + uint8_t maxRetransPerPyld; + uint8_t pinCs; + uint8_t pinCe; + uint8_t pinIrq; + uint8_t pinMiso; + uint8_t pinMosi; + uint8_t pinSclk; + uint8_t amplifierPower; +} cfgNrf24_t; + +typedef struct { + char addr[NTP_ADDR_LEN]; + uint16_t port; +} cfgNtp_t; + +typedef struct { + float lat; + float lon; + bool disNightCom; // disable night communication + uint16_t offsetSec; +} cfgSun_t; + +typedef struct { + uint16_t interval; + bool showIv; + bool debug; +} cfgSerial_t; + +typedef struct { + uint8_t led0; // first LED pin + uint8_t led1; // second LED pin +} cfgLed_t; + +typedef struct { + char broker[MQTT_ADDR_LEN]; + uint16_t port; + char user[MQTT_USER_LEN]; + char pwd[MQTT_PWD_LEN]; + char topic[MQTT_TOPIC_LEN]; + uint16_t interval; +} cfgMqtt_t; + +typedef struct { + bool enabled; + char name[MAX_NAME_LENGTH]; + serial_u serial; + uint16_t chMaxPwr[4]; + int32_t yieldCor[4]; // signed YieldTotal correction value + char chName[4][MAX_NAME_LENGTH]; +} cfgIv_t; + +typedef struct { + bool enabled; + cfgIv_t iv[MAX_NUM_INVERTERS]; + + bool rstYieldMidNight; + bool rstValsNotAvail; + bool rstValsCommStop; +} cfgInst_t; + +typedef struct { + uint8_t type; + bool pwrSaveAtIvOffline; + bool pxShift; + uint8_t rot; + //uint16_t wakeUp; + //uint16_t sleepAt; + uint8_t contrast; + uint8_t disp_data; + uint8_t disp_clk; + uint8_t disp_cs; + uint8_t disp_reset; + uint8_t disp_busy; + uint8_t disp_dc; +} display_t; + +typedef struct { + display_t display; +} plugins_t; + +typedef struct { + cfgSys_t sys; + cfgNrf24_t nrf; + cfgNtp_t ntp; + cfgSun_t sun; + cfgSerial_t serial; + cfgMqtt_t mqtt; + cfgLed_t led; + cfgInst_t inst; + plugins_t plugin; + bool valid; +} settings_t; + +class settings { + public: + settings() { + mLastSaveSucceed = false; + } + + void setup() { + DPRINTLN(DBG_INFO, F("Initializing FS ..")); + + mCfg.valid = false; + #if !defined(ESP32) + LittleFSConfig cfg; + cfg.setAutoFormat(false); + LittleFS.setConfig(cfg); + #define LITTLFS_TRUE + #define LITTLFS_FALSE + #else + #define LITTLFS_TRUE true + #define LITTLFS_FALSE false + #endif + + if(!LittleFS.begin(LITTLFS_FALSE)) { + DPRINTLN(DBG_INFO, F(".. format ..")); + LittleFS.format(); + if(LittleFS.begin(LITTLFS_TRUE)) { + DPRINTLN(DBG_INFO, F(".. success")); + } else { + DPRINTLN(DBG_INFO, F(".. failed")); + } + + } + else + DPRINTLN(DBG_INFO, F(" .. done")); + + readSettings("/settings.json"); + } + + // should be used before OTA + void stop() { + LittleFS.end(); + DPRINTLN(DBG_INFO, F("FS stopped")); + } + + void getPtr(settings_t *&cfg) { + cfg = &mCfg; + } + + bool getValid(void) { + return mCfg.valid; + } + + inline bool getLastSaveSucceed() { + return mLastSaveSucceed; + } + + void getInfo(uint32_t *used, uint32_t *size) { + #if !defined(ESP32) + FSInfo info; + LittleFS.info(info); + *used = info.usedBytes; + *size = info.totalBytes; + + DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --")); + DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used")); + #else + DPRINTLN(DBG_WARN, F("not supported by ESP32")); + #endif + } + + bool readSettings(const char* path) { + loadDefaults(); + File fp = LittleFS.open(path, "r"); + if(!fp) + DPRINTLN(DBG_WARN, F("failed to load json, using default config")); + else { + //DPRINTLN(DBG_INFO, fp.readString()); + //fp.seek(0, SeekSet); + DynamicJsonDocument root(MAX_ALLOWED_BUF_SIZE); + DeserializationError err = deserializeJson(root, fp); + root.shrinkToFit(); + if(!err && (root.size() > 0)) { + mCfg.valid = true; + jsonWifi(root[F("wifi")]); + jsonNrf(root[F("nrf")]); + jsonNtp(root[F("ntp")]); + jsonSun(root[F("sun")]); + jsonSerial(root[F("serial")]); + jsonMqtt(root[F("mqtt")]); + jsonLed(root[F("led")]); + jsonPlugin(root[F("plugin")]); + jsonInst(root[F("inst")]); + } + else { + Serial.println(F("failed to parse json, using default config")); + } + + fp.close(); + } + return mCfg.valid; + } + + bool saveSettings() { + DPRINTLN(DBG_DEBUG, F("save settings")); + + DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); + JsonObject root = json.to(); + jsonWifi(root.createNestedObject(F("wifi")), true); + jsonNrf(root.createNestedObject(F("nrf")), true); + jsonNtp(root.createNestedObject(F("ntp")), true); + jsonSun(root.createNestedObject(F("sun")), true); + jsonSerial(root.createNestedObject(F("serial")), true); + jsonMqtt(root.createNestedObject(F("mqtt")), true); + jsonLed(root.createNestedObject(F("led")), true); + jsonPlugin(root.createNestedObject(F("plugin")), true); + jsonInst(root.createNestedObject(F("inst")), true); + + DPRINT(DBG_INFO, F("memory usage: ")); + DBGPRINTLN(String(json.memoryUsage())); + DPRINT(DBG_INFO, F("capacity: ")); + DBGPRINTLN(String(json.capacity())); + DPRINT(DBG_INFO, F("max alloc: ")); + DBGPRINTLN(String(MAX_ALLOWED_BUF_SIZE)); + + if(json.overflowed()) { + DPRINTLN(DBG_ERROR, F("buffer too small!")); + mLastSaveSucceed = false; + return false; + } + + File fp = LittleFS.open("/settings.json", "w"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("can't open settings file!")); + mLastSaveSucceed = false; + return false; + } + + if(0 == serializeJson(root, fp)) { + DPRINTLN(DBG_ERROR, F("can't write settings file!")); + mLastSaveSucceed = false; + return false; + } + fp.close(); + + DPRINTLN(DBG_INFO, F("settings saved")); + mLastSaveSucceed = true; + return true; + } + + bool eraseSettings(bool eraseWifi = false) { + if(true == eraseWifi) + return LittleFS.format(); + loadDefaults(!eraseWifi); + return saveSettings(); + } + + private: + void loadDefaults(bool keepWifi = false) { + DPRINTLN(DBG_VERBOSE, F("loadDefaults")); + + cfgSys_t tmp; + if(keepWifi) { + // copy contents which should not be deleted + memset(&tmp.adminPwd, 0, PWD_LEN); + memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t)); + } + // erase all settings and reset to default + memset(&mCfg, 0, sizeof(settings_t)); + mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + mCfg.sys.darkMode = false; + // restore temp settings + if(keepWifi) + memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); + else { + snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); + snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); + } + + snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); + + mCfg.nrf.sendInterval = SEND_INTERVAL; + mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; + mCfg.nrf.pinCs = DEF_CS_PIN; + mCfg.nrf.pinCe = DEF_CE_PIN; + mCfg.nrf.pinIrq = DEF_IRQ_PIN; + mCfg.nrf.pinMiso = DEF_MISO_PIN; + mCfg.nrf.pinMosi = DEF_MOSI_PIN; + mCfg.nrf.pinSclk = DEF_SCLK_PIN; + + mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; + + snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); + mCfg.ntp.port = DEF_NTP_PORT; + + mCfg.sun.lat = 0.0; + mCfg.sun.lon = 0.0; + mCfg.sun.disNightCom = false; + mCfg.sun.offsetSec = 0; + + mCfg.serial.interval = SERIAL_INTERVAL; + mCfg.serial.showIv = false; + mCfg.serial.debug = false; + + mCfg.mqtt.port = DEF_MQTT_PORT; + snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); + snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); + snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); + snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); + mCfg.mqtt.interval = 0; // off + + mCfg.inst.rstYieldMidNight = false; + mCfg.inst.rstValsNotAvail = false; + mCfg.inst.rstValsCommStop = false; + + mCfg.led.led0 = DEF_PIN_OFF; + mCfg.led.led1 = DEF_PIN_OFF; + + memset(&mCfg.inst, 0, sizeof(cfgInst_t)); + + mCfg.plugin.display.pwrSaveAtIvOffline = false; + mCfg.plugin.display.contrast = 60; + mCfg.plugin.display.pxShift = true; + mCfg.plugin.display.rot = 0; + mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA + mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL + mCfg.plugin.display.disp_cs = DEF_PIN_OFF; + mCfg.plugin.display.disp_reset = DEF_PIN_OFF; + mCfg.plugin.display.disp_busy = DEF_PIN_OFF; + mCfg.plugin.display.disp_dc = DEF_PIN_OFF; + } + + void jsonWifi(JsonObject obj, bool set = false) { + if(set) { + char buf[16]; + obj[F("ssid")] = mCfg.sys.stationSsid; + obj[F("pwd")] = mCfg.sys.stationPwd; + obj[F("dev")] = mCfg.sys.deviceName; + obj[F("adm")] = mCfg.sys.adminPwd; + obj[F("prot_mask")] = mCfg.sys.protectionMask; + obj[F("dark")] = mCfg.sys.darkMode; + ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); + ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); + ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); + ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); + ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); + } else { + snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as()); + snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as()); + snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); + snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); + mCfg.sys.protectionMask = obj[F("prot_mask")]; + mCfg.sys.darkMode = obj[F("dark")]; + ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); + ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); + ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); + ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); + ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); + + if(mCfg.sys.protectionMask == 0) + mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + } + } + + void jsonNrf(JsonObject obj, bool set = false) { + if(set) { + obj[F("intvl")] = mCfg.nrf.sendInterval; + obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; + obj[F("cs")] = mCfg.nrf.pinCs; + obj[F("ce")] = mCfg.nrf.pinCe; + obj[F("irq")] = mCfg.nrf.pinIrq; + obj[F("sclk")] = mCfg.nrf.pinSclk; + obj[F("mosi")] = mCfg.nrf.pinMosi; + obj[F("miso")] = mCfg.nrf.pinMiso; + obj[F("pwr")] = mCfg.nrf.amplifierPower; + } else { + mCfg.nrf.sendInterval = obj[F("intvl")]; + mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; + mCfg.nrf.pinCs = obj[F("cs")]; + mCfg.nrf.pinCe = obj[F("ce")]; + mCfg.nrf.pinIrq = obj[F("irq")]; + mCfg.nrf.pinSclk = obj[F("sclk")]; + mCfg.nrf.pinMosi = obj[F("mosi")]; + mCfg.nrf.pinMiso = obj[F("miso")]; + mCfg.nrf.amplifierPower = obj[F("pwr")]; + if((obj[F("cs")] == obj[F("ce")])) { + mCfg.nrf.pinCs = DEF_CS_PIN; + mCfg.nrf.pinCe = DEF_CE_PIN; + mCfg.nrf.pinIrq = DEF_IRQ_PIN; + mCfg.nrf.pinSclk = DEF_SCLK_PIN; + mCfg.nrf.pinMosi = DEF_MOSI_PIN; + mCfg.nrf.pinMiso = DEF_MISO_PIN; + } + } + } + + void jsonNtp(JsonObject obj, bool set = false) { + if(set) { + obj[F("addr")] = mCfg.ntp.addr; + obj[F("port")] = mCfg.ntp.port; + } else { + snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as()); + mCfg.ntp.port = obj[F("port")]; + } + } + + void jsonSun(JsonObject obj, bool set = false) { + if(set) { + obj[F("lat")] = mCfg.sun.lat; + obj[F("lon")] = mCfg.sun.lon; + obj[F("dis")] = mCfg.sun.disNightCom; + obj[F("offs")] = mCfg.sun.offsetSec; + } else { + mCfg.sun.lat = obj[F("lat")]; + mCfg.sun.lon = obj[F("lon")]; + mCfg.sun.disNightCom = obj[F("dis")]; + mCfg.sun.offsetSec = obj[F("offs")]; + } + } + + void jsonSerial(JsonObject obj, bool set = false) { + if(set) { + obj[F("intvl")] = mCfg.serial.interval; + obj[F("show")] = mCfg.serial.showIv; + obj[F("debug")] = mCfg.serial.debug; + } else { + mCfg.serial.interval = obj[F("intvl")]; + mCfg.serial.showIv = obj[F("show")]; + mCfg.serial.debug = obj[F("debug")]; + } + } + + void jsonMqtt(JsonObject obj, bool set = false) { + if(set) { + obj[F("broker")] = mCfg.mqtt.broker; + obj[F("port")] = mCfg.mqtt.port; + obj[F("user")] = mCfg.mqtt.user; + obj[F("pwd")] = mCfg.mqtt.pwd; + obj[F("topic")] = mCfg.mqtt.topic; + obj[F("intvl")] = mCfg.mqtt.interval; + + } else { + mCfg.mqtt.port = obj[F("port")]; + mCfg.mqtt.interval = obj[F("intvl")]; + snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); + snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); + snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); + snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as()); + } + } + + void jsonLed(JsonObject obj, bool set = false) { + if(set) { + obj[F("0")] = mCfg.led.led0; + obj[F("1")] = mCfg.led.led1; + } else { + mCfg.led.led0 = obj[F("0")]; + mCfg.led.led1 = obj[F("1")]; + } + } + + void jsonPlugin(JsonObject obj, bool set = false) { + if(set) { + JsonObject disp = obj.createNestedObject("disp"); + disp[F("type")] = mCfg.plugin.display.type; + disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; + disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; + disp[F("rotation")] = mCfg.plugin.display.rot; + //disp[F("wake")] = mCfg.plugin.display.wakeUp; + //disp[F("sleep")] = mCfg.plugin.display.sleepAt; + disp[F("contrast")] = mCfg.plugin.display.contrast; + disp[F("data")] = mCfg.plugin.display.disp_data; + disp[F("clock")] = mCfg.plugin.display.disp_clk; + disp[F("cs")] = mCfg.plugin.display.disp_cs; + disp[F("reset")] = mCfg.plugin.display.disp_reset; + disp[F("busy")] = mCfg.plugin.display.disp_busy; + disp[F("dc")] = mCfg.plugin.display.disp_dc; + } else { + JsonObject disp = obj["disp"]; + mCfg.plugin.display.type = disp[F("type")]; + mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; + mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; + mCfg.plugin.display.rot = disp[F("rotation")]; + //mCfg.plugin.display.wakeUp = disp[F("wake")]; + //mCfg.plugin.display.sleepAt = disp[F("sleep")]; + mCfg.plugin.display.contrast = disp[F("contrast")]; + mCfg.plugin.display.disp_data = disp[F("data")]; + mCfg.plugin.display.disp_clk = disp[F("clock")]; + mCfg.plugin.display.disp_cs = disp[F("cs")]; + mCfg.plugin.display.disp_reset = disp[F("reset")]; + mCfg.plugin.display.disp_busy = disp[F("busy")]; + mCfg.plugin.display.disp_dc = disp[F("dc")]; + } + } + + void jsonInst(JsonObject obj, bool set = false) { + if(set) { + obj[F("en")] = (bool)mCfg.inst.enabled; + obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; + obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + } + else { + mCfg.inst.enabled = (bool)obj[F("en")]; + mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; + mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; + mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; + } + + JsonArray ivArr; + if(set) + ivArr = obj.createNestedArray(F("iv")); + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + if(set) { + if(mCfg.inst.iv[i].serial.u64 != 0ULL) + jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); + } + else { + if(!obj[F("iv")][i].isNull()) + jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + } + } + } + + void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { + if(set) { + obj[F("en")] = (bool)cfg->enabled; + obj[F("name")] = cfg->name; + obj[F("sn")] = cfg->serial.u64; + for(uint8_t i = 0; i < 4; i++) { + obj[F("yield")][i] = cfg->yieldCor[i]; + obj[F("pwr")][i] = cfg->chMaxPwr[i]; + obj[F("chName")][i] = cfg->chName[i]; + } + } else { + cfg->enabled = (bool)obj[F("en")]; + snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); + cfg->serial.u64 = obj[F("sn")]; + for(uint8_t i = 0; i < 4; i++) { + cfg->yieldCor[i] = obj[F("yield")][i]; + cfg->chMaxPwr[i] = obj[F("pwr")][i]; + snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); + } + } + } + + settings_t mCfg; + bool mLastSaveSucceed; +}; + +#endif /*__SETTINGS_H__*/ diff --git a/src/defines.h b/src/defines.h new file mode 100644 index 00000000..1be438ff --- /dev/null +++ b/src/defines.h @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __DEFINES_H__ +#define __DEFINES_H__ + +#include "config/config.h" + +//------------------------------------- +// VERSION +//------------------------------------- +#define VERSION_MAJOR 0 +#define VERSION_MINOR 6 +#define VERSION_PATCH 0 + +//------------------------------------- +typedef struct { + uint8_t ch; + uint8_t len; + uint8_t packet[MAX_RF_PAYLOAD_SIZE]; +} packet_t; + +typedef enum { + InverterDevInform_Simple = 0, // 0x00 + InverterDevInform_All = 1, // 0x01 + GridOnProFilePara = 2, // 0x02 + HardWareConfig = 3, // 0x03 + SimpleCalibrationPara = 4, // 0x04 + SystemConfigPara = 5, // 0x05 + RealTimeRunData_Debug = 11, // 0x0b + RealTimeRunData_Reality = 12, // 0x0c + RealTimeRunData_A_Phase = 13, // 0x0d + RealTimeRunData_B_Phase = 14, // 0x0e + RealTimeRunData_C_Phase = 15, // 0x0f + AlarmData = 17, // 0x11, Alarm data - all unsent alarms + AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms + RecordData = 19, // 0x13 + InternalData = 20, // 0x14 + GetLossRate = 21, // 0x15 + GetSelfCheckState = 30, // 0x1e + InitDataState = 0xff +} InfoCmdType; + +typedef enum { + TurnOn = 0, // 0x00 + TurnOff = 1, // 0x01 + Restart = 2, // 0x02 + Lock = 3, // 0x03 + Unlock = 4, // 0x04 + ActivePowerContr = 11, // 0x0b + ReactivePowerContr = 12, // 0x0c + PFSet = 13, // 0x0d + CleanState_LockAndAlarm = 20, // 0x14 + SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files + Init = 0xff +} DevControlCmdType; + +typedef enum { + AbsolutNonPersistent = 0UL, // 0x0000 + RelativNonPersistent = 1UL, // 0x0001 + AbsolutPersistent = 256UL, // 0x0100 + RelativPersistent = 257UL // 0x0101 +} PowerLimitControlType; + +union serial_u { + uint64_t u64; + uint8_t b[8]; +}; + +#define MIN_SERIAL_INTERVAL 2 // 5 +#define MIN_SEND_INTERVAL 15 +#define MIN_MQTT_INTERVAL 60 + + +#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0 +#define MQTT_STATUS_AVAIL_NOT_PROD 1 +#define MQTT_STATUS_AVAIL_PROD 2 + +enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; + +//------------------------------------- +// EEPROM +//------------------------------------- +#define SSID_LEN 32 +#define PWD_LEN 64 +#define DEVNAME_LEN 16 +#define NTP_ADDR_LEN 32 // DNS Name + +#define MQTT_ADDR_LEN 64 // DNS Name +#define MQTT_USER_LEN 65 // there is another byte necessary for \0 +#define MQTT_PWD_LEN 65 +#define MQTT_TOPIC_LEN 65 + +#define MQTT_MAX_PACKET_SIZE 384 + +typedef struct { + uint32_t rxFail; + uint32_t rxFailNoAnser; + uint32_t rxSuccess; + uint32_t frmCnt; +} statistics_t; + +#endif /*__DEFINES_H__*/ diff --git a/tools/esp8266/hmDefines.h b/src/hm/hmDefines.h similarity index 95% rename from tools/esp8266/hmDefines.h rename to src/hm/hmDefines.h index b89dd9a4..bd12f72b 100644 --- a/tools/esp8266/hmDefines.h +++ b/src/hm/hmDefines.h @@ -6,15 +6,11 @@ #ifndef __HM_DEFINES_H__ #define __HM_DEFINES_H__ -#include "dbg.h" +#include "../utils/dbg.h" #include - -union serial_u { - uint64_t u64; - uint8_t b[8]; -}; - +// inverter generations +enum {IV_HM = 0, IV_MI}; // units enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE}; @@ -30,9 +26,13 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", "U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId", - "active PowerLimit", /*"reactive PowerLimit","Powerfactor",*/ "LastAlarmCode"}; + "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; const char* const notAvail = "n/a"; +const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, + UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, + UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE}; + // mqtt discovery device classes enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; @@ -113,7 +113,7 @@ const byteAssign_t AlarmDataAssignment[] = { }; #define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t)) #define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off - +#define ALARM_LOG_ENTRY_SIZE 12 //------------------------------------- diff --git a/tools/esp8266/hmInverter.h b/src/hm/hmInverter.h similarity index 71% rename from tools/esp8266/hmInverter.h rename to src/hm/hmInverter.h index d1806e98..b1390b29 100644 --- a/tools/esp8266/hmInverter.h +++ b/src/hm/hmInverter.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -14,6 +14,7 @@ #include "hmDefines.h" #include #include +#include "../config/settings.h" /** * For values which are of interest and not transmitted by the inverter can be @@ -104,37 +105,35 @@ const calcFunc_t calcFunctions[] = { template class Inverter { public: - uint8_t id; // unique id - char name[MAX_NAME_LENGTH]; // human readable name, eg. "HM-600.1" - uint8_t type; // integer which refers to inverter type - uint16_t alarmMesIndex; // Last recorded Alarm Message Index - uint16_t fwVersion; // Firmware Version from Info Command Request - uint16_t powerLimit[2]; // limit power output - float actPowerLimit; // actual power limit - uint8_t devControlCmd; // carries the requested cmd - bool devControlRequest; // true if change needed - serial_u serial; // serial number as on barcode - serial_u radioId; // id converted to modbus - uint8_t channels; // number of PV channels (1-4) - record_t recordMeas; // structure for measured values - record_t recordInfo; // structure for info values - record_t recordConfig; // structure for system config values - record_t recordAlarm; // structure for alarm values - uint16_t chMaxPwr[4]; // maximum power of the modules (Wp) - char chName[4][MAX_NAME_LENGTH]; // human readable name for channels - String lastAlarmMsg; - bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) + uint8_t ivGen; // generation of inverter (HM / MI) + cfgIv_t *config; // stored settings + uint8_t id; // unique id + uint8_t type; // integer which refers to inverter type + uint16_t alarmMesIndex; // Last recorded Alarm Message Index + uint16_t powerLimit[2]; // limit power output + float actPowerLimit; // actual power limit + uint8_t devControlCmd; // carries the requested cmd + serial_u radioId; // id converted to modbus + uint8_t channels; // number of PV channels (1-4) + record_t recordMeas; // structure for measured values + record_t recordInfo; // structure for info values + record_t recordConfig; // structure for system config values + record_t recordAlarm; // structure for alarm values + //String lastAlarmMsg; + bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) + bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) Inverter() { - powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited - powerLimit[1] = AbsolutNonPersistent; // default power limit setting - actPowerLimit = 0xffff; // init feedback from inverter to -1 - devControlRequest = false; - devControlCmd = InitDataState; - initialized = false; - fwVersion = 0; - lastAlarmMsg = "nothing"; - alarmMesIndex = 0; + ivGen = IV_HM; + powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited + powerLimit[1] = AbsolutNonPersistent; // default power limit setting + actPowerLimit = 0xffff; // init feedback from inverter to -1 + mDevControlRequest = false; + devControlCmd = InitDataState; + initialized = false; + //lastAlarmMsg = "nothing"; + alarmMesIndex = 0; + isConnected = false; } ~Inverter() { @@ -144,35 +143,44 @@ class Inverter { template void enqueCommand(uint8_t cmd) { _commandQueue.push(std::make_shared(cmd)); - DPRINTLN(DBG_INFO, "enqueuedCmd: " + String(cmd)); + DPRINT_IVID(DBG_INFO, id); + DBGPRINT(F("enqueCommand: 0x")); + DBGHEXLN(cmd); } void setQueuedCmdFinished() { if (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) - _commandQueue.pop(); + _commandQueue.pop(); } } void clearCmdQueue() { + DPRINTLN(DBG_INFO, F("clearCmdQueue")); while (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) - _commandQueue.pop(); + _commandQueue.pop(); } } - uint8_t getQueuedCmd() { - if (_commandQueue.empty()){ - // Fill with default commands - enqueCommand(RealTimeRunData_Debug); - if (fwVersion == 0) - { // info needed maybe after "one night" (=> DC>0 to DC=0 and to DC>0) or reboot - enqueCommand(InverterDevInform_All); - } - if (actPowerLimit == 0xffff) - { // info needed maybe after "one nigth" (=> DC>0 to DC=0 and to DC>0) or reboot - enqueCommand(SystemConfigPara); + uint8_t getQueuedCmd() { + if (_commandQueue.empty()) { + if (ivGen != IV_MI) { + if (getFwVersion() == 0) + enqueCommand(InverterDevInform_All); // firmware version + enqueCommand(RealTimeRunData_Debug); // live data + } else if (ivGen == IV_MI){ + if (getFwVersion() == 0) + enqueCommand(InverterDevInform_All); // firmware version; might not work, esp. for 1/2 ch hardware + if (type == INV_TYPE_4CH) { + enqueCommand(0x36); + } else { + enqueCommand(0x09); + } } + + if ((actPowerLimit == 0xffff) && isConnected) + enqueCommand(SystemConfigPara); // power limit info } return _commandQueue.front().get()->getCmd(); } @@ -185,8 +193,6 @@ class Inverter { initAssignment(&recordConfig, SystemConfigPara); initAssignment(&recordAlarm, AlarmData); toRadioId(); - memset(name, 0, MAX_NAME_LENGTH); - memset(chName, 0, MAX_NAME_LENGTH * 4); initialized = true; } @@ -229,6 +235,22 @@ class Inverter { return 0; } + bool setDevControlRequest(uint8_t cmd) { + if(isConnected) { + mDevControlRequest = true; + devControlCmd = cmd; + } + return isConnected; + } + + void clearDevControlRequest() { + mDevControlRequest = false; + } + + inline bool getDevControlRequest() { + return mDevControlRequest; + } + void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue")); if(NULL != rec) { @@ -243,11 +265,12 @@ class Inverter { val <<= 8; val |= buf[ptr]; } while(++ptr != end); - if(FLD_T == rec->assign[pos].fieldId) { + if (FLD_T == rec->assign[pos].fieldId) { // temperature is a signed value! rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div); - } - else { + } else if (FLD_YT == rec->assign[pos].fieldId) { + rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); + } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); else @@ -264,34 +287,31 @@ class Inverter { if (alarmMesIndex < rec->record[pos]){ alarmMesIndex = rec->record[pos]; //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? + + DPRINT(DBG_INFO, "alarm ID incremented to "); + DBGPRINTLN(String(alarmMesIndex)); enqueCommand(AlarmData); } - else { - alarmMesIndex = rec->record[pos]; // no change - } } } else if (rec->assign == InfoAssignment) { DPRINTLN(DBG_DEBUG, "add info"); - // get at least the firmware version and save it to the inverter object - if (getPosByChFld(0, FLD_FW_VERSION, rec) == pos){ - fwVersion = rec->record[pos]; - DPRINT(DBG_DEBUG, F("Inverter FW-Version: ") + String(fwVersion)); - } + // eg. fw version ... + isConnected = true; } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); - // get at least the firmware version and save it to the inverter object if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ actPowerLimit = rec->record[pos]; - DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1)); + DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); + DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); } } else if (rec->assign == AlarmDataAssignment) { DPRINTLN(DBG_DEBUG, "add alarm"); - if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ - lastAlarmMsg = getAlarmStr(rec->record[pos]); - } + //if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ + // lastAlarmMsg = getAlarmStr(rec->record[pos]); + //} } else DPRINTLN(DBG_WARN, F("add with unknown assginment")); @@ -300,10 +320,43 @@ class Inverter { DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); } + /*inline REC_TYP getPowerLimit(void) { + record_t<> *rec = getRecordStruct(SystemConfigPara); + return getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, rec); + }*/ + + bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { + DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); + if(NULL == rec) + return false; + if(pos > rec->length) + return false; + rec->record[pos] = val; + return true; + } + + REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) { + uint8_t pos = 0; + if(NULL != rec) { + for(; pos < rec->length; pos++) { + if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) + break; + } + if(pos >= rec->length) + return 0; + + return rec->record[pos]; + } + else + return 0; + } + REC_TYP getValue(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) return 0; + if(pos > rec->length) + return 0; return rec->record[pos]; } @@ -318,20 +371,33 @@ class Inverter { } } - bool isAvailable(uint32_t timestamp, record_t<> *rec) { - DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable")); - return ((timestamp - rec->ts) < INACT_THRES_SEC); + bool isAvailable(uint32_t timestamp) { + if((timestamp - recordMeas.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordInfo.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordConfig.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordAlarm.ts) < INACT_THRES_SEC) + return true; + return false; } - bool isProducing(uint32_t timestamp, record_t<> *rec) { + bool isProducing(uint32_t timestamp) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); - if(isAvailable(timestamp, rec)) { - uint8_t pos = getPosByChFld(CH0, FLD_PAC, rec); - return (getValue(pos, rec) > INACT_PWR_THRESH); + if(isAvailable(timestamp)) { + uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas); + return (getValue(pos, &recordMeas) > INACT_PWR_THRESH); } return false; } + uint16_t getFwVersion() { + record_t<> *rec = getRecordStruct(InverterDevInform_All); + uint8_t pos = getPosByChFld(CH0, FLD_FW_VERSION, rec); + return getValue(pos, rec); + } + uint32_t getLastTs(record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getLastTs")); return rec->ts; @@ -339,10 +405,10 @@ class Inverter { record_t<> *getRecordStruct(uint8_t cmd) { switch (cmd) { - case RealTimeRunData_Debug: return &recordMeas; - case InverterDevInform_All: return &recordInfo; - case SystemConfigPara: return &recordConfig; - case AlarmData: return &recordAlarm; + case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b + case InverterDevInform_All: return &recordInfo; // 1 = 0x01 + case SystemConfigPara: return &recordConfig; // 5 = 0x05 + case AlarmData: return &recordAlarm; // 17 = 0x11 default: break; } return NULL; @@ -405,7 +471,27 @@ class Inverter { } } - String getAlarmStr(u_int16_t alarmCode) { + uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len, uint32_t *start, uint32_t *endTime) { + uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE; + if((startOff + ALARM_LOG_ENTRY_SIZE) > len) + return 0; + + uint16_t wCode = ((uint16_t)pyld[startOff]) << 8 | pyld[startOff+1]; + uint32_t startTimeOffset = 0, endTimeOffset = 0; + + if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM + startTimeOffset = 12 * 60 * 60; + if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM + endTimeOffset = 12 * 60 * 60; + + *start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset; + *endTime = (((uint16_t)pyld[startOff + 6] << 8) | ((uint16_t)pyld[startOff + 7])) + endTimeOffset; + + DPRINTLN(DBG_INFO, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(*start) + ", end: " + ah::getTimeStr(*endTime)); + return pyld[startOff+1]; + } + + String getAlarmStr(uint16_t alarmCode) { switch (alarmCode) { // breaks are intentionally missing! case 1: return String(F("Inverter start")); case 2: return String(F("DTU command failed")); @@ -480,16 +566,18 @@ class Inverter { } private: - std::queue> _commandQueue; void toRadioId(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:toRadioId")); radioId.u64 = 0ULL; - radioId.b[4] = serial.b[0]; - radioId.b[3] = serial.b[1]; - radioId.b[2] = serial.b[2]; - radioId.b[1] = serial.b[3]; + radioId.b[4] = config->serial.b[0]; + radioId.b[3] = config->serial.b[1]; + radioId.b[2] = config->serial.b[2]; + radioId.b[1] = config->serial.b[3]; radioId.b[0] = 0x01; } + + std::queue> _commandQueue; + bool mDevControlRequest; // true if change needed }; @@ -583,8 +671,8 @@ static T calcIrradiation(Inverter<> *iv, uint8_t arg0) { if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); uint8_t pos = iv->getPosByChFld(arg0, FLD_PDC, rec); - if(iv->chMaxPwr[arg0-1] > 0) - return iv->getValue(pos, rec) / iv->chMaxPwr[arg0-1] * 100.0f; + if(iv->config->chMaxPwr[arg0-1] > 0) + return iv->getValue(pos, rec) / iv->config->chMaxPwr[arg0-1] * 100.0f; } return 0.0; } diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h new file mode 100644 index 00000000..0af8f466 --- /dev/null +++ b/src/hm/hmPayload.h @@ -0,0 +1,419 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HM_PAYLOAD_H__ +#define __HM_PAYLOAD_H__ + +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint8_t txCmd; + uint8_t txId; + uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; + bool lastFound; + uint8_t retransmits; + bool requested; + bool gotFragment; +} invPayload_t; + + +typedef std::function payloadListenerType; +typedef std::function alarmListenerType; + + +template +class HmPayload { + public: + HmPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i); + } + mSerialDebug = false; + mHighPrioIv = NULL; + mCbAlarm = NULL; + mCbPayload = NULL; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbAlarm = cb; + } + + void loop() { + if(NULL != mHighPrioIv) { + ivSend(mHighPrioIv, true); + mHighPrioIv = NULL; + } + } + + void zeroYieldDay(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + uint8_t pos; + for(uint8_t ch = 0; ch < iv->channels; ch++) { + pos = iv->getPosByChFld(CH0, FLD_YD, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + void zeroInverterValues(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + for(uint8_t fld = 0; fld < FLD_EVT; fld++) { + switch(fld) { + case FLD_YD: + case FLD_YT: + continue; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + notify(RealTimeRunData_Debug); + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) { + DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + } + } + + reset(iv->id); + mPayload[iv->id].requested = true; + + yield(); + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + mPayload[iv->id].txCmd = cmd; + } + } + + void add(Inverter<> *iv, packet_t *p) { + if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command + mPayload[iv->id].txId = p->packet[0]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->packet[9]; + if (*pid == 0x00) { + DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); + } else { + DPRINT(DBG_DEBUG, F("PID: 0x")); + DPRINTLN(DBG_DEBUG, String(*pid, HEX)); + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; + mPayload[iv->id].gotFragment = true; + } + + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + } + } + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command + DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->packet[0]; + iv->clearDevControlRequest(); + + if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { + bool ok = true; + if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) + mApp->setMqttPowerLimitAck(iv); + else + ok = false; + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has ")); + if(!ok) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + } + iv->devControlCmd = Init; + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (IV_MI == iv->ivGen) // only process HM inverters + continue; // skip to next inverter + + if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { + // no processing needed if txId is not 0x95 + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + if (!mPayload[iv->id].complete) { + bool crcPass, pyldComplete; + crcPass = build(iv->id, &pyldComplete); + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINT_IVID(DBG_INFO, iv->id); + DPRINTLN(DBG_INFO, F("retransmit power limit")); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); + } else { + if(false == mPayload[iv->id].gotFragment) { + /* + DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + */ + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else { + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { + if (mPayload[iv->id].len[i] == 0) { + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINT(F("Frame ")); + DBGPRINT(String(i + 1)); + DBGPRINTLN(F(" missing: Request Retransmit")); + mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + break; // only request retransmit one frame per loop + } + yield(); + } + } + } + } + } + } else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + } else { // payload complete + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); + DPRINT(DBG_DEBUG, F("procPyld: max: ")); + DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + } + payloadLen -= 2; + + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (")); + DBGPRINT(String(payloadLen)); + DBGPRINT(F("): ")); + mSys->Radio.dumpBuf(payload, payloadLen); + } + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbPayload) + (mCbPayload)(val); + } + + void notify(uint16_t code, uint32_t start, uint32_t endTime) { + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, endTime); + } + + bool build(uint8_t id, bool *complete) { + DPRINTLN(DBG_VERBOSE, F("build")); + uint16_t crc = 0xffff, crcRcv = 0x0000; + if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + + // check if all fragments are there + *complete = true; + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if(mPayload[id].len[i] == 0) + *complete = false; + } + if(!*complete) + return false; + + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if (mPayload[id].len[i] > 0) { + if (i == (mPayload[id].maxPackId - 1)) { + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); + } else + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); + } + yield(); + } + + return (crc == crcRcv) ? true : false; + } + + void reset(uint8_t id) { + DPRINT(DBG_INFO, "resetPayload: id: "); + DBGPRINTLN(String(id)); + memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[id].txCmd = 0; + mPayload[id].gotFragment = false; + mPayload[id].retransmits = 0; + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false; + mPayload[id].complete = false; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + } + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + invPayload_t mPayload[MAX_NUM_INVERTERS]; + bool mSerialDebug; + Inverter<> *mHighPrioIv; + + alarmListenerType mCbAlarm; + payloadListenerType mCbPayload; +}; + +#endif /*__HM_PAYLOAD_H__*/ diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h new file mode 100644 index 00000000..c37ab7d0 --- /dev/null +++ b/src/hm/hmRadio.h @@ -0,0 +1,371 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __RADIO_H__ +#define __RADIO_H__ + +#include "../utils/dbg.h" +#include +#include "../utils/crc.h" +#include "../config/config.h" +#include "SPI.h" + +#define SPI_SPEED 1000000 + +#define RF_CHANNELS 5 + +#define TX_REQ_INFO 0x15 +#define TX_REQ_DEVCONTROL 0x51 +#define ALL_FRAMES 0x80 +#define SINGLE_FRAME 0x81 + +const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; + + +//----------------------------------------------------------------------------- +// MACROS +//----------------------------------------------------------------------------- +#define CP_U32_LittleEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[0] = ((v >> 24) & 0xff); \ + b[1] = ((v >> 16) & 0xff); \ + b[2] = ((v >> 8) & 0xff); \ + b[3] = ((v ) & 0xff); \ +}) + +#define CP_U32_BigEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[3] = ((v >> 24) & 0xff); \ + b[2] = ((v >> 16) & 0xff); \ + b[1] = ((v >> 8) & 0xff); \ + b[0] = ((v ) & 0xff); \ +}) + +#define BIT_CNT(x) ((x)<<3) + +//----------------------------------------------------------------------------- +// HM Radio class +//----------------------------------------------------------------------------- +template +class HmRadio { + public: + HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { + DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); + DPRINT(DBG_VERBOSE, String(CE_PIN)); + DPRINT(DBG_VERBOSE, F(", CS_PIN: ")); + DPRINT(DBG_VERBOSE, String(CS_PIN)); + DPRINT(DBG_VERBOSE, F(", SPI_SPEED: ")); + DPRINTLN(DBG_VERBOSE, String(SPI_SPEED) + ")"); + + // Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz. + // Channel List 2403, 2423, 2440, 2461, 2475MHz + mRfChLst[0] = 03; + mRfChLst[1] = 23; + mRfChLst[2] = 40; + mRfChLst[3] = 61; + mRfChLst[4] = 75; + + // default channels + mTxChIdx = 2; // Start TX with 40 + mRxChIdx = 0; // Start RX with 03 + + mSendCnt = 0; + mRetransmits = 0; + + mSerialDebug = false; + mIrqRcvd = false; + } + ~HmRadio() {} + + void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { + DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); + pinMode(irq, INPUT_PULLUP); + + uint32_t dtuSn = 0x87654321; + uint32_t chipID = 0; // will be filled with last 3 bytes of MAC + #ifdef ESP32 + uint64_t MAC = ESP.getEfuseMac(); + chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); + #else + chipID = ESP.getChipId(); + #endif + if(chipID) { + dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal + for(int i = 0; i < 7; i++) { + dtuSn |= (chipID % 10) << (i * 4); + chipID /= 10; + } + } + // change the byte order of the DTU serial number and append the required 0x01 at the end + DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; + + #ifdef ESP32 + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(FSPI); + #else + mSpi = new SPIClass(VSPI); + #endif + mSpi->begin(sclk, miso, mosi, cs); + #else + //the old ESP82xx cannot freely place their SPI pins + mSpi = new SPIClass(); + mSpi->begin(); + #endif + mNrf24.begin(mSpi, ce, cs); + mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms + + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + mNrf24.setDataRate(RF24_250KBPS); + mNrf24.setAutoAck(true); + mNrf24.enableDynamicPayloads(); + mNrf24.setCRCLength(RF24_CRC_16); + mNrf24.setAddressWidth(5); + mNrf24.openReadingPipe(1, DTU_RADIO_ID); + + // enable all receiving interrupts + mNrf24.maskIRQ(false, false, false); + + DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); + DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); + mNrf24.setPALevel(ampPwr & 0x03); + + if(mNrf24.isChipConnected()) { + DPRINTLN(DBG_INFO, F("Radio Config:")); + mNrf24.printPrettyDetails(); + } + else + DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); + } + + bool loop(void) { + if (!mIrqRcvd) + return false; // nothing to do + mIrqRcvd = false; + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + mNrf24.flush_tx(); // empty TX FIFO + //DBGPRINTLN("TX whatHappened Ch" + String(mRfChLst[mTxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready)); + + // start listening on the default RX channel + mRxChIdx = 0; + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + + //uint32_t debug_ms = millis(); + uint16_t cnt = 300; // that is 60 times 5 channels + while (0 < cnt--) { + uint32_t startMillis = millis(); + while (millis()-startMillis < 4) { // listen 4ms to each channel + if (mIrqRcvd) { + mIrqRcvd = false; + if (getReceived()) { // everything received + //DBGPRINTLN("RX finished Cnt: " + String(300-cnt) + " time used: " + String(millis()-debug_ms)+ " ms"); + return true; + } + } + yield(); + } + switchRxCh(); // switch to next RX channel + yield(); + } + // not finished but time is over + //DBGPRINTLN("RX not finished: 300 time used: " + String(millis()-debug_ms)+ " ms"); + return true; + } + + void handleIntr(void) { + mIrqRcvd = true; + } + + bool isChipConnected(void) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); + return mNrf24.isChipConnected(); + } + void enableDebug() { + mSerialDebug = true; + } + + void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + DBGHEXLN(cmd); + initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + uint8_t cnt = 10; + if (isNoMI) { + mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor + mTxBuf[cnt++] = 0x00; + if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet + mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit + mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit + mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + } + } else { //MI 2nd gen. specific + switch (cmd) { + case TurnOn: + mTxBuf[9] = 0x55; + mTxBuf[10] = 0xaa; + break; + case TurnOff: + mTxBuf[9] = 0xaa; + mTxBuf[10] = 0x55; + break; + case ActivePowerContr: + cnt++; + mTxBuf[9] = 0x5a; + mTxBuf[10] = 0x5a; + mTxBuf[11] = data[0]; // power limit + break; + default: + return; + } + cnt++; + } + sendPacket(invId, cnt, isRetransmit, true); + } + + void prepareDevInformCmd(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg. + DPRINTLN(DBG_DEBUG, F("prepareDevInformCmd 0x") + String(cmd, HEX)); + initPacket(invId, reqfld, ALL_FRAMES); + mTxBuf[10] = cmd; // cid + mTxBuf[11] = 0x00; + CP_U32_LittleEndian(&mTxBuf[12], ts); + if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { + mTxBuf[18] = (alarmMesId >> 8) & 0xff; + mTxBuf[19] = (alarmMesId ) & 0xff; + } + sendPacket(invId, 24, isRetransmit, true); + } + + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit) { + initPacket(invId, mid, pid); + sendPacket(invId, 10, isRetransmit, false); + } + + void dumpBuf(uint8_t buf[], uint8_t len) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); + for(uint8_t i = 0; i < len; i++) { + DHEX(buf[i]); + DBGPRINT(" "); + } + DBGPRINTLN(""); + } + + uint8_t getDataRate(void) { + if(!mNrf24.isChipConnected()) + return 3; // unkown + return mNrf24.getDataRate(); + } + + bool isPVariant(void) { + return mNrf24.isPVariant(); + } + + std::queue mBufCtrl; + + uint32_t mSendCnt; + uint32_t mRetransmits; + + bool mSerialDebug; + + private: + bool getReceived(void) { + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + //DBGPRINTLN("RX whatHappened Ch" + String(mRfChLst[mRxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready)); + + bool isLastPackage = false; + while(mNrf24.available()) { + uint8_t len; + len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed + if (len > 0) { + packet_t p; + p.ch = mRfChLst[mRxChIdx]; + p.len = len; + mNrf24.read(p.packet, len); + mBufCtrl.push(p); + if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + isLastPackage = (p.packet[9] > 0x11); // > 0x11 indicates last packet received + else if (p.packet[0] != 0x00 && p.packet[0] != 0x88 && p.packet[0] != 0x92) + // ignore fragment number zero and MI status messages + isLastPackage = true; // response from dev control command + yield(); + } + } + return isLastPackage; + } + + void switchRxCh() { + mNrf24.stopListening(); + // get next channel index + if(++mRxChIdx >= RF_CHANNELS) + mRxChIdx = 0; + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + } + + void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { + DPRINTLN(DBG_VERBOSE, F("initPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); + memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); + mTxBuf[0] = mid; // message id + CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); + CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); + mTxBuf[9] = pid; + } + + void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool clear=false) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); + //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); + + // append crc's + if (len > 10) { + // crc control data + uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); + mTxBuf[len++] = (crc >> 8) & 0xff; + mTxBuf[len++] = (crc ) & 0xff; + } + // crc over all + mTxBuf[len] = ah::crc8(mTxBuf, len); + len++; + + if(mSerialDebug) { + DPRINT(DBG_INFO, F("TX ")); + DBGPRINT(String(len)); + DBGPRINT("B Ch"); + DBGPRINT(String(mRfChLst[mTxChIdx])); + DBGPRINT(F(" | ")); + dumpBuf(mTxBuf, len); + } + + mNrf24.stopListening(); + mNrf24.setChannel(mRfChLst[mTxChIdx]); + mNrf24.openWritingPipe(reinterpret_cast(&invId)); + mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + + // switch TX channel for next packet + if(++mTxChIdx >= RF_CHANNELS) + mTxChIdx = 0; + + if(isRetransmit) + mRetransmits++; + else + mSendCnt++; + } + + volatile bool mIrqRcvd; + uint64_t DTU_RADIO_ID; + + uint8_t mRfChLst[RF_CHANNELS]; + uint8_t mTxChIdx; + uint8_t mRxChIdx; + + SPIClass* mSpi; + RF24 mNrf24; + uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; +}; + +#endif /*__RADIO_H__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h new file mode 100644 index 00000000..a95d4d24 --- /dev/null +++ b/src/hm/hmSystem.h @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HM_SYSTEM_H__ +#define __HM_SYSTEM_H__ + +#include "hmInverter.h" +#include "hmRadio.h" + +template > +class HmSystem { + public: + HmRadio<> Radio; + + HmSystem() {} + + void setup() { + mNumInv = 0; + Radio.setup(); + } + + void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) { + mNumInv = 0; + Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin); + } + + void addInverters(cfgInst_t *config) { + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = addInverter(&config->iv[i]); + if (0ULL != config->iv[i].serial.u64) { + if (NULL != iv) { + DPRINT(DBG_INFO, "added inverter "); + if(iv->config->serial.b[5] == 0x11) + DBGPRINT("HM"); + else { + DBGPRINT(((iv->config->serial.b[4] & 0x03) == 0x01) ? " (2nd Gen) " : " (3rd Gen) "); + } + + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + + if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01)) + DPRINTLN(DBG_WARN, F("MI Inverter are not fully supported now!!!")); + } + } + } + } + + INVERTERTYPE *addInverter(cfgIv_t *config) { + DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); + if(MAX_INVERTER <= mNumInv) { + DPRINT(DBG_WARN, F("max number of inverters reached!")); + return NULL; + } + INVERTERTYPE *p = &mInverter[mNumInv]; + p->id = mNumInv; + p->config = config; + DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->config->serial.b[5], HEX)); + DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX)); + if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) { + switch(p->config->serial.b[4]) { + case 0x22: + case 0x21: p->type = INV_TYPE_1CH; break; + case 0x42: + case 0x41: p->type = INV_TYPE_2CH; break; + case 0x62: + case 0x61: p->type = INV_TYPE_4CH; break; + default: + DPRINTLN(DBG_ERROR, F("unknown inverter type")); + break; + } + + if(p->config->serial.b[5] == 0x11) + p->ivGen = IV_HM; + else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM + p->ivGen = IV_HM; + else // MI 2nd Gen + p->ivGen = IV_MI; + } + else if(p->config->serial.u64 != 0ULL) + DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); + + p->init(); + + mNumInv ++; + return p; + } + + INVERTERTYPE *findInverter(uint8_t buf[]) { + DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); + INVERTERTYPE *p; + for(uint8_t i = 0; i < mNumInv; i++) { + p = &mInverter[i]; + if((p->config->serial.b[3] == buf[0]) + && (p->config->serial.b[2] == buf[1]) + && (p->config->serial.b[1] == buf[2]) + && (p->config->serial.b[0] == buf[3])) + return p; + } + return NULL; + } + + INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { + DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); + if(pos >= MAX_INVERTER) + return NULL; + else if((mInverter[pos].initialized && mInverter[pos].config->serial.u64 != 0ULL) || false == check) + return &mInverter[pos]; + else + return NULL; + } + + uint8_t getNumInverters(void) { + /*uint8_t num = 0; + INVERTERTYPE *p; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + p = &mInverter[i]; + if(p->config->serial.u64 != 0ULL) + num++; + } + return num;*/ + return MAX_NUM_INVERTERS; + } + + void enableDebug() { + Radio.enableDebug(); + } + + private: + INVERTERTYPE mInverter[MAX_INVERTER]; + uint8_t mNumInv; +}; + +#endif /*__HM_SYSTEM_H__*/ diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h new file mode 100644 index 00000000..dbe43c06 --- /dev/null +++ b/src/hm/miPayload.h @@ -0,0 +1,825 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __MI_PAYLOAD_H__ +#define __MI_PAYLOAD_H__ + +//#include "hmInverter.h" +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint32_t ts; + bool requested; + bool limitrequested; + uint8_t txCmd; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + bool dataAB[3]; + bool stsAB[3]; + uint16_t sts[6]; + uint8_t txId; + uint8_t invId; + uint8_t retransmits; + //uint8_t skipfirstrepeat; + bool gotFragment; + /* + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t maxPackId; + bool lastFound;*/ +} miPayload_t; + + +typedef std::function miPayloadListenerType; + + +template +class MiPayload { + public: + MiPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i, true); + mPayload[i].limitrequested = true; + } + mSerialDebug = false; + mHighPrioIv = NULL; + mCbMiPayload = NULL; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(miPayloadListenerType cb) { + mCbMiPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbMiAlarm = cb; + } + + void loop() { + if(NULL != mHighPrioIv) { // && mHighPrioIv->ivGen == IV_MI) { + ivSend(mHighPrioIv, true); // for devcontrol commands? + mHighPrioIv = NULL; + } + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (!mPayload[iv->id].gotFragment) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("enqueued cmd failed/timeout")); + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + } + } + + reset(iv->id); + mPayload[iv->id].requested = true; + + yield(); + if (mSerialDebug){ + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DHEX(iv->devControlCmd); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + mPayload[iv->id].limitrequested = true; + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // try to read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + uint8_t cmd2 = cmd; + if ( cmd == SystemConfigPara ) { //0x05 for HM-types + if (!mPayload[iv->id].limitrequested) { // only do once at startup + iv->setQueuedCmdFinished(); + cmd = iv->getQueuedCmd(); + } else { + mPayload[iv->id].limitrequested = false; + } + } + + if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types + cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command + cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame? + mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); + } else { + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + }; + + mPayload[iv->id].txCmd = cmd; + if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH1] = false; + mPayload[iv->id].stsAB[CH1] = false; + mPayload[iv->id].dataAB[CH0] = false; + mPayload[iv->id].stsAB[CH0] = false; + } + + if (iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH2] = false; + mPayload[iv->id].stsAB[CH2] = false; + } + } + } + + void add(Inverter<> *iv, packet_t *p) { + //DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX)); + if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09 + miStsDecode(iv, p); + } + + else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11 + miStsDecode(iv, p, CH2); + } + + else if ( p->packet[0] == 0x09 + ALL_FRAMES || + p->packet[0] == 0x11 + ALL_FRAMES || + ( p->packet[0] >= (0x36 + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME) + && mPayload[iv->id].txCmd != 0x0f) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 + mPayload[iv->id].txId = p->packet[0]; + miDataDecode(iv,p); + } + + else if (p->packet[0] == ( 0x0f + ALL_FRAMES)) { + // MI response from get hardware information request + record_t<> *rec = iv->getRecordStruct(InverterDevInform_All); // choose the record structure + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + +/* + Polling the device software and hardware version number command + start byte Command word routing address target address User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] + 0x7e 0x0f xx xx xx xx YY YY YY YY 0x00 CRC 0x7f + Command Receipt - First Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x00 USFWBuild_VER APPFWBuild_VER APPFWBuild_YYYY APPFWBuild_MMDD APPFWBuild_HHMM APPFW_PN HW_VER CRC 0x7f + Command Receipt - Second Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x01 HW_PN HW_FB_TLmValue HW_FB_ReSPRT HW_GridSamp_ResValule HW_ECapValue Matching_APPFW_PN CRC 0x7f + Command receipt - third frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[15] byte[16] byte[17] byte[18] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x12 APPFW_MINVER HWInfoAddr PNInfoCRC_gusv PNInfoCRC_gusv CRC 0x7f +*/ + +/* +case InverterDevInform_All: + rec->length = (uint8_t)(HMINFO_LIST_LEN); + rec->assign = (byteAssign_t *)InfoAssignment; + rec->pyldLen = HMINFO_PAYLOAD_LEN; + break; +const byteAssign_t InfoAssignment[] = { + { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, + { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, + { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, + { FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } +}; +*/ + + if ( p->packet[9] == 0x00 ) {//first frame + //FLD_FW_VERSION + for (uint8_t i = 0; i < 5; i++) { + iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); + } + iv->isConnected = true; + if(mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DPRINT(DBG_INFO,F("HW_VER is ")); + DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); + } + /*iv->setQueuedCmdFinished(); + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);*/ + } else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10 + DPRINT_IVID(DBG_INFO, iv->id); + if ( p->packet[9] == 0x01 ) { + DBGPRINTLN(F("got 2nd frame (hw info)")); + } else { + DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[] + } + // xlsx: HW_ECapValue is total energy?!? (data coll. inst. #154) + DPRINT(DBG_INFO,F("HW_PartNo ")); + DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); + //DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); + if ( p->packet[9] == 0x01 ) { + iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1); + if(mSerialDebug) { + DPRINT(DBG_INFO,F("HW_ECapValue ")); + DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); + + DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); + DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); + DPRINT(DBG_INFO,F("HW_FB_ReSPRT ")); + DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17])); + DPRINT(DBG_INFO,F("HW_GridSamp_ResValule ")); + DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19])); + } + } + } else if ( p->packet[9] == 0x12 ) {//3rd frame + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("got 3rd frame (hw info)")); + iv->setQueuedCmdFinished(); + mStat->rxSuccess++; + } + + } else if ( p->packet[0] == (TX_REQ_INFO + ALL_FRAMES) // response from get information command + || (p->packet[0] == 0xB6 && mPayload[iv->id].txCmd != 0x36)) { // strange short response from MI-1500 3rd gen; might be missleading! + // atm, we just do nothing else than print out what we got... + // for decoding see xls- Data collection instructions - #147ff + //mPayload[iv->id].txId = p->packet[0]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->packet[9]; + if (*pid == 0x00) { + DPRINT(DBG_DEBUG, F("fragment number zero received")); + iv->setQueuedCmdFinished(); + } else if (p->packet[9] == 0x81) { // might need some additional check, as this is only ment for short answers! + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINTLN(F("seems to use 3rd gen. protocol - switching ivGen!")); + iv->ivGen = IV_HM; + iv->setQueuedCmdFinished(); + iv->clearCmdQueue(); + //DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); + /* (old else-tree) + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {^ + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; + mPayload[iv->id].gotFragment = true; + } + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + }*/ + } + //} + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES ) // response from dev control command + || p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES -1)) { // response from DRED instruction + DPRINT_IVID(DBG_DEBUG, iv->id); + DBGPRINTLN(F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->packet[0]; + iv->clearDevControlRequest(); + + if ((p->packet[9] == 0x5a) && (p->packet[10] == 0x5a)) { + mApp->setMqttPowerLimitAck(iv); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + } + iv->devControlCmd = Init; + } else { // some other response; copied from hmPayload:process; might not be correct to do that here!!! + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); + //DPRINT(DBG_DEBUG, F("procPyld: max: ")); + //DBGPRINTLN(String(mPayload[iv->id].maxPackId)); + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + /*for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + }*/ + payloadLen -= 2; + + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); + mSys->Radio.dumpBuf(payload, payloadLen); + } + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbMiAlarm) + (mCbMiAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (IV_HM == iv->ivGen) // only process MI inverters + continue; // skip to next inverter + + if ( !mPayload[iv->id].complete && + (mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && + (mPayload[iv->id].txId < (0x36 + ALL_FRAMES)) && + (mPayload[iv->id].txId > (0x39 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x09 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x11 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x88)) && + (mPayload[iv->id].txId != (0x92)) && + (mPayload[iv->id].txId != 0 )) { + // no processing needed if txId is not one of 0x95, 0x88, 0x89, 0x91, 0x92 or resonse to 0x36ff + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + //delayed next message? + //mPayload[iv->id].skipfirstrepeat++; + /*if (mPayload[iv->id].skipfirstrepeat) { + mPayload[iv->id].skipfirstrepeat = 0; //reset counter + continue; // skip to next inverter + }*/ + + if (!mPayload[iv->id].complete) { + //DPRINTLN(DBG_INFO, F("Pyld incompl code")); //info for testing only + bool crcPass, pyldComplete; + crcPass = build(iv->id, &pyldComplete); + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("retransmit power limit")); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false); + } else { + uint8_t cmd = mPayload[iv->id].txCmd; + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if( !mPayload[iv->id].gotFragment ) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if ( cmd == 0x0f ) { + //hard/firmware request + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true); + //iv->setQueuedCmdFinished(); + //cmd = iv->getQueuedCmd(); + } else { + bool change = false; + if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command + //cmd++; // just request the next channel + //change = true; + } else if ( cmd == 0x09 ) {//MI single or dual channel device + if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { + if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {} + //first try to get missing sts for first channel a second time + else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { + cmd = 0x11; + change = true; + mPayload[iv->id].retransmits = 0; //reset counter + } + } + } else if ( cmd == 0x11) { + if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there? + if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) { + cmd = 0x09; + change = true; + } + } + } + DPRINT_IVID(DBG_INFO, iv->id); + if (change) { + DBGPRINT(F("next request is")); + //mPayload[iv->id].skipfirstrepeat = 0; + mPayload[iv->id].txCmd = cmd; + } else { + DBGPRINT(F("sth.")); + DBGPRINT(F(" missing: Request Retransmit")); + } + DBGPRINT(F(" 0x")); + DBGHEXLN(cmd); + //mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd); + yield(); + } + } + } + } + } else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + } + /*else { // payload complete + //This tree is not really tested, most likely it's not truly complete.... + DPRINTLN(DBG_INFO, F("procPyld: cmd: 0x") + String(mPayload[iv->id].txCmd, HEX)); + DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); + //DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); + //record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + //uint8_t payload[128]; + //uint8_t payloadLen = 0; + //memset(payload, 0, 128); + //for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + // memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + // payloadLen += (mPayload[iv->id].len[i]); + // yield(); + //} + //payloadLen -= 2; + //if (mSerialDebug) { + // DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); + // mSys->Radio.dumpBuf(payload, payloadLen); + //} + //if (NULL == rec) { + // DPRINTLN(DBG_ERROR, F("record is NULL!")); + //} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + // if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + // mStat->rxSuccess++; + // rec->ts = mPayload[iv->id].ts; + // for (uint8_t i = 0; i < rec->length; i++) { + // iv->addValue(i, payload, rec); + // yield(); + // } + // iv->doCalculations(); + // notify(mPayload[iv->id].txCmd); + // if(AlarmData == mPayload[iv->id].txCmd) { + // uint8_t i = 0; + // uint16_t code; + // uint32_t start, end; + // while(1) { + // code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + // if(0 == code) + // break; + // if (NULL != mCbAlarm) + // (mCbAlarm)(code, start, end); + // yield(); + // } + // } + //} else { + // DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); + // mStat->rxFail++; + //} + //iv->setQueuedCmdFinished(); + //}*/ + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbMiPayload) + (mCbMiPayload)(val); + } + + void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX)); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + mPayload[iv->id].txId = p->packet[0]; + miStsConsolidate(iv, stschan, rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); + mPayload[iv->id].stsAB[stschan] = true; + if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) + mPayload[iv->id].stsAB[CH0] = true; + //mPayload[iv->id].skipfirstrepeat = 1; + if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) { + miComplete(iv); + } + } + + void miStsConsolidate(Inverter<> *iv, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { + //uint8_t status = (p->packet[11] << 8) + p->packet[12]; + uint16_t status = 3; // regular status for MI, change to 1 later? + if ( uState == 2 ) { + status = 5050 + stschan; //first approach, needs review! + if (lState) + status += lState*10; + } else if ( uState > 3 ) { + status = uState*1000 + uEnum*10; + if (lState) + status += lState*100; //needs review, esp. for 4ch-8310 state! + //if (lEnum) + status += lEnum; + if (uEnum < 6) { + status += stschan; + } + if (status == 8000) + status = 8310; //trick? + } + + uint16_t prntsts = status == 3 ? 1 : status; + if ( status != mPayload[iv->id].sts[stschan] ) { //sth.'s changed? + mPayload[iv->id].sts[stschan] = status; + DPRINT(DBG_WARN, F("Status change for CH")); + DBGPRINT(String(stschan)); DBGPRINT(F(" (")); + DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); + DBGPRINTLN(iv->getAlarmStr(prntsts)); + } + + if ( !mPayload[iv->id].sts[0] || prntsts < mPayload[iv->id].sts[0] ) { + mPayload[iv->id].sts[0] = prntsts; + iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); + } + + if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ + iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("alarm ID incremented to ")); + DBGPRINTLN(String(iv->alarmMesIndex)); + } + /*if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ + } + + void miDataDecode(Inverter<> *iv, packet_t *p) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + + uint8_t datachan = ( p->packet[0] == 0x89 || p->packet[0] == (0x36 + ALL_FRAMES) ) ? CH1 : + ( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : + p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : + CH4; + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan); + // count in RF_communication_protocol.xlsx is with offset = -1 + iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); + yield(); + iv->setValue(iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100); + iv->setValue(iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10); + yield(); + iv->setValue(iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10); + iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan))); + //AC Power is missing; we may have to calculate, as no respective data is in payload + + if ( datachan < 3 ) { + mPayload[iv->id].dataAB[datachan] = true; + } + if ( !mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH2] && mPayload[iv->id].dataAB[CH2] ) { + mPayload[iv->id].dataAB[CH0] = true; + } + + if (p->packet[0] >= (0x36 + ALL_FRAMES) ) { + + /*For MI1500: + if (MI1500) { + STAT = (uint8_t)(p->packet[25] ); + FCNT = (uint8_t)(p->packet[26]); + FCODE = (uint8_t)(p->packet[27]); + }*/ + + /*uint16_t status = (uint8_t)(p->packet[23]); + mPayload[iv->id].sts[datachan] = status; + if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) { + mPayload[iv->id].sts[0] = status; + iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status); + }*/ + miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]); + + if (p->packet[0] < (0x39 + ALL_FRAMES) ) { + /*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1; + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + mPayload[iv->id].txCmd = cmd;*/ + mPayload[iv->id].txCmd++; + if (mPayload[iv->id].retransmits) + mPayload[iv->id].retransmits--; // reserve retransmissions for each response + mPayload[iv->id].complete = false; + } + + else if (p->packet[0] == (0x39 + ALL_FRAMES) ) { + /*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1; + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + mPayload[iv->id].txCmd = cmd;*/ + mPayload[iv->id].complete = true; + } + + /*if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ + iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT_TXT(TXT_INCRALM); + DBGPRINTLN(String(iv->alarmMesIndex)); + }*/ + + } + + if ( mPayload[iv->id].complete || //4ch device + (iv->type != INV_TYPE_4CH //other devices + && mPayload[iv->id].dataAB[CH0] + && mPayload[iv->id].stsAB[CH0])) { + miComplete(iv); + } + + + +/* + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbMiAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ + } + + void miComplete(Inverter<> *iv) { + if (mPayload[iv->id].complete) + return; //if we got second message as well in repreated attempt + mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short... + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("got all msgs")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); + + //preliminary AC calculation... + float ac_pow = 0; + for(uint8_t i = 1; i <= iv->channels; i++) { + if (mPayload[iv->id].sts[i] == 3) { + uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); + ac_pow += iv->getValue(pos, rec); + } + } + ac_pow = (int) (ac_pow*9.5); + iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); + + iv->doCalculations(); + iv->setQueuedCmdFinished(); + mStat->rxSuccess++; + yield(); + notify(mPayload[iv->id].txCmd); + } + + bool build(uint8_t id, bool *complete) { + DPRINTLN(DBG_VERBOSE, F("build")); + // check if all messages are there + + *complete = mPayload[id].complete; + uint8_t txCmd = mPayload[id].txCmd; + + if(!*complete) { + DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX)); + //DBGHEXLN(txCmd); + if (txCmd == 0x09 || txCmd == 0x11 || (txCmd >= 0x36 && txCmd <= 0x39)) + return false; + } + + return true; + } + + void reset(uint8_t id, bool clrSts = false) { + DPRINT_IVID(DBG_INFO, id); + DBGPRINTLN(F("resetPayload")); + memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[id].gotFragment = false; + /*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false;*/ + mPayload[id].retransmits = 0; + mPayload[id].complete = false; + mPayload[id].dataAB[CH0] = true; //required for 1CH and 2CH devices + mPayload[id].dataAB[CH1] = true; //required for 1CH and 2CH devices + mPayload[id].dataAB[CH2] = true; //only required for 2CH devices + mPayload[id].stsAB[CH0] = true; //required for 1CH and 2CH devices + mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices + mPayload[id].stsAB[CH2] = true; //only required for 2CH devices + mPayload[id].txCmd = 0; + //mPayload[id].skipfirstrepeat = 0; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + mPayload[id].sts[0] = 0; + if (clrSts) { // only clear channel states at startup + mPayload[id].sts[CH1] = 0; + mPayload[id].sts[CH2] = 0; + mPayload[id].sts[CH3] = 0; + mPayload[id].sts[CH4] = 0; + mPayload[id].sts[5] = 0; //remember last summarized state + } + } + + + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + miPayload_t mPayload[MAX_NUM_INVERTERS]; + bool mSerialDebug; + + Inverter<> *mHighPrioIv; + alarmListenerType mCbMiAlarm; + payloadListenerType mCbMiPayload; +}; + +#endif /*__MI_PAYLOAD_H__*/ diff --git a/tools/esp8266/main.cpp b/src/main.cpp similarity index 75% rename from tools/esp8266/main.cpp rename to src/main.cpp index eb7a388b..af10abce 100644 --- a/tools/esp8266/main.cpp +++ b/src/main.cpp @@ -1,11 +1,11 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- -#include "dbg.h" +#include "utils/dbg.h" #include "app.h" -#include "config.h" +#include "config/config.h" app myApp; @@ -18,7 +18,7 @@ IRAM_ATTR void handleIntr(void) { //----------------------------------------------------------------------------- void setup() { - myApp.setup(WIFI_TRY_CONNECT_TIME); + myApp.setup(); // TODO: move to HmRadio attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING); diff --git a/src/platformio.ini b/src/platformio.ini new file mode 100644 index 00000000..cae35934 --- /dev/null +++ b/src/platformio.ini @@ -0,0 +1,146 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +src_dir = . +include_dir = . + +[env] +framework = arduino +board_build.filesystem = littlefs +upload_speed = 921600 + +;build_flags = +; ;;;;; Possible Debug options ;;;;;; +; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level + ;-DDEBUG_ESP_PORT=Serial + ;-DDEBUG_ESP_CORE + ;-DDEBUG_ESP_WIFI + ;-DDEBUG_ESP_HTTP_CLIENT + ;-DDEBUG_ESP_HTTP_SERVER + ;-DDEBUG_ESP_OOM + +monitor_speed = 115200 + +extra_scripts = + pre:../scripts/auto_firmware_version.py + pre:web/html/convert.py + +lib_deps = + https://github.com/yubox-node-org/ESPAsyncWebServer + nrf24/RF24 @ ^1.4.5 + paulstoffregen/Time @ ^1.6.1 + https://github.com/bertmelis/espMqttClient#v1.4.2 + bblanchon/ArduinoJson @ ^6.21.0 + https://github.com/JChristensen/Timezone @ ^1.2.4 + olikraus/U8g2 @ ^2.34.16 + zinggjm/GxEPD2 @ ^1.5.0 + + +[env:esp8266-release] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = -D RELEASE + ;-Wl,-Map,output.map +monitor_filters = + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp8266_exception_decoder + + +[env:esp8266-release-prometheus] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = -D RELEASE -DENABLE_PROMETHEUS_EP +monitor_filters = + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp8266_exception_decoder + +[env:esp8266-debug] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 +build_type = debug +monitor_filters = + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line + log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + +[env:esp8285-release] +platform = espressif8266 +board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld +board_build.f_cpu = 80000000L +build_flags = -D RELEASE +monitor_filters = + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + +[env:esp8285-debug] +platform = espressif8266 +board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld +board_build.f_cpu = 80000000L +build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial +build_type = debug +monitor_filters = + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line + log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + +[env:esp32-wroom32-release] +platform = espressif32 +board = lolin_d32 +build_flags = -D RELEASE -std=gnu++14 +build_unflags = -std=gnu++11 +monitor_filters = + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp32_exception_decoder + +[env:esp32-wroom32-release-prometheus] +platform = espressif32 +board = lolin_d32 +build_flags = -D RELEASE -std=gnu++14 -DENABLE_PROMETHEUS_EP +build_unflags = -std=gnu++11 +monitor_filters = + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp32_exception_decoder + +[env:esp32-wroom32-debug] +platform = espressif32 +board = lolin_d32 +build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14 +build_unflags = -std=gnu++11 +build_type = debug +monitor_filters = + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line + log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + +[env:opendtufusionv1-release] +platform = espressif32 +board = esp32-s3-devkitc-1 +build_flags = -D RELEASE -std=gnu++14 +build_unflags = -std=gnu++11 +monitor_filters = + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h new file mode 100644 index 00000000..1a0222b2 --- /dev/null +++ b/src/plugins/Display/Display.h @@ -0,0 +1,114 @@ +#ifndef __DISPLAY__ +#define __DISPLAY__ + +#include +#include + +#include "../../hm/hmSystem.h" +#include "../../utils/helper.h" +#include "Display_Mono.h" +#include "Display_ePaper.h" + +template +class Display { + public: + Display() {} + + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { + mCfg = cfg; + mSys = sys; + mUtcTs = utcTs; + mNewPayload = false; + mLoopCnt = 0; + mVersion = version; + + if (mCfg->type == 0) + return; + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); + mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + } else if (mCfg->type >= 10) { + #if defined(ESP32) + mRefreshCycle = 0; + mEpaper.config(mCfg->rot); + mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + #endif + } + } + + void payloadEventListener(uint8_t cmd) { + mNewPayload = true; + } + + void tickerSecond() { + mMono.loop(); + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { + mNewPayload = false; + mLoopCnt = 0; + DataScreen(); + } + } + + private: + void DataScreen() { + if (mCfg->type == 0) + return; + if (*mUtcTs == 0) + return; + + float totalPower = 0; + float totalYieldDay = 0; + float totalYieldTotal = 0; + + uint8_t isprod = 0; + + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + + if (iv->isProducing(*mUtcTs)) + isprod++; + + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); + } + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type >= 10) { + #if defined(ESP32) + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mRefreshCycle++; + #endif + } + + #if defined(ESP32) + if (mRefreshCycle > 480) { + mEpaper.fullRefresh(); + mRefreshCycle = 0; + } + #endif + } + + // private member variables + bool mNewPayload; + uint8_t mLoopCnt; + uint32_t *mUtcTs; + const char *mVersion; + display_t *mCfg; + HMSYSTEM *mSys; + uint16_t mRefreshCycle; + + #if defined(ESP32) + DisplayEPaper mEpaper; + #endif + DisplayMono mMono; +}; + +#endif /*__DISPLAY__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp new file mode 100644 index 00000000..d55b6061 --- /dev/null +++ b/src/plugins/Display/Display_Mono.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Display_Mono.h" + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif +#include "../../utils/helper.h" + +//#ifdef U8X8_HAVE_HW_SPI +//#include +//#endif +//#ifdef U8X8_HAVE_HW_I2C +//#include +//#endif + +DisplayMono::DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + _dispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; +} + + + +void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { + if ((0 < type) && (type < 4)) { + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + switch(type) { + case 1: + mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + default: + case 2: + mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + case 3: + mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); + break; + } + + mUtcTs = utcTs; + + mDisplay->begin(); + + mIsLarge = (mDisplay->getWidth() > 120); + calcLineHeights(); + + mDisplay->clearBuffer(); + if (3 != mType) + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + mDisplay->sendBuffer(); + } +} + +void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; +} + +void DisplayMono::loop(void) { + if (mEnPowerSafe) + if(mTimeout != 0) + mTimeout--; +} + +void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + + + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + if (totalPower > 999) { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + } else { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + } + printText(_fmtText, 0); + } else { + printText("offline", 0, 25); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(_fmtText, 1); + + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(_fmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(_mExtra % 10) && (ip)) { + printText(ip.toString().c_str(), 3); + } else if (!(_mExtra % 5)) { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(_fmtText, 3); + } else { + if(mIsLarge && (NULL != mUtcTs)) + printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + else + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + } + + mDisplay->sendBuffer(); + + _dispY = 0; + _mExtra++; +} + +void DisplayMono::calcLineHeights() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineOffsets[i] = yOff; + } +} + +inline void DisplayMono::setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); + break; + } +} + +void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) { + if (!mIsLarge) { + dispX = (line == 0) ? 10 : 5; + } + setFont(line); + + dispX += (mEnScreenSaver) ? (_mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineOffsets[line], text); +} diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h new file mode 100644 index 00000000..ad04c9f4 --- /dev/null +++ b/src/plugins/Display/Display_Mono.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#define DISP_DEFAULT_TIMEOUT 60 // in seconds +#define DISP_FMT_TEXT_LEN 32 + +class DisplayMono { + public: + DisplayMono(); + + void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version); + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum); + void loop(void); + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + private: + void calcLineHeights(); + void setFont(uint8_t line); + void printText(const char* text, uint8_t line, uint8_t dispX = 5); + + U8G2* mDisplay; + + uint8_t mType; + bool mEnPowerSafe, mEnScreenSaver; + uint8_t mLuminance; + + bool mIsLarge = false; + uint8_t mLoopCnt; + uint32_t* mUtcTs; + uint8_t mLineOffsets[5]; + + uint16_t _dispY; + + uint8_t _mExtra; + uint16_t mTimeout; + char _fmtText[DISP_FMT_TEXT_LEN]; +}; diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp new file mode 100644 index 00000000..99d35ed8 --- /dev/null +++ b/src/plugins/Display/Display_ePaper.cpp @@ -0,0 +1,197 @@ +#include "Display_ePaper.h" + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif +#include "../../utils/helper.h" +#include "imagedata.h" + +#if defined(ESP32) + +static const uint32_t spiClk = 4000000; // 4 MHz + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) +SPIClass hspi(HSPI); +#endif + +DisplayEPaper::DisplayEPaper() { + mDisplayRotation = 2; + mHeadFootPadding = 16; +} + + +//*************************************************************************** +void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) { + mUtcTs = utcTs; + + if (type > 9) { + Serial.begin(115200); + _display = new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); + hspi.begin(_SCK, _BUSY, _MOSI, _CS); + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) + _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); +#endif + _display->init(115200, true, 2, false); + _display->setRotation(mDisplayRotation); + _display->setFullWindow(); + + // Logo + _display->fillScreen(GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + while (_display->nextPage()) + ; + + // clean the screen + delay(2000); + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; + + headlineIP(); + + // call the PowerPage to change the PV Power Values + actualPowerPaged(0, 0, 0, 0); + } +} + +void DisplayEPaper::config(uint8_t rotation) { + mDisplayRotation = rotation; +} + +//*************************************************************************** +void DisplayEPaper::fullRefresh() { + // screen complete black + _display->fillScreen(GxEPD_BLACK); + while (_display->nextPage()) + ; + delay(2000); + // screen complete white + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; +} +//*************************************************************************** +void DisplayEPaper::headlineIP() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, 0, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + + do { + if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { + snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str()); + } else { + snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (mHeadFootPadding - 2)); + _display->println(_fmtText); + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::lastUpdatePaged() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + do { + if (NULL != mUtcTs) { + snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str()); + + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (_display->height() - 3)); + _display->println(_fmtText); + } + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { + int16_t tbx, tby; + uint16_t tbw, tbh, x, y; + + _display->setFont(&FreeSans24pt7b); + _display->setTextColor(GxEPD_BLACK); + + _display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); + _display->fillScreen(GxEPD_WHITE); + do { + if (_totalPower > 9999) { + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); + _changed = true; + } else if ((_totalPower > 0) && (_totalPower <= 9999)) { + snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); + _changed = true; + } else { + snprintf(_fmtText, sizeof(_fmtText), "offline"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, mHeadFootPadding + tbh + 10); + _display->print(_fmtText); + + _display->setFont(&FreeSans12pt7b); + y = _display->height() / 2; + _display->setCursor(5, y); + _display->print("today:"); + snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 38, y); + _display->println("Wh"); + + y = y + tbh + 7; + _display->setCursor(5, y); + _display->print("total:"); + snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 50, y); + _display->println("kWh"); + + _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); + _display->println(_fmtText); + + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + // check if the IP has changed + if (_settedIP != WiFi.localIP().toString().c_str()) { + // save the new IP and call the Headline Funktion to adapt the Headline + _settedIP = WiFi.localIP().toString().c_str(); + headlineIP(); + } + + // call the PowerPage to change the PV Power Values + actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); + + // if there was an change and the Inverter is producing set a new Timestam in the footline + if ((isprod > 0) && (_changed)) { + _changed = false; + lastUpdatePaged(); + } + + _display->powerOff(); +} +//*************************************************************************** +#endif // ESP32 diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h new file mode 100644 index 00000000..b2729f25 --- /dev/null +++ b/src/plugins/Display/Display_ePaper.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#if defined(ESP32) + +// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board +#define USE_HSPI_FOR_EPD + +/// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram +// #include +// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code +// enable GxEPD2_GFX base class +#define ENABLE_GxEPD2_GFX 1 + +#include +#include +#include + +#include +// FreeFonts from Adafruit_GFX +#include +#include +#include +#include + +// GDEW027C44 2.7 " b/w/r 176x264, IL91874 +// GDEH0154D67 1.54" b/w 200x200 + +class DisplayEPaper { + public: + DisplayEPaper(); + void fullRefresh(); + void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); + void config(uint8_t rotation); + void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + + private: + void headlineIP(); + void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); + void lastUpdatePaged(); + + uint8_t mDisplayRotation; + bool _changed = false; + char _fmtText[35]; + const char* _settedIP; + uint8_t mHeadFootPadding; + GxEPD2_GFX* _display; + uint32_t *mUtcTs; +}; + +#endif // ESP32 diff --git a/src/plugins/Display/imagedata.h b/src/plugins/Display/imagedata.h new file mode 100644 index 00000000..baaddec8 --- /dev/null +++ b/src/plugins/Display/imagedata.h @@ -0,0 +1,329 @@ +// GxEPD2_ESP32_ESP8266_WifiData_V1_und_V2 + +#ifndef __IMAGEDATA_H__ +#define __IMAGEDATA_H__ + +#if defined(__AVR__) || defined(ARDUINO_ARCH_SAMD) +#include +#elif defined(ESP8266) || defined(ESP32) +#include +#endif + +// 'Logo', 200x200px +const unsigned char logo[] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x06, + 0x0f, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7e, 0x0f, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x03, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x19, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x70, 0x7f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x3f, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x0f, 0xe0, 0x3f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe0, 0x1f, 0x83, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, + 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xc1, 0x07, 0x80, 0x07, 0xfe, 0xff, 0xff, 0xfc, 0x07, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe1, 0x07, 0xc0, 0x01, 0xe0, 0x0f, + 0xff, 0xfc, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xe1, 0x83, 0xc0, 0x01, 0xc0, 0x07, 0xff, 0xf8, 0x0f, 0xfc, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe1, 0x83, 0xc0, 0x00, + 0xc0, 0x07, 0x8f, 0xf8, 0x1f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x01, 0xc0, 0x00, 0x81, 0x83, 0x07, 0xf0, 0x3f, 0xf9, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01, + 0xe0, 0xe0, 0x87, 0xe3, 0x0f, 0xf0, 0x3f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xe0, 0x00, 0xe0, 0xe0, 0x87, 0xe1, 0x0c, 0x60, 0x7f, + 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, + 0xe0, 0x00, 0xe1, 0xf0, 0x87, 0xe1, 0x08, 0x60, 0x7f, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xe0, 0xe0, 0xe0, 0xe0, 0x87, 0xc2, 0x00, + 0x40, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x8f, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, 0x82, 0x00, 0xc0, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, + 0x06, 0x01, 0x81, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xe0, 0xe0, 0x20, 0xe0, 0xe0, 0x0c, 0x03, 0x81, 0xff, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x30, + 0xe1, 0xf8, 0x18, 0x07, 0xe1, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xf3, 0xfe, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xc0, + 0xfb, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x1f, 0xfc, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xfc, 0x0f, + 0xf8, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x33, 0xef, 0xff, 0xff, 0xff, 0xff, 0x81, 0xfc, 0x0f, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xa0, 0x00, 0x7f, 0xe3, + 0xfc, 0x0f, 0xf3, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf1, 0xf9, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x0f, 0xe7, 0xff, 0xe0, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf9, 0xff, 0x80, 0x3f, 0xff, + 0xe0, 0x0f, 0xfe, 0x1f, 0xc7, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xf8, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x8f, 0xff, 0xfc, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x70, 0x3f, + 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x1f, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x63, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x3f, + 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfe, + 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7e, 0x3f, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x0c, 0x7f, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x87, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x55, 0x00, 0x3f, 0xf8, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x01, 0xff, 0xff, 0xf8, 0x0f, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x9f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x03, + 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe3, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe7, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x7e, 0x06, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xcf, + 0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0x03, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x1f, 0x23, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xf3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xf8, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x0f, 0xff, 0x8f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0xf5, 0x90, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x03, 0xff, + 0x1f, 0xe3, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0xfe, 0x31, 0xfe, 0x7f, 0xe7, 0xff, 0x80, 0x00, 0x40, 0x00, + 0x07, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0x3c, + 0xf9, 0xfc, 0xff, 0xe7, 0xfe, 0x3f, 0xc9, 0xff, 0xf1, 0x1f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x3c, 0xf9, 0xf9, 0xff, 0xc7, 0xfc, 0xff, 0x90, + 0x7f, 0xf3, 0x03, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, + 0x3f, 0x39, 0xfd, 0xf3, 0xff, 0xcf, 0xfc, 0xff, 0x90, 0x3f, 0xf3, 0x83, 0xf8, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x39, 0xf9, 0xc7, 0xff, 0xcf, 0xfc, + 0xff, 0x32, 0x7f, 0xe4, 0x77, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xff, 0x7f, 0x33, 0xf9, 0x8f, 0xff, 0xcf, 0xf9, 0xff, 0x00, 0x7f, 0xe0, 0x67, 0xfc, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xb3, 0xf3, 0xbf, 0xff, + 0xcf, 0xf9, 0xff, 0x00, 0xff, 0xfe, 0x47, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xf7, 0xf3, 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe0, 0xff, 0xfc, 0x0f, + 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xe7, 0xe7, + 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe1, 0xff, 0xfc, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xe7, 0xef, 0xff, 0xc7, 0xf9, 0xff, 0xc3, 0xff, + 0xfc, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, + 0xef, 0xef, 0xc0, 0xff, 0xe7, 0xf9, 0xff, 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xcf, 0xf0, 0x01, 0xe7, 0xf1, 0xff, + 0x87, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, + 0xff, 0xbf, 0xcf, 0xe7, 0xff, 0xc1, 0xe3, 0xe1, 0xff, 0x8f, 0xff, 0xf0, 0xff, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x9f, 0xef, 0xe7, 0xff, 0xff, 0xf3, + 0xc1, 0xff, 0x96, 0xaf, 0xf9, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf9, 0xff, 0xff, 0x9f, 0xe7, 0xe3, 0xff, 0xff, 0xf1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, + 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xcf, 0xe7, 0xf3, 0xff, + 0xff, 0xf8, 0xc0, 0x00, 0x4a, 0x90, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf9, 0xff, 0xff, 0xef, 0xf3, 0xf3, 0x9f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe7, 0xf1, + 0xe7, 0xc7, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xf0, 0x07, 0xe3, 0xff, 0xff, 0x81, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, + 0xf8, 0x07, 0x1f, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xfc, 0x1f, 0x9f, 0xf8, 0xff, 0xff, 0xc3, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, + 0xff, 0xff, 0xf8, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf9, 0xff, 0x9f, 0xfe, 0x3f, + 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfd, 0xff, 0xff, 0xf1, 0xff, 0x9f, 0xff, 0x9f, 0xff, 0xf3, 0xff, 0x3f, 0x3f, 0xff, 0xff, + 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe1, 0xff, 0xcf, + 0xff, 0xc7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xe1, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, + 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xc1, + 0xff, 0xcf, 0xff, 0xf3, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x81, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, + 0xff, 0x91, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x11, 0xff, 0x9f, 0xff, 0xff, 0xff, + 0xf3, 0xff, 0x1f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x21, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x9f, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x20, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x60, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x64, 0x3f, + 0x1f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xe7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, + 0xff, 0x3f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, + 0xe7, 0x80, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xf8, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x9f, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xff, 0xfe, 0x7f, 0xff, 0xc3, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf3, 0xf3, 0xff, 0xfc, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xf8, 0xff, 0xff, + 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xf9, 0xe7, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf3, 0xf9, 0xff, 0xe1, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe7, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0x3f, 0x07, + 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf3, 0xe7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x00, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0xe0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, + 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf7, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x83, 0xe7, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x13, 0xe7, 0xff, 0xfc, 0x03, + 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0x31, 0xe7, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0xe3, 0xff, + 0xfc, 0x00, 0x1f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x31, 0xf3, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xc7, 0xff, 0xff, + 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x19, + 0xf3, 0xff, 0xfc, 0x00, 0x07, 0xff, 0x87, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xf3, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x83, 0xf3, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x07, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe3, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xff, 0xfe, + 0x01, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xf0, 0x00, 0x3f, 0x80, 0x07, 0xff, 0xff, 0xf0, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x4c, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xf0, 0x00, 0x00, 0x0b, 0x87, 0xff, + 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0e, 0x7f, 0xf8, 0x00, 0x3f, 0xff, 0xc7, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x7f, 0xfe, 0x00, 0xff, 0xff, + 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xf3, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xf3, 0xc0, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff, + 0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x17, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +#endif /*__IMAGEDATA_H__*/ diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h new file mode 100644 index 00000000..b008d8d2 --- /dev/null +++ b/src/publisher/pubMqtt.h @@ -0,0 +1,669 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +// https://bert.emelis.net/espMqttClient/ + +#ifndef __PUB_MQTT_H__ +#define __PUB_MQTT_H__ + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif + +#include "../utils/dbg.h" +#include "../config/config.h" +#include +#include +#include "../defines.h" +#include "../hm/hmSystem.h" + +#include "pubMqttDefs.h" + +#define QOS_0 0 + +typedef std::function subscriptionCb; + +struct alarm_t { + uint16_t code; + uint32_t start; + uint32_t end; + alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {} +}; + +typedef struct { + bool running; + uint8_t lastIvId; + uint8_t sub; + uint8_t foundIvCnt; +} discovery_t; + +template +class PubMqtt { + public: + PubMqtt() { + mRxCnt = 0; + mTxCnt = 0; + mSubscriptionCb = NULL; + memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS); + mLastAnyAvail = false; + } + + ~PubMqtt() { } + + void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) { + mCfgMqtt = cfg_mqtt; + mDevName = devName; + mVersion = version; + mSys = sys; + mUtcTimestamp = utcTs; + mIntervalTimeout = 1; + + mDiscovery.running = false; + + snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); + + if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0)) + mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); + snprintf(mClientId, 24, "%s-", mDevName); + uint8_t pos = strlen(mClientId); + mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(13, 14).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(15, 16).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(16, 17).c_str()[0]; + mClientId[pos++] = '\0'; + + mClient.setClientId(mClientId); + mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port); + mClient.setWill(mLwtTopic, QOS_0, true, mqttStr[MQTT_STR_LWT_NOT_CONN]); + mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1)); + mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1)); + mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + } + + void loop() { + #if defined(ESP8266) + mClient.loop(); + yield(); + #endif + + if(mDiscovery.running) + discoveryConfigLoop(); + } + + + void tickerSecond() { + if (mIntervalTimeout > 0) + mIntervalTimeout--; + + if(mClient.disconnected()) { + mClient.connect(); + return; // next try in a second + } + + if(0 == mCfgMqtt->interval) // no fixed interval, publish once new data were received (from inverter) + sendIvData(); + else { // send mqtt data in a fixed interval + if(mIntervalTimeout == 0) { + mIntervalTimeout = mCfgMqtt->interval; + mSendList.push(RealTimeRunData_Debug); + sendIvData(); + } + } + } + + void tickerMinute() { + snprintf(mVal, 40, "%ld", millis() / 1000); + publish(subtopics[MQTT_UPTIME], mVal); + publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); + publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); + #ifndef ESP32 + publish(subtopics[MQTT_HEAP_FRAG], String(ESP.getHeapFragmentation()).c_str()); + #endif + } + + bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { + if (!mClient.connected()) + return false; + + publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true); + publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); + publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); + publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); + publish(subtopics[MQTT_DIS_NIGHT_COMM], ((disNightCom) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + + return true; + } + + bool tickerComm(bool disabled) { + if (!mClient.connected()) + return false; + + publish(subtopics[MQTT_COMM_DISABLED], ((disabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + publish(subtopics[MQTT_COMM_DIS_TS], String(*mUtcTimestamp).c_str(), true); + + return true; + } + + void tickerMidnight() { + // set Total YieldDay to zero + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]); + snprintf(mVal, 2, "0"); + publish(mSubTopic, mVal, true); + } + + void payloadEventListener(uint8_t cmd) { + if(mClient.connected()) { // prevent overflow if MQTT broker is not reachable but set + if((0 == mCfgMqtt->interval) || (RealTimeRunData_Debug != cmd)) // no interval or no live data + mSendList.push(cmd); + } + } + + void alarmEventListener(uint16_t code, uint32_t start, uint32_t endTime) { + if(mClient.connected()) { + mAlarmList.push(alarm_t(code, start, endTime)); + } + } + + void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) { + if(!mClient.connected()) + return; + + if(addTopic){ + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic); + } else { + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic); + } + + do { + if(0 != mClient.publish(mTopic, QOS_0, retained, payload)) + break; + if(!mClient.connected()) + break; + #if defined(ESP8266) + mClient.loop(); + #endif + yield(); + } while(1); + + mTxCnt++; + } + + void subscribe(const char *subTopic) { + char topic[MQTT_TOPIC_LEN + 20]; + snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s/%s", mCfgMqtt->topic, subTopic); + mClient.subscribe(topic, QOS_0); + } + + void setSubscriptionCb(subscriptionCb cb) { + mSubscriptionCb = cb; + } + + inline bool isConnected() { + return mClient.connected(); + } + + inline uint32_t getTxCnt(void) { + return mTxCnt; + } + + inline uint32_t getRxCnt(void) { + return mRxCnt; + } + + void sendDiscoveryConfig(void) { + DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); + mDiscovery.running = true; + mDiscovery.lastIvId = 0; + mDiscovery.sub = 0; + mDiscovery.foundIvCnt = 0; + } + + void setPowerLimitAck(Inverter<> *iv) { + if (NULL != iv) { + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); + publish(mSubTopic, "true", true); + } + } + + private: + void onConnect(bool sessionPreset) { + DPRINTLN(DBG_INFO, F("MQTT connected")); + + publish(subtopics[MQTT_VERSION], mVersion, true); + publish(subtopics[MQTT_DEVICE], mDevName, true); + publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); + tickerMinute(); + publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); + + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + snprintf(mVal, 20, "ctrl/limit/%d", i); + subscribe(mVal); + snprintf(mVal, 20, "ctrl/restart/%d", i); + subscribe(mVal); + } + subscribe(subscr[MQTT_SUBS_SET_TIME]); + } + + void onDisconnect(espMqttClientTypes::DisconnectReason reason) { + DPRINT(DBG_INFO, F("MQTT disconnected, reason: ")); + switch (reason) { + case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED: + DBGPRINTLN(F("TCP disconnect")); + break; + case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + DBGPRINTLN(F("wrong protocol version")); + break; + case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED: + DBGPRINTLN(F("identifier rejected")); + break; + case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE: + DBGPRINTLN(F("broker unavailable")); + break; + case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS: + DBGPRINTLN(F("malformed credentials")); + break; + case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED: + DBGPRINTLN(F("not authorized")); + break; + default: + DBGPRINTLN(F("unknown")); + } + } + + void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { + if(len == 0) + return; + DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]); + DBGPRINTLN(String(topic)); + if(NULL == mSubscriptionCb) + return; + + DynamicJsonDocument json(128); + JsonObject root = json.to(); + + bool limitAbs = false; + if(len > 0) { + char *pyld = new char[len + 1]; + strncpy(pyld, (const char*)payload, len); + pyld[len] = '\0'; + root[F("val")] = atoi(pyld); + if(pyld[len-1] == 'W') + limitAbs = true; + delete[] pyld; + } + + const char *p = topic; + uint8_t pos = 0; + uint8_t elm = 0; + char tmp[30]; + + while(1) { + if(('/' == p[pos]) || ('\0' == p[pos])) { + strncpy(tmp, p, pos); + tmp[pos] = '\0'; + switch(elm++) { + case 1: root[F("path")] = String(tmp); break; + case 2: + if(strncmp("limit", tmp, 5) == 0) { + if(limitAbs) + root[F("cmd")] = F("limit_nonpersistent_absolute"); + else + root[F("cmd")] = F("limit_nonpersistent_relative"); + } + else + root[F("cmd")] = String(tmp); + break; + case 3: root[F("id")] = atoi(tmp); break; + default: break; + } + if('\0' == p[pos]) + break; + p = p + pos + 1; + pos = 0; + } + pos++; + } + + /*char out[128]; + serializeJson(root, out, 128); + DPRINTLN(DBG_INFO, "json: " + String(out));*/ + (mSubscriptionCb)(root); + + mRxCnt++; + } + + void discoveryConfigLoop(void) { + char topic[64], name[32], uniq_id[32], buf[350]; + DynamicJsonDocument doc(256); + + uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC}; + const char* unitTotal[4] = {"W", "kWh", "Wh", "W"}; + + String node_id = String(mDevName) + "_TOTAL"; + bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS); + + Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId); + record_t<> *rec = NULL; + if (NULL != iv) { + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if(0 == mDiscovery.sub) + mDiscovery.foundIvCnt++; + } + + if ((NULL != iv) || total) { + if (!total) { + doc[F("name")] = iv->config->name; + doc[F("ids")] = String(iv->config->serial.u64, HEX); + doc[F("mdl")] = iv->config->name; + } + else { + doc[F("name")] = node_id; + doc[F("ids")] = node_id; + doc[F("mdl")] = node_id; + } + + doc[F("cu")] = F("http://") + String(WiFi.localIP().toString()); + doc[F("mf")] = F("Hoymiles"); + JsonObject deviceObj = doc.as(); // deviceObj is only pointer!? + + const char *devCls, *stateCls; + if (!total) { + if (rec->assign[mDiscovery.sub].ch == CH0) + snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(mDiscovery.sub, rec)); + else + snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + snprintf(topic, 64, "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + snprintf(uniq_id, 32, "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + + devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId); + stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId); + } + + else { // total values + snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]); + snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]); + snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]); + devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]); + stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]); + } + + DynamicJsonDocument doc2(512); + doc2[F("name")] = name; + doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic); + doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub])); + doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id; + doc2[F("dev")] = deviceObj; + if (!(String(stateCls) == String("total_increasing"))) + doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!? + if (devCls != NULL) + doc2[F("dev_cla")] = String(devCls); + if (stateCls != NULL) + doc2[F("stat_cla")] = String(stateCls); + + if (!total) + snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + else // total values + snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]); + size_t size = measureJson(doc2) + 1; + memset(buf, 0, size); + serializeJson(doc2, buf, size); + publish(topic, buf, true, false); + + if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { + mDiscovery.sub = 0; + checkDiscoveryEnd(); + } + } else { + mDiscovery.sub = 0; + checkDiscoveryEnd(); + } + + yield(); + } + + void checkDiscoveryEnd(void) { + if(++mDiscovery.lastIvId == MAX_NUM_INVERTERS) { + // check if only one inverter was found, then don't create 'total' sensor + if(mDiscovery.foundIvCnt == 1) + mDiscovery.running = false; + } else if(mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1)) + mDiscovery.running = false; + } + + const char *getFieldDeviceClass(uint8_t fieldId) { + uint8_t pos = 0; + for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { + if (deviceFieldAssignment[pos].fieldId == fieldId) + break; + } + return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : deviceClasses[deviceFieldAssignment[pos].deviceClsId]; + } + + const char *getFieldStateClass(uint8_t fieldId) { + uint8_t pos = 0; + for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { + if (deviceFieldAssignment[pos].fieldId == fieldId) + break; + } + return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; + } + + bool processIvStatus() { + // returns true if any inverter is available + bool allAvail = true; // shows if all enabled inverters are available + bool anyAvail = false; // shows if at least one enabled inverter is available + bool changed = false; + Inverter<> *iv; + record_t<> *rec; + + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + if (!iv->config->enabled) + continue; // skip to next inverter + + rec = iv->getRecordStruct(RealTimeRunData_Debug); + + // inverter status + uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; + if (iv->isAvailable(*mUtcTimestamp)) { + anyAvail = true; + status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD; + } + else // inverter is enabled but not available + allAvail = false; + + if(mLastIvState[id] != status) { + // if status changed from producing to not producing send last data immediately + if (MQTT_STATUS_AVAIL_PROD == mLastIvState[id]) + sendData(iv, RealTimeRunData_Debug); + + mLastIvState[id] = status; + changed = true; + + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); + snprintf(mVal, 40, "%d", status); + publish(mSubTopic, mVal, true); + + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); + snprintf(mVal, 40, "%d", iv->getLastTs(rec)); + publish(mSubTopic, mVal, true); + } + } + + if(changed) { + snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); + publish("status", mVal, true); + } + + return anyAvail; + } + + void sendAlarmData() { + if(mAlarmList.empty()) + return; + Inverter<> *iv = mSys->getInverterByPos(0, false); + while(!mAlarmList.empty()) { + alarm_t alarm = mAlarmList.front(); + publish(subtopics[MQTT_ALARM], iv->getAlarmStr(alarm.code).c_str()); + publish(subtopics[MQTT_ALARM_START], String(alarm.start).c_str()); + publish(subtopics[MQTT_ALARM_END], String(alarm.end).c_str()); + mAlarmList.pop(); + } + } + + void sendData(Inverter<> *iv, uint8_t curInfoCmd) { + record_t<> *rec = iv->getRecordStruct(curInfoCmd); + + if (iv->getLastTs(rec) > 0) { + for (uint8_t i = 0; i < rec->length; i++) { + bool retained = false; + if (curInfoCmd == RealTimeRunData_Debug) { + switch (rec->assign[i].fieldId) { + case FLD_YT: + case FLD_YD: + if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart + continue; + retained = true; + break; + } + } + + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); + snprintf(mVal, 40, "%g", ah::round3(iv->getValue(i, rec))); + publish(mSubTopic, mVal, retained); + + yield(); + } + } + } + + void sendIvData() { + bool anyAvail = processIvStatus(); + if (mLastAnyAvail != anyAvail) + mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated + + if(mSendList.empty()) + return; + + float total[4]; + bool RTRDataHasBeenSent = false; + + while(!mSendList.empty()) { + memset(total, 0, sizeof(float) * 4); + uint8_t curInfoCmd = mSendList.front(); + + if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once + bool sendTotals = (curInfoCmd == RealTimeRunData_Debug); + + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + if (!iv->config->enabled) + continue; // skip to next inverter + + // send RTR Data only if status is available + if ((curInfoCmd != RealTimeRunData_Debug) || (MQTT_STATUS_NOT_AVAIL_NOT_PROD != mLastIvState[id])) + sendData(iv, curInfoCmd); + + // calculate total values for RealTimeRunData_Debug + if (sendTotals) { + record_t<> *rec = iv->getRecordStruct(curInfoCmd); + + sendTotals &= (iv->getLastTs(rec) > 0); + if (sendTotals) { + for (uint8_t i = 0; i < rec->length; i++) { + if (CH0 == rec->assign[i].ch) { + switch (rec->assign[i].fieldId) { + case FLD_PAC: + total[0] += iv->getValue(i, rec); + break; + case FLD_YT: + total[1] += iv->getValue(i, rec); + break; + case FLD_YD: + total[2] += iv->getValue(i, rec); + break; + case FLD_PDC: + total[3] += iv->getValue(i, rec); + break; + } + } + } + } + } + yield(); + } + + if (sendTotals) { + uint8_t fieldId; + for (uint8_t i = 0; i < 4; i++) { + bool retained = true; + switch (i) { + default: + case 0: + fieldId = FLD_PAC; + retained = false; + break; + case 1: + fieldId = FLD_YT; + break; + case 2: + fieldId = FLD_YD; + break; + case 3: + fieldId = FLD_PDC; + retained = false; + break; + } + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); + snprintf(mVal, 40, "%g", ah::round3(total[i])); + publish(mSubTopic, mVal, retained); + } + RTRDataHasBeenSent = true; + yield(); + } + } + + mSendList.pop(); // remove from list once all inverters were processed + } + + mLastAnyAvail = anyAvail; + } + + espMqttClient mClient; + cfgMqtt_t *mCfgMqtt; + #if defined(ESP8266) + WiFiEventHandler mHWifiCon, mHWifiDiscon; + #endif + + HMSYSTEM *mSys; + uint32_t *mUtcTimestamp; + uint32_t mRxCnt, mTxCnt; + std::queue mSendList; + std::queue mAlarmList; + subscriptionCb mSubscriptionCb; + bool mLastAnyAvail; + uint8_t mLastIvState[MAX_NUM_INVERTERS]; + uint16_t mIntervalTimeout; + + // last will topic and payload must be available trough lifetime of 'espMqttClient' + char mLwtTopic[MQTT_TOPIC_LEN+5]; + const char *mDevName, *mVersion; + char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT + // global buffer for mqtt topic. Used when publishing mqtt messages. + char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; + char mSubTopic[32 + MAX_NAME_LENGTH + 1]; + char mVal[40]; + discovery_t mDiscovery; +}; + +#endif /*__PUB_MQTT_H__*/ diff --git a/src/publisher/pubMqttDefs.h b/src/publisher/pubMqttDefs.h new file mode 100644 index 00000000..088023b7 --- /dev/null +++ b/src/publisher/pubMqttDefs.h @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __PUB_MQTT_DEFS_H__ +#define __PUB_MQTT_DEFS_H__ + +#include + +enum { + STR_TRUE, + STR_FALSE +}; + +const char* const dict[] PROGMEM = { + "true", + "false" +}; + +enum { + MQTT_STR_LWT_CONN, + MQTT_STR_LWT_NOT_CONN, + MQTT_STR_AVAILABLE, + MQTT_STR_LAST_SUCCESS, + MQTT_STR_TOTAL, + MQTT_STR_GOT_TOPIC +}; + +const char* const mqttStr[] PROGMEM = { + "connected", + "not connected", + "available", + "last_success", + "total", + "MQTT got topic: " +}; + + +enum { + MQTT_UPTIME = 0, + MQTT_RSSI, + MQTT_FREE_HEAP, + MQTT_HEAP_FRAG, + MQTT_SUNRISE, + MQTT_SUNSET, + MQTT_COMM_START, + MQTT_COMM_STOP, + MQTT_DIS_NIGHT_COMM, + MQTT_COMM_DISABLED, + MQTT_COMM_DIS_TS, + MQTT_VERSION, + MQTT_DEVICE, + MQTT_IP_ADDR, + MQTT_STATUS, + MQTT_ALARM, + MQTT_ALARM_START, + MQTT_ALARM_END, + MQTT_LWT_ONLINE, + MQTT_LWT_OFFLINE, + MQTT_ACK_PWR_LMT +}; + +const char* const subtopics[] PROGMEM = { + "uptime", + "wifi_rssi", + "free_heap", + "heap_frag", + "sunrise", + "sunset", + "comm_start", + "comm_stop", + "dis_night_comm", + "comm_disabled", + "comm_dis_ts", + "version", + "device", + "ip_addr", + "status", + "alarm", + "alarm_start", + "alarm_end", + "connected", + "not_connected", + "ack_pwr_limit" +}; + +enum { + MQTT_SUBS_SET_TIME +}; + +const char* const subscr[] PROGMEM = { + "setup/set_time" +}; + +#endif /*__PUB_MQTT_DEFS_H__*/ diff --git a/src/publisher/pubSerial.h b/src/publisher/pubSerial.h new file mode 100644 index 00000000..522a227d --- /dev/null +++ b/src/publisher/pubSerial.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __PUB_SERIAL_H__ +#define __PUB_SERIAL_H__ + +#include "../utils/dbg.h" +#include "../config/settings.h" +#include "../hm/hmSystem.h" + +template +class PubSerial { + public: + PubSerial() {} + + void setup(settings_t *cfg, HMSYSTEM *sys, uint32_t *utcTs) { + mCfg = cfg; + mSys = sys; + mUtcTimestamp = utcTs; + } + + void tick(void) { + if (mCfg->serial.showIv) { + char topic[32 + MAX_NAME_LENGTH], val[40]; + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv->isAvailable(*mUtcTimestamp)) { + DPRINTLN(DBG_INFO, "Iv: " + String(id)); + for (uint8_t i = 0; i < rec->length; i++) { + if (0.0f != iv->getValue(i, rec)) { + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); + snprintf(val, 40, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec)); + DPRINTLN(DBG_INFO, String(topic) + ": " + String(val)); + } + yield(); + } + DPRINTLN(DBG_INFO, ""); + } + } + } + } + } + + private: + settings_t *mCfg; + HMSYSTEM *mSys; + uint32_t *mUtcTimestamp; +}; + + +#endif /*__PUB_SERIAL_H__*/ diff --git a/tools/esp8266/crc.cpp b/src/utils/crc.cpp similarity index 100% rename from tools/esp8266/crc.cpp rename to src/utils/crc.cpp diff --git a/tools/esp8266/crc.h b/src/utils/crc.h similarity index 100% rename from tools/esp8266/crc.h rename to src/utils/crc.h diff --git a/tools/esp8266/dbg.cpp b/src/utils/dbg.cpp similarity index 100% rename from tools/esp8266/dbg.cpp rename to src/utils/dbg.cpp diff --git a/tools/esp8266/include/dbg.h b/src/utils/dbg.h similarity index 72% rename from tools/esp8266/include/dbg.h rename to src/utils/dbg.h index 17b334c9..4716d7ae 100644 --- a/tools/esp8266/include/dbg.h +++ b/src/utils/dbg.h @@ -1,11 +1,11 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- #ifndef __DBG_H__ #define __DBG_H__ -#if defined(ESP32) && defined(F) +#if defined(F) && defined(ESP32) #undef F #define F(sl) (sl) #endif @@ -19,6 +19,8 @@ #define DBG_DEBUG 4 #define DBG_VERBOSE 5 +//#define LOG_MAX_MSG_LEN 100 + //----------------------------------------------------------------------------- // globally used level @@ -58,7 +60,12 @@ mCb(String(b, HEX)); } } - inline void DHEX(uint16_t b) { + + inline void DBGHEXLN(uint8_t b) { + DHEX(b); + DBGPRINT(F("\r\n")); + } + /*inline void DHEX(uint16_t b) { if( b<0x10 ) DSERIAL.print(F("000")); else if( b<0x100 ) DSERIAL.print(F("00")); else if( b<0x1000 ) DSERIAL.print(F("0")); @@ -89,7 +96,7 @@ else if( b<0x10000000 ) mCb(F("0")); mCb(String(b, HEX)); } - } + }*/ #endif #endif @@ -144,6 +151,10 @@ }\ }) +#define DPRINT_IVID(level, id) ({\ + DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\ +}) + #define DPRINTLN(level, str) ({\ switch(level) {\ case DBG_ERROR: PERRLN(str); break; \ @@ -154,4 +165,53 @@ }\ }) + +/*class ahoyLog { + public: + ahoyLog() {} + + inline void logMsg(uint8_t lvl, bool newLine, const char *fmt, va_list args) { + snprintf(mLogBuf, LOG_MAX_MSG_LEN, fmt, args); + DSERIAL.print(mLogBuf); + if(NULL != mCb) + mCb(mLogBuf); + if(newLine) { + DSERIAL.print(F("\r\n")); + if(NULL != mCb) + mCb(F("\r\n")); + } + } + + inline void logError(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_ERROR + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + inline void logWarn(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_WARN + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + inline void logInfo(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_INFO + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + private: + char mLogBuf[LOG_MAX_MSG_LEN]; + DBG_CB mCb = NULL; +};*/ + #endif /*__DBG_H__*/ diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp new file mode 100644 index 00000000..97d7418b --- /dev/null +++ b/src/utils/helper.cpp @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#include "helper.h" + +namespace ah { + void ip2Arr(uint8_t ip[], const char *ipStr) { + uint8_t p = 1; + memset(ip, 0, 4); + for(uint8_t i = 0; i < 16; i++) { + if(ipStr[i] == 0) + return; + if(0 == i) + ip[0] = atoi(ipStr); + else if(ipStr[i] == '.') + ip[p++] = atoi(&ipStr[i+1]); + } + } + + // note: char *str needs to be at least 16 bytes long + void ip2Char(uint8_t ip[], char *str) { + if(0 == ip[0]) + str[0] = '\0'; + else + snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } + + double round3(double value) { + return (int)(value * 1000 + 0.5) / 1000.0; + } + + String getDateTimeStr(time_t t) { + char str[20]; + if(0 == t) + sprintf(str, "n/a"); + else + sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); + return String(str); + } + + String getTimeStr(time_t t) { + char str[9]; + if(0 == t) + sprintf(str, "n/a"); + else + sprintf(str, "%02d:%02d:%02d", hour(t), minute(t), second(t)); + return String(str); + } + + uint64_t Serial2u64(const char *val) { + char tmp[3]; + uint64_t ret = 0ULL; + uint64_t u64; + memset(tmp, 0, 3); + for(uint8_t i = 0; i < 6; i++) { + tmp[0] = val[i*2]; + tmp[1] = val[i*2 + 1]; + if((tmp[0] == '\0') || (tmp[1] == '\0')) + break; + u64 = strtol(tmp, NULL, 16); + ret |= (u64 << ((5-i) << 3)); + } + return ret; + } +} diff --git a/src/utils/helper.h b/src/utils/helper.h new file mode 100644 index 00000000..efd056d2 --- /dev/null +++ b/src/utils/helper.h @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HELPER_H__ +#define __HELPER_H__ + +#include +#include +#include +#include +#include +#include + +static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time +static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time +static Timezone gTimezone(CEST, CET); + + +#define CHECK_MASK(a,b) ((a & b) == b) + +namespace ah { + void ip2Arr(uint8_t ip[], const char *ipStr); + void ip2Char(uint8_t ip[], char *str); + double round3(double value); + String getDateTimeStr(time_t t); + String getTimeStr(time_t t); + uint64_t Serial2u64(const char *val); +} + +#endif /*__HELPER_H__*/ diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h new file mode 100644 index 00000000..ca250a3e --- /dev/null +++ b/src/utils/scheduler.h @@ -0,0 +1,171 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Lukas Pusch, lukas@lpusch.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __SCHEDULER_H__ +#define __SCHEDULER_H__ + +#include +#include "dbg.h" + +namespace ah { + typedef std::function scdCb; + + enum {SCD_SEC = 1, SCD_MIN = 60, SCD_HOUR = 3600, SCD_12H = 43200, SCD_DAY = 86400}; + + struct sP { + scdCb c; + uint32_t timeout; + uint32_t reload; + bool isTimestamp; + char name[6]; + sP() : c(NULL), timeout(0), reload(0), isTimestamp(false), name("\n") {} + sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its), name("\n") {} + }; + + #define MAX_NUM_TICKER 30 + + class Scheduler { + public: + Scheduler() {} + + void setup() { + mUptime = 0; + mTimestamp = 0; + mMax = 0; + mPrevMillis = millis(); + resetTicker(); + } + + void loop(void) { + mMillis = millis(); + mDiff = mMillis - mPrevMillis; + if (mDiff < 1000) + return; + + mDiffSeconds = 1; + if (mDiff < 2000) + mPrevMillis += 1000; + else { + if (mMillis < mPrevMillis) { // overflow + mDiff = mMillis; + if (mDiff < 1000) + return; + } + mDiffSeconds = mDiff / 1000; + mPrevMillis += (mDiffSeconds * 1000); + } + + mUptime += mDiffSeconds; + if(0 != mTimestamp) + mTimestamp += mDiffSeconds; + checkTicker(); + + } + + void once(scdCb c, uint32_t timeout, const char *name) { addTicker(c, timeout, 0, false, name); } + void onceAt(scdCb c, uint32_t timestamp, const char *name) { addTicker(c, timestamp, 0, true, name); } + uint8_t every(scdCb c, uint32_t interval, const char *name){ return addTicker(c, interval, interval, false, name); } + + void everySec(scdCb c, const char *name) { every(c, SCD_SEC, name); } + void everyMin(scdCb c, const char *name) { every(c, SCD_MIN, name); } + void everyHour(scdCb c, const char *name) { every(c, SCD_HOUR, name); } + void every12h(scdCb c, const char *name) { every(c, SCD_12H, name); } + void everyDay(scdCb c, const char *name) { every(c, SCD_DAY, name); } + + virtual void setTimestamp(uint32_t ts) { + mTimestamp = ts; + } + + bool resetEveryById(uint8_t id) { + if (mTickerInUse[id] == false) + return false; + mTicker[id].timeout = mTicker[id].reload; + return true; + } + + uint32_t getUptime(void) { + return mUptime; + } + + uint32_t getTimestamp(void) { + return mTimestamp; + } + + inline void resetTicker(void) { + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) + mTickerInUse[i] = false; + } + + void getStat(uint8_t *max) { + *max = mMax; + } + + void printSchedulers() { + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { + if (mTickerInUse[i]) { + DPRINT(DBG_INFO, String(mTicker[i].name)); + DBGPRINT(", tmt: "); + DBGPRINT(String(mTicker[i].timeout)); + DBGPRINT(", rel: "); + DBGPRINTLN(String(mTicker[i].reload)); + } + } + } + + protected: + uint32_t mTimestamp; + + private: + inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { + if (!mTickerInUse[i]) { + mTickerInUse[i] = true; + mTicker[i].c = c; + mTicker[i].timeout = timeout; + mTicker[i].reload = reload; + mTicker[i].isTimestamp = isTimestamp; + memset(mTicker[i].name, 0, 6); + strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); + if(mMax == i) + mMax = i + 1; + return i; + } + } + return 0xff; + } + + inline void checkTicker(void) { + bool inUse[MAX_NUM_TICKER]; + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) + inUse[i] = mTickerInUse[i]; + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { + if (inUse[i]) { + if (mTicker[i].timeout <= ((mTicker[i].isTimestamp) ? mTimestamp : mDiffSeconds)) { // expired + if(0 == mTicker[i].reload) + mTickerInUse[i] = false; + else + mTicker[i].timeout = mTicker[i].reload; + //DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout)); + (mTicker[i].c)(); + yield(); + } + else // not expired + if (!mTicker[i].isTimestamp) + mTicker[i].timeout -= mDiffSeconds; + } + } + } + + sP mTicker[MAX_NUM_TICKER]; + bool mTickerInUse[MAX_NUM_TICKER]; + uint32_t mMillis, mPrevMillis, mDiff; + uint32_t mUptime; + uint8_t mDiffSeconds; + uint8_t mMax; + }; +} + +#endif /*__SCHEDULER_H__*/ diff --git a/src/utils/sun.h b/src/utils/sun.h new file mode 100644 index 00000000..c66149c2 --- /dev/null +++ b/src/utils/sun.h @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __SUN_H__ +#define __SUN_H__ + +namespace ah { + void calculateSunriseSunset(uint32_t utcTs, uint32_t offset, float lat, float lon, uint32_t *sunrise, uint32_t *sunset) { + // Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth + + // Julian day since 1.1.2000 12:00 + double n_JulianDay = (utcTs + offset) / 86400 - 10957.0; + // Mean solar time + double J = n_JulianDay - lon / 360; + // Solar mean anomaly + double M = fmod((357.5291 + 0.98560028 * J), 360); + // Equation of the center + double C = 1.9148 * SIN(M) + 0.02 * SIN(2 * M) + 0.0003 * SIN(3 * M); + // Ecliptic longitude + double lambda = fmod((M + C + 180 + 102.9372), 360); + // Solar transit + double Jtransit = 2451545.0 + J + 0.0053 * SIN(M) - 0.0069 * SIN(2 * lambda); + // Declination of the sun + double delta = ASIN(SIN(lambda) * SIN(23.44)); + // Hour angle + double omega = ACOS((SIN(-0.83) - SIN(lat) * SIN(delta)) / (COS(lat) * COS(delta))); + // Calculate sunrise and sunset + double Jrise = Jtransit - omega / 360; + double Jset = Jtransit + omega / 360; + // Julian sunrise/sunset to UTC unix timestamp (days incl. fraction to seconds + unix offset 1.1.2000 12:00) + *sunrise = (Jrise - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line + *sunset = (Jset - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line + } +} + +#endif /*__SUN_H__*/ diff --git a/src/web/RestApi.h b/src/web/RestApi.h new file mode 100644 index 00000000..b224f239 --- /dev/null +++ b/src/web/RestApi.h @@ -0,0 +1,617 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __WEB_API_H__ +#define __WEB_API_H__ + +#include "../utils/dbg.h" +#ifdef ESP32 +#include "AsyncTCP.h" +#else +#include "ESPAsyncTCP.h" +#endif +#include "../appInterface.h" +#include "../hm/hmSystem.h" +#include "../utils/helper.h" +#include "AsyncJson.h" +#include "ESPAsyncWebServer.h" + +#if defined(F) && defined(ESP32) +#undef F +#define F(sl) (sl) +#endif + +const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; +const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR}; + +template +class RestApi { + public: + RestApi() { + mTimezoneOffset = 0; + mHeapFree = 0; + mHeapFreeBlk = 0; + mHeapFrag = 0; + nr = 0; + } + + void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { + mApp = app; + mSrv = srv; + mSys = sys; + mConfig = config; + mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); + mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( + std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + + mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); + } + + uint32_t getTimezoneOffset(void) { + return mTimezoneOffset; + } + + void ctrlRequest(JsonObject obj) { + /*char out[128]; + serializeJson(obj, out, 128); + DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ + DynamicJsonDocument json(128); + JsonObject dummy = json.as(); + if(obj[F("path")] == "ctrl") + setCtrl(obj, dummy); + else if(obj[F("path")] == "setup") + setSetup(obj, dummy); + } + + private: + void onApi(AsyncWebServerRequest *request) { + mHeapFree = ESP.getFreeHeap(); + #ifndef ESP32 + mHeapFreeBlk = ESP.getMaxFreeBlockSize(); + mHeapFrag = ESP.getHeapFragmentation(); + #endif + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); + JsonObject root = response->getRoot(); + + String path = request->url().substring(5); + if(path == "html/system") getHtmlSystem(root); + else if(path == "html/logout") getHtmlLogout(root); + else if(path == "html/reboot") getHtmlReboot(root); + else if(path == "html/save") getHtmlSave(root); + else if(path == "system") getSysInfo(root); + else if(path == "generic") getGeneric(root); + else if(path == "reboot") getReboot(root); + else if(path == "statistics") getStatistics(root); + else if(path == "inverter/list") getInverterList(root); + else if(path == "index") getIndex(root); + else if(path == "setup") getSetup(root); + else if(path == "setup/networks") getNetworks(root); + else if(path == "live") getLive(root); + else if(path == "record/info") getRecord(root, InverterDevInform_All); + else if(path == "record/alarm") getRecord(root, AlarmData); + else if(path == "record/config") getRecord(root, SystemConfigPara); + else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); + else { + if(path.substring(0, 12) == "inverter/id/") + getInverter(root, request->url().substring(17).toInt()); + else + getNotFound(root, F("http://") + request->host() + F("/api/")); + } + + //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); + response->addHeader("Access-Control-Allow-Origin", "*"); + response->addHeader("Access-Control-Allow-Headers", "content-type"); + response->setLength(); + request->send(response); + } + + void onApiPost(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, "onApiPost"); + } + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + DPRINTLN(DBG_VERBOSE, "onApiPostBody"); + DynamicJsonDocument json(200); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); + JsonObject root = response->getRoot(); + + DeserializationError err = deserializeJson(json, (const char *)data, len); + JsonObject obj = json.as(); + root[F("success")] = (err) ? false : true; + if(!err) { + String path = request->url().substring(5); + if(path == "ctrl") + root[F("success")] = setCtrl(obj, root); + else if(path == "setup") + root[F("success")] = setSetup(obj, root); + else { + root[F("success")] = false; + root[F("error")] = "Path not found: " + path; + } + } + else { + switch (err.code()) { + case DeserializationError::Ok: break; + case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; + case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; + default: root[F("error")] = F("Deserialization failed"); break; + } + } + + response->setLength(); + request->send(response); + } + + void getNotFound(JsonObject obj, String url) { + JsonObject ep = obj.createNestedObject("avail_endpoints"); + ep[F("system")] = url + F("system"); + ep[F("statistics")] = url + F("statistics"); + ep[F("inverter/list")] = url + F("inverter/list"); + ep[F("index")] = url + F("index"); + ep[F("setup")] = url + F("setup"); + ep[F("live")] = url + F("live"); + ep[F("record/info")] = url + F("record/info"); + ep[F("record/alarm")] = url + F("record/alarm"); + ep[F("record/config")] = url + F("record/config"); + ep[F("record/live")] = url + F("record/live"); + } + + + void onDwnldSetup(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + + File fp = LittleFS.open("/settings.json", "r"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("failed to load settings")); + response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); + } + else { + String tmp = fp.readString(); + int i = 0; + // remove all passwords + while (i != -1) { + i = tmp.indexOf("\"pwd\":", i); + if(-1 != i) { + i+=7; + tmp.remove(i, tmp.indexOf("\"", i)-i); + } + } + response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); + } + + response->addHeader("Content-Type", "application/octet-stream"); + response->addHeader("Content-Description", "File Transfer"); + response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); + request->send(response); + fp.close(); + } + + void getGeneric(JsonObject obj) { + obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); + obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("menu_prot")] = mApp->getProtection(); + obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); + obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); + + #if defined(ESP32) + obj[F("esp_type")] = F("ESP32"); + #else + obj[F("esp_type")] = F("ESP8266"); + #endif + } + + void getSysInfo(JsonObject obj) { + obj[F("ssid")] = mConfig->sys.stationSsid; + obj[F("device_name")] = mConfig->sys.deviceName; + obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; + + obj[F("mac")] = WiFi.macAddress(); + obj[F("hostname")] = mConfig->sys.deviceName; + obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); + obj[F("prot_mask")] = mConfig->sys.protectionMask; + + obj[F("sdk")] = ESP.getSdkVersion(); + obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); + obj[F("heap_free")] = mHeapFree; + obj[F("sketch_total")] = ESP.getFreeSketchSpace(); + obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb + getGeneric(obj); + + getRadio(obj.createNestedObject(F("radio"))); + getStatistics(obj.createNestedObject(F("statistics"))); + + #if defined(ESP32) + obj[F("heap_total")] = ESP.getHeapSize(); + obj[F("chip_revision")] = ESP.getChipRevision(); + obj[F("chip_model")] = ESP.getChipModel(); + obj[F("chip_cores")] = ESP.getChipCores(); + //obj[F("core_version")] = F("n/a"); + //obj[F("flash_size")] = F("n/a"); + //obj[F("heap_frag")] = F("n/a"); + //obj[F("max_free_blk")] = F("n/a"); + //obj[F("reboot_reason")] = F("n/a"); + #else + //obj[F("heap_total")] = F("n/a"); + //obj[F("chip_revision")] = F("n/a"); + //obj[F("chip_model")] = F("n/a"); + //obj[F("chip_cores")] = F("n/a"); + obj[F("core_version")] = ESP.getCoreVersion(); + obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb + obj[F("heap_frag")] = mHeapFrag; + obj[F("max_free_blk")] = mHeapFreeBlk; + obj[F("reboot_reason")] = ESP.getResetReason(); + #endif + //obj[F("littlefs_total")] = LittleFS.totalBytes(); + //obj[F("littlefs_used")] = LittleFS.usedBytes(); + + uint8_t max; + mApp->getSchedulerInfo(&max); + obj[F("schMax")] = max; + } + + void getHtmlSystem(JsonObject obj) { + getSysInfo(obj.createNestedObject(F("system"))); + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("html")] = F("Factory Reset

Reboot"); + } + + void getHtmlLogout(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 3; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("succesfully logged out"); + } + + void getHtmlReboot(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 20; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("rebooting ..."); + } + + void getHtmlSave(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj["pending"] = (bool)mApp->getSavePending(); + obj["success"] = (bool)mApp->getLastSaveSucceed(); + } + + void getReboot(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 10; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("reboot. Autoreload after 10 seconds"); + } + + void getStatistics(JsonObject obj) { + statistics_t *stat = mApp->getStatistics(); + obj[F("rx_success")] = stat->rxSuccess; + obj[F("rx_fail")] = stat->rxFail; + obj[F("rx_fail_answer")] = stat->rxFailNoAnser; + obj[F("frame_cnt")] = stat->frmCnt; + obj[F("tx_cnt")] = mSys->Radio.mSendCnt; + obj[F("retransmits")] = mSys->Radio.mRetransmits; + } + + void getInverterList(JsonObject obj) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("id")] = i; + obj2[F("name")] = String(iv->config->name); + obj2[F("serial")] = String(iv->config->serial.u64, HEX); + obj2[F("channels")] = iv->channels; + obj2[F("version")] = String(iv->getFwVersion()); + + for(uint8_t j = 0; j < iv->channels; j ++) { + obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; + obj2[F("ch_name")][j] = iv->config->chName[j]; + obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; + } + } + } + obj[F("interval")] = String(mConfig->nrf.sendInterval); + obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); + obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; + obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; + obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + } + + void getInverter(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + obj[F("id")] = id; + obj[F("enabled")] = (bool)iv->config->enabled; + obj[F("name")] = String(iv->config->name); + obj[F("serial")] = String(iv->config->serial.u64, HEX); + obj[F("version")] = String(iv->getFwVersion()); + obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + obj[F("ts_last_success")] = rec->ts; + + JsonArray ch = obj.createNestedArray("ch"); + + // AC + uint8_t pos; + obj[F("ch_name")][0] = "AC"; + JsonArray ch0 = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + + // DC + for(uint8_t j = 0; j < iv->channels; j ++) { + obj[F("ch_name")][j+1] = iv->config->chName[j]; + obj[F("ch_max_pwr")][j+1] = iv->config->chMaxPwr[j]; + JsonArray cur = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); + cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + } + } + } + + void getMqtt(JsonObject obj) { + obj[F("broker")] = String(mConfig->mqtt.broker); + obj[F("port")] = String(mConfig->mqtt.port); + obj[F("user")] = String(mConfig->mqtt.user); + obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); + obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("interval")] = String(mConfig->mqtt.interval); + } + + void getNtp(JsonObject obj) { + obj[F("addr")] = String(mConfig->ntp.addr); + obj[F("port")] = String(mConfig->ntp.port); + } + + void getSun(JsonObject obj) { + obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; + obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; + obj[F("disnightcom")] = mConfig->sun.disNightCom; + obj[F("offs")] = mConfig->sun.offsetSec; + } + + void getPinout(JsonObject obj) { + obj[F("cs")] = mConfig->nrf.pinCs; + obj[F("ce")] = mConfig->nrf.pinCe; + obj[F("irq")] = mConfig->nrf.pinIrq; + obj[F("sclk")] = mConfig->nrf.pinSclk; + obj[F("mosi")] = mConfig->nrf.pinMosi; + obj[F("miso")] = mConfig->nrf.pinMiso; + obj[F("led0")] = mConfig->led.led0; + obj[F("led1")] = mConfig->led.led1; + } + + void getRadio(JsonObject obj) { + obj[F("power_level")] = mConfig->nrf.amplifierPower; + obj[F("isconnected")] = mSys->Radio.isChipConnected(); + obj[F("DataRate")] = mSys->Radio.getDataRate(); + obj[F("isPVariant")] = mSys->Radio.isPVariant(); + } + + void getSerial(JsonObject obj) { + obj[F("interval")] = (uint16_t)mConfig->serial.interval; + obj[F("show_live_data")] = mConfig->serial.showIv; + obj[F("debug")] = mConfig->serial.debug; + } + + void getStaticIp(JsonObject obj) { + char buf[16]; + ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf); + ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf); + ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf); + ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); + } + + void getDisplay(JsonObject obj) { + obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; + obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; + obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; + obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; + obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; + obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; + obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; + obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; + obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; + obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; + obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; + } + + void getIndex(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("ts_now")] = mApp->getTimestamp(); + obj[F("ts_sunrise")] = mApp->getSunrise(); + obj[F("ts_sunset")] = mApp->getSunset(); + obj[F("ts_offset")] = mConfig->sun.offsetSec; + obj[F("disNightComm")] = mConfig->sun.disNightCom; + + JsonArray inv = obj.createNestedArray(F("inverter")); + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + JsonObject invObj = inv.createNestedObject(); + invObj[F("enabled")] = (bool)iv->config->enabled; + invObj[F("id")] = i; + invObj[F("name")] = String(iv->config->name); + invObj[F("version")] = String(iv->getFwVersion()); + invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); + invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); + invObj[F("ts_last_success")] = iv->getLastTs(rec); + } + } + + JsonArray warn = obj.createNestedArray(F("warnings")); + if(!mSys->Radio.isChipConnected()) + warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); + else if(!mSys->Radio.isPVariant()) + warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); + if(!mApp->getSettingsValid()) + warn.add(F("your settings are invalid")); + if(mApp->getRebootRequestState()) + warn.add(F("reboot your ESP to apply all your configuration changes")); + if(0 == mApp->getTimestamp()) + warn.add(F("time not set. No communication to inverter possible")); + /*if(0 == mSys->getNumInverters()) + warn.add(F("no inverter configured"));*/ + + if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0)) + warn.add(F("MQTT is not connected")); + + JsonArray info = obj.createNestedArray(F("infos")); + if(mApp->getMqttIsConnected()) + info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); + if(mConfig->mqtt.interval > 0) + info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); + } + + void getSetup(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + getSysInfo(obj.createNestedObject(F("system"))); + //getInverterList(obj.createNestedObject(F("inverter"))); + getMqtt(obj.createNestedObject(F("mqtt"))); + getNtp(obj.createNestedObject(F("ntp"))); + getSun(obj.createNestedObject(F("sun"))); + getPinout(obj.createNestedObject(F("pinout"))); + getRadio(obj.createNestedObject(F("radio"))); + getSerial(obj.createNestedObject(F("serial"))); + getStaticIp(obj.createNestedObject(F("static_ip"))); + getDisplay(obj.createNestedObject(F("display"))); + } + + void getNetworks(JsonObject obj) { + mApp->getAvailNetworks(obj); + } + + void getLive(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = mConfig->nrf.sendInterval; + + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); + obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); + } + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); + obj[F("fld_names")][fld] = String(fields[dcList[fld]]); + } + + Inverter<> *iv; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + bool parse = false; + if(NULL != iv) + parse = iv->config->enabled; + obj[F("iv")][i] = parse; + } + } + + void getRecord(JsonObject obj, uint8_t recType) { + JsonArray invArr = obj.createNestedArray(F("inverter")); + + Inverter<> *iv; + record_t<> *rec; + uint8_t pos; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + rec = iv->getRecordStruct(recType); + JsonArray obj2 = invArr.createNestedArray(); + for(uint8_t j = 0; j < rec->length; j++) { + byteAssign_t *assign = iv->getByteAssign(j, rec); + pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); + obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; + obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; + obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; + } + } + } + } + + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { + Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); + bool accepted = true; + if(NULL == iv) { + jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); + return false; + } + + if(F("power") == jsonIn[F("cmd")]) + accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); + else if(F("restart") == jsonIn[F("restart")]) + accepted = iv->setDevControlRequest(Restart); + else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { + iv->powerLimit[0] = jsonIn["val"]; + if(F("limit_persistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativPersistent; + else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutPersistent; + else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")]) + iv->powerLimit[1] = RelativNonPersistent; + else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) + iv->powerLimit[1] = AbsolutNonPersistent; + + accepted = iv->setDevControlRequest(ActivePowerContr); + } + else if(F("dev") == jsonIn[F("cmd")]) { + DPRINTLN(DBG_INFO, F("dev cmd")); + iv->enqueCommand(jsonIn[F("val")].as()); + } + else { + jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; + return false; + } + + if(!accepted) { + jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); + return false; + } else + mApp->ivSendHighPrio(iv); + + return true; + } + + bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { + if(F("scan_wifi") == jsonIn[F("cmd")]) + mApp->scanAvailNetworks(); + else if(F("set_time") == jsonIn[F("cmd")]) + mApp->setTimestamp(jsonIn[F("val")]); + else if(F("sync_ntp") == jsonIn[F("cmd")]) + mApp->setTimestamp(0); // 0: update ntp flag + else if(F("serial_utc_offset") == jsonIn[F("cmd")]) + mTimezoneOffset = jsonIn[F("val")]; + else if(F("discovery_cfg") == jsonIn[F("cmd")]) { + mApp->setMqttDiscoveryFlag(); // for homeassistant + } else { + jsonOut[F("error")] = F("unknown cmd"); + return false; + } + + return true; + } + + IApp *mApp; + HMSYSTEM *mSys; + AsyncWebServer *mSrv; + settings_t *mConfig; + + uint32_t mTimezoneOffset; + uint32_t mHeapFree, mHeapFreeBlk; + uint8_t mHeapFrag; + uint16_t nr; +}; + +#endif /*__WEB_API_H__*/ diff --git a/src/web/html/about.html b/src/web/html/about.html new file mode 100644 index 00000000..c0eb8c5e --- /dev/null +++ b/src/web/html/about.html @@ -0,0 +1,57 @@ + + + + About + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+

About AhoyDTU

+
+
+
Used Libraries
+
+ + + + + + + + +
+
Contact Information
+
+
+
Github Repository
+ +
+
+
Discord Chat
+ +
+
+
E-Mail
+ +
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/api.js b/src/web/html/api.js new file mode 100644 index 00000000..5ccb0e15 --- /dev/null +++ b/src/web/html/api.js @@ -0,0 +1,265 @@ +/** + * SVG ICONS + */ + +iconWifi1 = [ + "M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z" +]; + +iconWifi2 = [ + "M13.229 8.271c.216-.216.194-.578-.063-.745A9.456 9.456 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.577 1.336c.205.132.48.108.652-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.408.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .708 0l.707-.707z" +]; + +iconWifi3 = [ + "M15.384 6.115a.485.485 0 0 0-.047-.736A12.444 12.444 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.518.518 0 0 0 .668.05A11.448 11.448 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049z", + "M13.229 8.271a.482.482 0 0 0-.063-.745A9.455 9.455 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091l.016-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z" +]; + +iconWarn = [ + "M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z", + "M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z" +]; + +iconInfo = [ + "M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z", + "m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" +]; + +iconSuccess = [ + "M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z", + "M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" +]; + + /** + * GENERIC FUNCTIONS + */ +function ml(tagName, ...args) { + var el = document.createElement(tagName); + if(args[0]) { + for(var name in args[0]) { + if(name.indexOf("on") === 0) { + el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) + } else { + el.setAttribute(name, args[0][name]); + } + } + } + if (!args[1]) { + return el; + } + return nester(el, args[1]) +} + +function nester(el, n) { + if (typeof n === "string") { + var t = document.createTextNode(n); + el.appendChild(t); + } else if (n instanceof Array) { + for(var i = 0; i < n.length; i++) { + if (typeof n[i] === "string") { + var t = document.createTextNode(n[i]); + el.appendChild(t); + } else if (n[i] instanceof Node){ + el.appendChild(n[i]); + } + } + } else if (n instanceof Node){ + el.appendChild(n) + } + return el; +} + +function topnav() { + toggle("topnav", "mobile"); +} + +function parseNav(obj) { + for(i = 0; i < 11; i++) { + if(i == 2) + continue; + var l = document.getElementById("nav"+i); + if(window.location.pathname == "/" + l.href.split('/').pop()) + l.classList.add("active"); + + if(obj["menu_protEn"]) { + if(obj["menu_prot"]) { + if(0 == i) + l.classList.remove("hide"); + else if(i > 2) { + if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00) + l.classList.remove("hide"); + } + } else if(0 != i) + l.classList.remove("hide"); + } else if(i > 1) + l.classList.remove("hide"); + } +} + +function parseVersion(obj) { + document.getElementById("version").appendChild( + link("https://github.com/lumapu/ahoy/commits/" + obj["build"], "Git SHA: " + obj["build"] + " :: " + obj["version"], "_blank") + ); +} + +function parseESP(obj) { + document.getElementById("esp_type").append( + document.createTextNode("Board: " + obj["esp_type"]) + ); +} + +function parseRssi(obj) { + var icon = iconWifi3; + if(obj["wifi_rssi"] <= -80) + icon = iconWifi1; + else if(obj["wifi_rssi"] <= -70) + icon = iconWifi2; + document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); +} + +function setHide(id, hide) { + var elm = document.getElementById(id); + if(hide) { + if(!elm.classList.contains("hide")) + elm.classList.add("hide"); + } + else + elm.classList.remove('hide'); +} + +function toggle(id, cl="hide") { + var e = document.getElementById(id); + if(!e.classList.contains(cl)) + e.classList.add(cl); + else + e.classList.remove(cl); +} + +function getAjax(url, ptr, method="GET", json=null) { + var xhr = new XMLHttpRequest(); + if(xhr != null) { + xhr.open(method, url, true); + xhr.onreadystatechange = p; + if("POST" == method) + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.send(json); + } + function p() { + if(xhr.readyState == 4) { + if(null != xhr.responseText) { + if(null != ptr) + ptr(JSON.parse(xhr.responseText)); + } + } + } +} + +/** + * CREATE DOM FUNCTIONS + */ + +function des(val) { + e = document.createElement('p'); + e.classList.add("subdes"); + e.innerHTML = val; + return e; +} + +function lbl(htmlfor, val, cl=null, id=null) { + e = document.createElement('label'); + e.htmlFor = htmlfor; + e.innerHTML = val; + if(null != cl) e.classList.add(...cl); + if(null != id) e.id = id; + return e; +} + +function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, title=null, checked=null) { + e = document.createElement('input'); + e.classList.add(...cl); + e.name = name; + if(null != val) e.value = val; + if(null != max) e.maxLength = max; + if(null != id) e.id = id; + if(null != type) e.type = type; + if(null != pattern) e.pattern = pattern; + if(null != title) e.title = title; + if(null != checked) e.checked = checked; + return e; +} + +function sel(name, options, selId) { + e = document.createElement('select'); + e.name = name; + for(it of options) { + o = opt(it[0], it[1], (it[0] == selId)); + e.appendChild(o); + } + return e; +} + +function selDelAllOpt(sel) { + var i, l = sel.options.length - 1; + for(i = l; i >= 0; i--) { + sel.remove(i); + } +} + +function opt(val, html, sel=false) { + o = document.createElement('option'); + o.value = val; + o.innerHTML = html; + if(sel) + o.selected = true; + return o; +} + +function div(cl, h=null) { + e = document.createElement('div'); + e.classList.add(...cl); + if(null != h) e.innerHTML = h; + return e; +} + +function span(val, cl=null, id=null) { + e = document.createElement('span'); + e.innerHTML = val; + if(null != cl) e.classList.add(...cl); + if(null != id) e.id = id; + return e; +} + +function br() { + return document.createElement('br'); +} + +function link(dst, text, target=null) { + var a = document.createElement('a'); + var t = document.createTextNode(text); + a.href = dst; + if(null != target) + a.target = target; + a.appendChild(t); + return a; +} + +function svg(data=null, w=24, h=24, cl=null, tooltip=null) { + var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + s.setAttribute('width', w); + s.setAttribute('height', h); + s.setAttribute('viewBox', '0 0 16 16'); + if(null != cl) s.setAttribute('class', cl); + if(null != data) { + for(const e of data) { + const i = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + i.setAttribute('d', e); + s.appendChild(i); + } + } + if(null != tooltip) { + const t = document.createElement("title"); + t.appendChild(document.createTextNode(tooltip)); + s.appendChild(t); + } + return s; +} diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css new file mode 100644 index 00000000..929828ca --- /dev/null +++ b/src/web/html/colorBright.css @@ -0,0 +1,27 @@ +:root { + --bg: #fff; + --fg: #000; + --fg2: #fff; + + --info: #0000dd; + --warn: #ff7700; + --success: #009900; + + --input-bg: #eee; + + --nav-bg: #333; + --primary: #006ec0; + --primary-hover: #044e86; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #8e5903; + --total-bg: #b06e04; + --iv-head-title: #1c6800; + --iv-head-bg: #32b004; + --ch-head-title: #003c80; + --ch-head-bg: #006ec0; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css new file mode 100644 index 00000000..aa98c862 --- /dev/null +++ b/src/web/html/colorDark.css @@ -0,0 +1,27 @@ +:root { + --bg: #222; + --fg: #ccc; + --fg2: #fff; + + --info: #0072c8; + --warn: #ffaa00; + --success: #00bb00; + + --input-bg: #333; + + --nav-bg: #333; + --primary: #004d87; + --primary-hover: #023155; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #555511; + --total-bg: #666622; + --iv-head-title: #115511; + --iv-head-bg: #226622; + --ch-head-title: #112255; + --ch-head-bg: #223366; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/convert.py b/src/web/html/convert.py new file mode 100644 index 00000000..00f398ac --- /dev/null +++ b/src/web/html/convert.py @@ -0,0 +1,148 @@ +import re +import os +import gzip +import glob +import shutil +from datetime import date +from pathlib import Path +import subprocess + + +def get_git_sha(): + try: + return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() + except: + return "0000000" + +def readVersion(path): + f = open(path, "r") + lines = f.readlines() + f.close() + + today = date.today() + search = ["_MAJOR", "_MINOR", "_PATCH"] + version = today.strftime("%y%m%d") + "_ahoy_" + ver = "" + for line in lines: + if(line.find("VERSION_") != -1): + for s in search: + p = line.find(s) + if(p != -1): + version += line[p+13:].rstrip() + "." + ver += line[p+13:].rstrip() + "." + return ver[:-1] + +def htmlParts(file, header, nav, footer, version): + p = ""; + f = open(file, "r") + lines = f.readlines() + f.close(); + + f = open(header, "r") + h = f.read().strip() + f.close() + + f = open(nav, "r") + n = f.read().strip() + f.close() + + f = open(footer, "r") + fo = f.read().strip() + f.close() + + for line in lines: + line = line.replace("{#HTML_HEADER}", h) + line = line.replace("{#HTML_NAV}", n) + line = line.replace("{#HTML_FOOTER}", fo) + p += line + + #placeholders + link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + '' + p = p.replace("{#VERSION}", version) + p = p.replace("{#VERSION_GIT}", link) + f = open("tmp/" + file, "w") + f.write(p); + f.close(); + return p + +def convert2Header(inFile, version): + fileType = inFile.split(".")[1] + define = inFile.split(".")[0].upper() + define2 = inFile.split(".")[1].upper() + inFileVarName = inFile.replace(".", "_") + + if os.getcwd()[-4:] != "html": + outName = "html/" + "h/" + inFileVarName + ".h" + inFile = "html/" + inFile + Path("html/h").mkdir(exist_ok=True) + else: + outName = "h/" + inFileVarName + ".h" + + data = "" + if fileType == "ico": + f = open(inFile, "rb") + data = f.read() + f.close() + else: + if fileType == "html": + data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version) + else: + f = open(inFile, "r") + data = f.read() + f.close() + + if fileType == "css": + data = data.replace('\n', '') + data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css + + length = len(data) + + f = open(outName, "w") + f.write("#ifndef __{}_{}_H__\n".format(define, define2)) + f.write("#define __{}_{}_H__\n".format(define, define2)) + + if fileType == "ico": + zipped = gzip.compress(bytes(data)) + else: + zipped = gzip.compress(bytes(data, 'utf-8')) + zippedStr = "" + for i in range(len(zipped)): + zippedStr += "0x{:02x}".format(zipped[i]) #hex(zipped[i]) + if (i + 1) != len(zipped): + zippedStr += ", " + if (i + 1) % 16 == 0 and i != 0: + zippedStr += "\n" + f.write("#define {}_len {}\n".format(inFileVarName, len(zipped))) + f.write("const uint8_t {}[] PROGMEM = {{\n{}}};\n".format(inFileVarName, zippedStr)) + f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) + f.close() + +# delete all files in the 'h' dir +wd = 'h' +if os.getcwd()[-4:] != "html": + wd = "web/html/" + wd + +if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) +wd += "/tmp" +if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) + +# grab all files with following extensions +if os.getcwd()[-4:] != "html": + os.chdir('./web/html') +types = ('*.html', '*.css', '*.js', '*.ico') # the tuple of file types +files_grabbed = [] +for files in types: + files_grabbed.extend(glob.glob(files)) + +Path("h").mkdir(exist_ok=True) +Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements +shutil.copyfile("style.css", "tmp/style.css") +version = readVersion("../../defines.h") + +# go throw the array +for val in files_grabbed: + convert2Header(val, version) diff --git a/src/web/html/favicon.ico b/src/web/html/favicon.ico new file mode 100644 index 00000000..cb159e2b Binary files /dev/null and b/src/web/html/favicon.ico differ diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html new file mode 100644 index 00000000..bbc4c6ce --- /dev/null +++ b/src/web/html/includes/footer.html @@ -0,0 +1,16 @@ + diff --git a/src/web/html/includes/header.html b/src/web/html/includes/header.html new file mode 100644 index 00000000..f38a30f7 --- /dev/null +++ b/src/web/html/includes/header.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html new file mode 100644 index 00000000..3dac1590 --- /dev/null +++ b/src/web/html/includes/nav.html @@ -0,0 +1,24 @@ + diff --git a/src/web/html/index.html b/src/web/html/index.html new file mode 100644 index 00000000..72537e5e --- /dev/null +++ b/src/web/html/index.html @@ -0,0 +1,225 @@ + + + + Index + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+

+ Uptime:
+ ESP-Time: +

+

+ System Infos: +

+
+
+

+ +
+
+

Support this project:

+ +

+ This project was started from this discussion. (Mikrocontroller.net) +

+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/login.html b/src/web/html/login.html new file mode 100644 index 00000000..e790f6a4 --- /dev/null +++ b/src/web/html/login.html @@ -0,0 +1,23 @@ + + + + Login + {#HTML_HEADER} + + +
+
+
+
+

AhoyDTU

+
+
+
+
+
+
+
+
+ {#HTML_FOOTER} + + diff --git a/src/web/html/save.html b/src/web/html/save.html new file mode 100644 index 00000000..54d43d7f --- /dev/null +++ b/src/web/html/save.html @@ -0,0 +1,51 @@ + + + + Save + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/tools/esp8266/html/serial.html b/src/web/html/serial.html similarity index 50% rename from tools/esp8266/html/serial.html rename to src/web/html/serial.html index 07b74802..da9d2816 100644 --- a/tools/esp8266/html/serial.html +++ b/src/web/html/serial.html @@ -2,85 +2,72 @@ Serial Console - - - + {#HTML_HEADER} - + {#HTML_NAV}
-
-
- connected: - Uptime: - - -
-
-
-
-
+
+ +
+
+
connected:
+
Uptime:
+
+ + +
+
+
+

Commands

-
- - -
-
+
+
+
Select Inverter
+
+
+
+
Power Limit Command
+
+ +
+
+
+
Power Limit Value
+
+
+
+
+
+
+
+
Control Inverter
+
-
-
-
-
-
- - - - -
- -
-

Ctrl result: n/a

+
+
+
Ctrl result
+
n/a
- + {#HTML_FOOTER} diff --git a/src/web/html/setup.html b/src/web/html/setup.html new file mode 100644 index 00000000..bfbda402 --- /dev/null +++ b/src/web/html/setup.html @@ -0,0 +1,806 @@ + + + + Setup + {#HTML_HEADER} + + + + {#HTML_NAV} +
+
+
+ +
+
+ Device Host Name +
+
Device Name
+
+
+
+
Dark Mode
+
+
+
+
+ System Config +

Pinout

+
+ +

Radio (NRF24L01+)

+
+ +

Serial Console

+
+
print inverter data
+
+
+
+
Serial Debug
+
+
+
+
Interval [s]
+
+
+
+
+ + +
+
+ WiFi +

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

+ +
+
Search Networks
+
+
+ +
+
Avail Networks
+
+ +
+
+
+
SSID
+
+
+
+
Password
+
+
+
+
+ Static IP (optional) +

+ Leave fields blank for DHCP
+ The following fields are parsed in this format: 192.168.4.1 +

+
+
IP Address
+
+
+
+
Submask
+
+
+
+
DNS 1
+
+
+
+
DNS 2
+
+
+
+
Gateway
+
+
+
+
+ + +
+
+ Protection +
+
Admin Password
+
+
+

Select pages which should be protected by password

+
+
+
+ + +
+
+ Inverter +
+
+
+
+
+
+

Note

+

A 'max module power' value of '0' disables the channel in 'live' view

+
+
+

General

+
+
+
+
Interval [s]
+
+
+
+
Max retries per Payload
+
+
+
+
Reset values and YieldDay at midnight
+
+
+
+
Reset values when inverter polling pauses at sunset
+
+
+
+
Reset values when inverter status is 'not available'
+
+
+
+
+ + +
+
+ NTP Server +
+
NTP Server / IP
+
+
+
+
NTP Port
+
+
+
+
set system time
+
+ + + +
+
+
+
+ + +
+
+ Sunrise & Sunset +

Use a decimal separator: '.' (dot) for Latitude and Longitude

+ +
+
Latitude (decimal)
+
+
+
+
Longitude (decimal)
+
+
+
+
Offset (pre sunrise, post sunset)
+
+
+
+
Pause polling inverters during night
+
+
+
+
+ + +
+
+ MQTT +
+
Broker / Server IP
+
+
+
+
Port
+
+
+
+
Username (optional)
+
+
+
+
Password (optional)
+
+
+
+
Topic
+
+
+

Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

+
+
Interval [s]
+
+
+
+
Discovery Config (homeassistant)
+
+ + +
+
+
+
+ + +
+
+ Display Config +
+
+
+
Turn off while inverters are offline
+
+
+
+
Enable Screensaver (pixel shifting, OLED only)
+
+
+
+
Luminance
+
+
+

Pinout

+
+
+
+ +
+
Reboot device after successful save
+
+ + +
+
+
+
+
+ ERASE SETTINGS (not WiFi) +
+ Import / Export JSON Settings +
+
Import
+
+
+ + +
+
+
+
+
Export
+
+ Export settings (JSON file) (only values, passwords will be removed!) +
+
+
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/style.css b/src/web/html/style.css new file mode 100644 index 00000000..ca4b0c9a --- /dev/null +++ b/src/web/html/style.css @@ -0,0 +1,633 @@ +html, body { + font-family: Arial; + margin: 0; + padding: 0; + height: 100%; + min-height: 100%; + background-color: var(--bg); + color: var(--fg); +} + +h2 { + padding-left: 10px; +} + +span, li, h3, label, fieldset { + color: var(--fg); +} + +fieldset, input[type=submit], .btn { + border-radius: 4px; +} + +#live span { + color: var(--fg2); +} + +.topnav { + background-color: var(--nav-bg); + position: fixed; + top: 0; + width: 100%; +} + +.topnav a { + color: var(--fg2); + padding: 14px 14px; + text-decoration: none; + font-size: 17px; + display: block; +} + +#topnav a { + color: #fff; +} + +.topnav a.icon { + top: 0; + left: 0; + background: var(--nav-bg); + display: block; + position: absolute; +} + +.topnav a:hover { + background-color: var(--primary-hover) !important; +} + +.topnav .info { + color: var(--fg2); + position: absolute; + right: 24px; + top: 5px; +} + +.topnav .mobile { + display: none; +} + +svg.icon { + vertical-align: middle; + display: inline-block; + margin-top:-4x; + padding: 5px 7px 5px 0px; +} + +.icon-info { + fill: var(--info); +} + +.icon-warn { + fill: var(--warn); +} + +.icon-success { + fill: var(--success); +} + +.wifi { + fill: var(--fg2); +} + +.title { + background-color: var(--primary); + color: #fff !important; + padding-left: 80px !important +} + +.topnav .icon span { + display: block; + width: 30px; + height: 3px; + margin-bottom: 5px; + position: relative; + background: #fff; + border-radius: 2px; +} + +.topnav .active { + background-color: var(--nav-active); +} + +span.seperator { + width: 100%; + height: 1px; + margin: 5px 0px 5px; + background-color: #494949; + display: block; +} + +#content { + max-width: 1140px; +} + +.total-h { + background-color: var(--total-head-title); + color: var(--fg2); +} + +.total-bg { + background-color: var(--total-bg); + color: var(--fg2); +} + +.iv-h { + background-color: var(--iv-head-title); + color: var(--fg2); +} + +.iv-bg { + background-color: var(--iv-head-bg); + color: var(--fg2); +} + +.ch-h { + background-color: var(--ch-head-title); + color: var(--fg2); +} + +.ch-bg { + background-color: var(--ch-head-bg); + color: var(--fg2); +} + +.ts-h { + background-color: var(--ts-head); + color: var(--fg2); +} + +.ts-bg { + background-color: var(--ts-bg); + color: var(--fg2); +} + +.hr { + border-top: 1px solid var(--iv-head-title); + margin: 1rem 0 1rem; +} + +p { + text-align: justify; + font-size: 13pt; + color: var(--fg); +} + +#footer { + background-color: var(--footer-bg); +} + +.row { display: flex; max-width: 100%; flex-wrap: wrap; } +.col { flex: 1 0 0%; } + +.col-1, .col-2, .col-3, .col-4, +.col-5, .col-6, .col-7, .col-8, +.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } + +.col-1 { width: 8.333333333%; } +.col-2 { width: 16.66666667%; } +.col-3 { width: 25%; } +.col-4 { width: 33.33333333%; } +.col-5 { width: 41.66666667%; } +.col-6 { width: 50%; } +.col-7 { width: 58.33333333%; } +.col-8 { width: 66.66666667%; } +.col-9 { width: 75%; } +.col-10 { width: 83.33333333%; } +.col-11 { width: 91.66666667%; } +.col-12 { width: 100%; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 1rem; } +.p-4 { padding: 1.5rem; } +.p-5 { padding: 3rem; } + +.px-1 { padding: 0 0.25rem 0 0.25rem; } +.px-2 { padding: 0 0.5rem 0 0.5rem; } +.px-3 { padding: 0 1rem 0 1rem; } +.px-4 { padding: 0 1.5rem 0 1.5rem; } +.px-5 { padding: 0 3rem 0 3rem; } + +.py-1 { padding: 0.25rem 0 0.25rem; } +.py-2 { padding: 0.5rem 0 0.5rem; } +.py-3 { padding: 1rem 0 1rem; } +.py-4 { padding: 1.5rem 0 1.5rem; } +.py-5 { padding: 3rem 0 3rem; } + +.mx-1 { margin: 0 0.25rem 0 0.25rem; } +.mx-2 { margin: 0 0.5rem 0 0.5rem; } +.mx-3 { margin: 0 1rem 0 1rem; } +.mx-4 { margin: 0 1.5rem 0 1.5rem; } +.mx-5 { margin: 0 3rem 0 3rem; } + +.my-1 { margin: 0.25rem 0 0.25rem; } +.my-2 { margin: 0.5rem 0 0.5rem; } +.my-3 { margin: 1rem 0 1rem; } +.my-4 { margin: 1.5rem 0 1.5rem; } +.my-5 { margin: 3rem 0 3rem; } + +.mt-1 { margin-top: 0.25rem } +.mt-2 { margin-top: 0.5rem } +.mt-3 { margin-top: 1rem } +.mt-4 { margin-top: 1.5rem } +.mt-5 { margin-top: 3rem } + +.mb-1 { margin-bottom: 0.25rem } +.mb-2 { margin-bottom: 0.5rem } +.mb-3 { margin-bottom: 1rem } +.mb-4 { margin-bottom: 1.5rem } +.mb-5 { margin-bottom: 3rem } + +.fs-1 { font-size: 3.5rem; } +.fs-2 { font-size: 3rem; } +.fs-3 { font-size: 2.5rem; } +.fs-4 { font-size: 2rem; } +.fs-5 { font-size: 1.75rem; } +.fs-6 { font-size: 1.5rem; } +.fs-7 { font-size: 1.25rem; } +.fs-8 { font-size: 1rem; } +.fs-9 { font-size: 0.75rem; } +.fs-10 { font-size: 0.5rem; } + +.a-r { text-align: right; } +.a-c { text-align: center; } + +.row > * { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +*, ::after, ::before { + box-sizing: border-box; +} + +/* sm */ +@media(min-width: 768px) { + .col-sm-1 { width: 8.333333333%; } + .col-sm-2 { width: 16.66666667%; } + .col-sm-3 { width: 25%; } + .col-sm-4 { width: 33.33333333%; } + .col-sm-5 { width: 41.66666667%; } + .col-sm-6 { width: 50%; } + .col-sm-7 { width: 58.33333333%; } + .col-sm-8 { width: 66.66666667%; } + .col-sm-9 { width: 75%; } + .col-sm-10 { width: 83.33333333%; } + .col-sm-11 { width: 91.66666667%; } + .col-sm-12 { width: 100%; } + + .mb-sm-1 { margin-bottom: 0.25rem } + .mb-sm-2 { margin-bottom: 0.5rem } + .mb-sm-3 { margin-bottom: 1rem } + .mb-sm-4 { margin-bottom: 1.5rem } + .mb-sm-5 { margin-bottom: 3rem } + + .fs-sm-1 { font-size: 3.5rem; } + .fs-sm-2 { font-size: 3rem; } + .fs-sm-3 { font-size: 2.5rem; } + .fs-sm-4 { font-size: 2rem; } + .fs-sm-5 { font-size: 1.75rem; } + .fs-sm-6 { font-size: 1.5rem; } + .fs-sm-7 { font-size: 1.25rem; } + .fs-sm-8 { font-size: 1rem; } +} + +/* md */ +@media(min-width: 992px) { + .col-md-1 { width: 8.333333333%; } + .col-md-2 { width: 16.66666667%; } + .col-md-3 { width: 25%; } + .col-md-4 { width: 33.33333333%; } + .col-md-5 { width: 41.66666667%; } + .col-md-6 { width: 50%; } + .col-md-7 { width: 58.33333333%; } + .col-md-8 { width: 66.66666667%; } + .col-md-9 { width: 75%; } + .col-md-10 { width: 83.33333333%; } + .col-md-11 { width: 91.66666667%; } + .col-md-12 { width: 100%; } +} + +#wrapper { + min-height: 100%; +} + +#content { + padding: 50px 20px 120px 20px; + overflow: auto; +} + +#footer { + height: 121px; + margin-top: -121px; + width: 100%; + font-size: 13px; +} + +#footer .right { + color: #bbb; + margin: 6px 25px; + text-align: right; +} + +#footer .left { + color: #bbb; + margin: 23px 0px 0px 25px; +} + +#footer ul { + list-style-type: none; + margin: 20px auto; + padding: 0; +} + +#footer ul li, #footer a { + color: #bbb; + margin-bottom: 10px; + padding-left: 5px; + font-size: 13px; +} + +#footer a:hover { + color: #fff; +} + +.hide { + display: none !important; +} + +@media only screen and (min-width: 992px) { + .topnav { + width: 230px !important; + height: 100%; + } + + .topnav a.icon { + display: none !important; + } + + .topnav a { + padding: 14px 24px; + } + + .topnav .title { + padding-left: 24px !important; + } + + .topnav .mobile { + display: block; + } + + .topnav .info { + top: auto !important; + right: auto !important; + bottom: 14px; + left: 24px; + } + + #content { + padding: 15px 15px 120px 250px; + } + + #footer .left { + margin-left: 250px !important; + } +} + +p.lic, p.lic a { + font-size: 8pt; + color: #999; +} + +.des { + margin-top: 20px; + font-size: 13pt; + color: var(--secondary); +} + +.s_active, .s_collapsible:hover { + background-color: var(--primary-hover); + color: #fff; +} + +.s_content { + display: none; + overflow: hidden; +} + +.s_collapsible { + background-color: var(--primary); + color: white; + cursor: pointer; + padding: 12px; + width: 100%; + border: none; + text-align: left; + outline: none; + font-size: 15px; + margin-bottom: 5px; +} + +.subdes { + font-size: 12pt; + color: var(--secondary); + margin-left: 7px; +} + +.subsubdes { + font-size:12pt; + color:var(--secondary); + margin: 0 0 7px 12px; +} + +a:link, a:visited { + text-decoration: none; + font-size: 13pt; + color: var(--secondary); +} + +a:hover, a:focus { + color: #f00; +} + +a.btn { + background-color: var(--primary); + color: #fff; + padding: 7px 15px 7px 15px; + display: inline-block; +} + +a.btn:hover { + background-color: var(--primary-hover) !important; +} + +input, select { + padding: 7px; + font-size: 13pt; +} + +input[type=text], input[type=password], select, input[type=number] { + width: 100%; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 4px; + background-color: var(--input-bg); + color: var(--fg); +} + +input.sh { + max-width: 150px !important; + margin-right: 10px; +} + +input.btnDel { + background-color: #c00 !important; +} + +input.btn { + background-color: var(--primary); + color: #fff; + border: 0px; + padding: 7px 20px 7px 20px; + margin-bottom: 10px; + text-transform: uppercase; + cursor: pointer; +} + +input.btn:hover { + background-color: #044e86; +} + +input.cb { + margin-bottom: 15px; + margin-top: 10px; +} + +label { + width: 20%; + display: inline-block; + font-size: 12pt; + padding-right: 10px; + margin: 10px 0px 0px 15px; + vertical-align: top; +} + +pre { + white-space: pre-wrap; +} + +.left { + float: left; +} + +.right { + float: right; +} + +.subgrp { + float: left; + width: 220px; +} + +div.ModPwr, div.ModName, div.YieldCor { + width:70%; + display: inline-block; +} + +div.hr { + height: 1px; + border-top: 1px solid #ccc; + margin: 10px 0px 10px; +} + +#note { + margin: 10px 10px 10px 10px; + padding-top: 10px; + width: 100%; +} + +@media(max-width: 500px) { + div.ch .unit, div.ch-iv .unit { + font-size: 18px; + } + + div.ch { + width: 170px; + min-height: 100px + } + + .subgrp { + width: 180px; + } +} + +#serial { + width: 100%; +} + +#content .serial { + max-width: 1000px; +} + +.dot { + height: 15px; + width: 15px; + background-color: #f00; + border-radius: 50%; + display: inline-block; + margin-top: 15px; +} + +#login { + width: 450px; + height: 200px; + border: 1px solid #ccc; + background-color: var(--input-bg); + position: absolute; + top: 50%; + left: 50%; + margin-top: -160px; + margin-left: -225px; +} + + +.head { + background-color: var(--primary); + color: #fff; +} + + +.css-tooltip{ + position: relative; +} +.css-tooltip:hover:after{ + content:attr(data-tooltip); + background:#000; + padding:5px; + border-radius:3px; + display: inline-block; + position: absolute; + transform: translate(-50%,-100%); + margin:0 auto; + color:#FFF; + min-width:100px; + min-width:150px; + top:-5px; + left: 50%; + text-align:center; +} +.css-tooltip:hover:before { + top:-5px; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(0, 0, 0, 0); + border-top-color: #000; + border-width: 5px; + margin-left: -5px; + transform: translate(0,0px); +} diff --git a/src/web/html/system.html b/src/web/html/system.html new file mode 100644 index 00000000..0f1df319 --- /dev/null +++ b/src/web/html/system.html @@ -0,0 +1,120 @@ + + + + System + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+

+                
+
+
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/update.html b/src/web/html/update.html new file mode 100644 index 00000000..214bc19f --- /dev/null +++ b/src/web/html/update.html @@ -0,0 +1,37 @@ + + + + Update + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+
+ Select firmware file (*.bin) +
+ + +
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html new file mode 100644 index 00000000..de5b0069 --- /dev/null +++ b/src/web/html/visualization.html @@ -0,0 +1,238 @@ + + + + Live + {#HTML_HEADER} + + + + {#HTML_NAV} +
+
+
+

Every seconds the values are updated

+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/web.h b/src/web/web.h new file mode 100644 index 00000000..47bdeb1d --- /dev/null +++ b/src/web/web.h @@ -0,0 +1,856 @@ +//----------------------------------------------------------------------------- +// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __WEB_H__ +#define __WEB_H__ + +#include "../utils/dbg.h" +#ifdef ESP32 +#include "AsyncTCP.h" +#include "Update.h" +#else +#include "ESPAsyncTCP.h" +#endif +#include "../appInterface.h" +#include "../hm/hmSystem.h" +#include "../utils/helper.h" +#include "ESPAsyncWebServer.h" +#include "html/h/api_js.h" +#include "html/h/colorBright_css.h" +#include "html/h/colorDark_css.h" +#include "html/h/favicon_ico.h" +#include "html/h/index_html.h" +#include "html/h/login_html.h" +#include "html/h/serial_html.h" +#include "html/h/setup_html.h" +#include "html/h/style_css.h" +#include "html/h/system_html.h" +#include "html/h/save_html.h" +#include "html/h/update_html.h" +#include "html/h/visualization_html.h" +#include "html/h/about_html.h" + +#define WEB_SERIAL_BUF_SIZE 2048 + +const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1"}; + +template +class Web { + public: + Web(void) : mWeb(80), mEvts("/events") { + mProtected = true; + mLogoutTimeout = 0; + + memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); + mSerialBufFill = 0; + mSerialAddTime = true; + mSerialClientConnnected = false; + } + + void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + + DPRINTLN(DBG_VERBOSE, F("app::setup-on")); + mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); + mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); + mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); + mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); + mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); + mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); + mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); + mWeb.onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1)); + mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1)); + mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1)); + mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1)); + mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); + + mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); + mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); + + mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); + //mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); + + #ifdef ENABLE_PROMETHEUS_EP + mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); + #endif + + mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1)); + mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1), + std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1), + std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1)); + mWeb.on("/about", HTTP_GET, std::bind(&Web::onAbout, this, std::placeholders::_1)); + mWeb.on("/debug", HTTP_GET, std::bind(&Web::onDebug, this, std::placeholders::_1)); + + + mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1)); + mWeb.addHandler(&mEvts); + + mWeb.begin(); + + registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h + + mUploadFail = false; + } + + void tickSecond() { + if (0 != mLogoutTimeout) { + mLogoutTimeout -= 1; + if (0 == mLogoutTimeout) { + if (strlen(mConfig->sys.adminPwd) > 0) + mProtected = true; + } + + DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); + } + + if (mSerialClientConnnected) { + if (mSerialBufFill > 0) { + mEvts.send(mSerialBuf, "serial", millis()); + memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); + mSerialBufFill = 0; + } + } + } + + AsyncWebServer *getWebSrvPtr(void) { + return &mWeb; + } + + void setProtection(bool protect) { + mProtected = protect; + } + + bool getProtection() { + return mProtected; + } + + void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + mApp->setOnUpdate(); + + if (!index) { + Serial.printf("Update Start: %s\n", filename.c_str()); + #ifndef ESP32 + Update.runAsync(true); + #endif + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + Update.printError(Serial); + } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) + Update.printError(Serial); + } + if (final) { + if (Update.end(true)) + Serial.printf("Update Success: %uB\n", index + len); + else + Update.printError(Serial); + } + } + + void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + if (!index) { + mUploadFail = false; + mUploadFp = LittleFS.open("/tmp.json", "w"); + if (!mUploadFp) { + DPRINTLN(DBG_ERROR, F("can't open file!")); + mUploadFail = true; + mUploadFp.close(); + return; + } + } + mUploadFp.write(data, len); + if (final) { + mUploadFp.close(); + char pwd[PWD_LEN]; + strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD + if (!mApp->readSettings("/tmp.json")) { + mUploadFail = true; + DPRINTLN(DBG_ERROR, F("upload JSON error!")); + } else { + LittleFS.remove("/tmp.json"); + strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD + mApp->saveSettings(true); + } + if (!mUploadFail) + DPRINTLN(DBG_INFO, F("upload finished!")); + } + } + + void serialCb(String msg) { + if (!mSerialClientConnnected) + return; + + msg.replace("\r\n", ""); + if (mSerialAddTime) { + if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { + if (mApp->getTimestamp() > 0) { + strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); + mSerialBufFill += 9; + } + } else { + mSerialBufFill = 0; + mEvts.send("webSerial, buffer overflow!", "serial", millis()); + return; + } + mSerialAddTime = false; + } + + if (msg.endsWith("")) + mSerialAddTime = true; + + uint16_t length = msg.length(); + if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { + strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); + mSerialBufFill += length; + } else { + mSerialBufFill = 0; + mEvts.send("webSerial, buffer overflow!", "serial", millis()); + } + } + + private: + void checkRedirect(AsyncWebServerRequest *request) { + if ((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX) + request->redirect(F("/index")); + else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE) + request->redirect(F("/live")); + else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL) + request->redirect(F("/serial")); + else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM) + request->redirect(F("/system")); + else + request->redirect(F("/login")); + } + + void onUpdate(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onUpdate")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showUpdate(AsyncWebServerRequest *request) { + bool reboot = (!Update.hasError()); + + String html = F("UpdateUpdate: "); + if (reboot) + html += "success"; + else + html += "failed"; + html += F("

rebooting ... auto reload after 20s"); + + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); + response->addHeader("Connection", "close"); + request->send(response); + mApp->setRebootFlag(); + } + + void onUpload(AsyncWebServerRequest *request) { + bool reboot = !mUploadFail; + + String html = F("UploadUpload: "); + if (reboot) + html += "success"; + else + html += "failed"; + html += F("

rebooting ... auto reload after 20s"); + + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); + response->addHeader("Connection", "close"); + request->send(response); + mApp->setRebootFlag(); + } + + void onConnect(AsyncEventSourceClient *client) { + DPRINTLN(DBG_VERBOSE, "onConnect"); + + mSerialClientConnnected = true; + + if (client->lastId()) + DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); + + client->send("hello!", NULL, millis(), 1000); + } + + void onIndex(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onIndex")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onLogin(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLogin")); + + if (request->args() > 0) { + if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { + mProtected = false; + request->redirect("/"); + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onLogout(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLogout")); + + if (mProtected) { + checkRedirect(request); + return; + } + + mProtected = true; + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onColor(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onColor")); + AsyncWebServerResponse *response; + if (mConfig->sys.darkMode) + response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len); + else + response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onCss(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onCss")); + mLogoutTimeout = LOGOUT_TIMEOUT; + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onApiJs(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onApiJs")); + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onFavicon(AsyncWebServerRequest *request) { + static const char favicon_type[] PROGMEM = "image/x-icon"; + AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showNotFound(AsyncWebServerRequest *request) { + if (mProtected) + checkRedirect(request); + else + request->redirect("/setup"); + } + + void onReboot(AsyncWebServerRequest *request) { + mApp->setRebootFlag(); + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showErase(AsyncWebServerRequest *request) { + if (mProtected) { + checkRedirect(request); + return; + } + + DPRINTLN(DBG_VERBOSE, F("showErase")); + mApp->eraseSettings(false); + onReboot(request); + } + + void showFactoryRst(AsyncWebServerRequest *request) { + if (mProtected) { + checkRedirect(request); + return; + } + + DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); + String content = ""; + int refresh = 3; + if (request->args() > 0) { + if (request->arg("reset").toInt() == 1) { + refresh = 10; + if (mApp->eraseSettings(true)) + content = F("factory reset: success\n\nrebooting ... "); + else + content = F("factory reset: failed\n\nrebooting ... "); + } else { + content = F("factory reset: aborted"); + refresh = 3; + } + } else { + content = F("

Factory Reset

" + "

RESET

CANCEL

"); + refresh = 120; + } + request->send(200, F("text/html; charset=UTF-8"), F("Factory Reset") + content + F("")); + if (refresh == 10) + onReboot(request); + } + + void onSetup(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSetup")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void showSave(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("showSave")); + + if (mProtected) { + checkRedirect(request); + return; + } + + if (request->args() == 0) + return; + + char buf[20] = {0}; + + // general + if (request->arg("ssid") != "") + request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); + if (request->arg("pwd") != "{PWD}") + request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); + if (request->arg("device") != "") + request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); + mConfig->sys.darkMode = (request->arg("darkMode") == "on"); + + // protection + if (request->arg("adminpwd") != "{PWD}") { + request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); + mProtected = (strlen(mConfig->sys.adminPwd) > 0); + } + mConfig->sys.protectionMask = 0x0000; + for (uint8_t i = 0; i < 6; i++) { + if (request->arg("protMask" + String(i)) == "on") + mConfig->sys.protectionMask |= (1 << i); + } + + // static ip + request->arg("ipAddr").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.ip, buf); + request->arg("ipMask").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.mask, buf); + request->arg("ipDns1").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.dns1, buf); + request->arg("ipDns2").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.dns2, buf); + request->arg("ipGateway").toCharArray(buf, 20); + ah::ip2Arr(mConfig->sys.ip.gateway, buf); + + // inverter + Inverter<> *iv; + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i, false); + // enable communication + iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on"); + // address + request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); + if (strlen(buf) == 0) + memset(buf, 0, 20); + iv->config->serial.u64 = ah::Serial2u64(buf); + switch(iv->config->serial.b[4]) { + case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; + case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; + case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; + default: break; + } + + // name + request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH); + + // max channel power / name + for (uint8_t j = 0; j < 4; j++) { + iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt(); + iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; + request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH); + } + iv->initialized = true; + } + + if (request->arg("invInterval") != "") + mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); + if (request->arg("invRetry") != "") + mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt(); + mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); + mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); + mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); + + // pinout + uint8_t pin; + for (uint8_t i = 0; i < 8; i++) { + pin = request->arg(String(pinArgNames[i])).toInt(); + switch(i) { + default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; + case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break; + case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break; + case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_SCLK_PIN); break; + case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_MOSI_PIN); break; + case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_MISO_PIN); break; + case 6: mConfig->led.led0 = pin; break; + case 7: mConfig->led.led1 = pin; break; + } + } + + // nrf24 amplifier power + mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03; + + // ntp + if (request->arg("ntpAddr") != "") { + request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN); + mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff; + } + + // sun + if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { + mConfig->sun.lat = 0.0; + mConfig->sun.lon = 0.0; + mConfig->sun.disNightCom = false; + mConfig->sun.offsetSec = 0; + } else { + mConfig->sun.lat = request->arg("sunLat").toFloat(); + mConfig->sun.lon = request->arg("sunLon").toFloat(); + mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on"); + mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; + } + + // mqtt + if (request->arg("mqttAddr") != "") { + String addr = request->arg("mqttAddr"); + addr.trim(); + addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); + } else + mConfig->mqtt.broker[0] = '\0'; + request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); + if (request->arg("mqttPwd") != "{PWD}") + request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); + request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); + mConfig->mqtt.port = request->arg("mqttPort").toInt(); + mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); + + // serial console + if (request->arg("serIntvl") != "") { + mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff; + + mConfig->serial.debug = (request->arg("serDbg") == "on"); + mConfig->serial.showIv = (request->arg("serEn") == "on"); + // Needed to log TX buffers to serial console + mSys->Radio.mSerialDebug = mConfig->serial.debug; + } + + // display + mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); + mConfig->plugin.display.pxShift = (request->arg("disp_pxshift") == "on"); + mConfig->plugin.display.rot = request->arg("disp_rot").toInt(); + mConfig->plugin.display.type = request->arg("disp_typ").toInt(); + mConfig->plugin.display.contrast = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt(); + mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_data").toInt(); + mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_clk").toInt(); + mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_cs").toInt(); + mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt(); + mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt(); + mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt(); + + mApp->saveSettings((request->arg("reboot") == "on")); + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onLive(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onLive")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + response->addHeader(F("content-type"), "text/html; charset=UTF-8"); + + request->send(response); + } + + void onAbout(AsyncWebServerRequest *request) { + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + response->addHeader(F("content-type"), "text/html; charset=UTF-8"); + + request->send(response); + } + + void onDebug(AsyncWebServerRequest *request) { + mApp->getSchedulerNames(); + AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); + request->send(response); + } + + void onSerial(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSerial")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + void onSystem(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("onSystem")); + + if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { + if (mProtected) { + checkRedirect(request); + return; + } + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + request->send(response); + } + + +#ifdef ENABLE_PROMETHEUS_EP + enum { + metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd + } metricsStep; + int metricsInverterId,metricsChannelId; + + void showMetrics(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); + + metricsStep = metricsStateStart; + AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), + [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t + { + Inverter<> *iv; + record_t<> *rec; + statistics_t *stat; + String promUnit, promType; + String metrics; + char type[60], topic[100], val[25]; + size_t len = 0; + int alarmChannelId; + + switch (metricsStep) { + case metricsStateStart: // System Info & NRF Statistics : fit to one packet + snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", + mApp->getVersion(), mConfig->sys.deviceName); + metrics = String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); + metrics += String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime()); + metrics += String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI()); + metrics += String(type) + String(topic); + + // NRF Statistics + stat = mApp->getStatistics(); + metrics += radioStatistic(F("rx_success"), stat->rxSuccess); + metrics += radioStatistic(F("rx_fail"), stat->rxFail); + metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); + metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); + metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); + + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + // Start Inverter loop + metricsInverterId = 0; + metricsStep = metricsStateInverter; + break; + + case metricsStateInverter: // Inverter loop + if (metricsInverterId < mSys->getNumInverters()) { + iv = mSys->getInverterByPos(metricsInverterId); + if(NULL != iv) { + // Inverter info : fit to one packet + snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", + iv->config->name, iv->config->serial.u64); + metrics = String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); + metrics += String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); + metrics += String(type) + String(topic); + + snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); + snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); + metrics += String(type) + String(topic); + + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + + // Start Realtime Data Channel loop for this inverter + metricsChannelId = 0; + metricsStep = metricStateRealtimeData; + } + } else { + metricsStep = metricsStateEnd; + } + break; + + case metricStateRealtimeData: // Realtime Data Channel loop + iv = mSys->getInverterByPos(metricsInverterId); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (metricsChannelId < rec->length) { + uint8_t channel = rec->assign[metricsChannelId].ch; + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + if (0 == channel) { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); + } else { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); + } + snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); + len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); + + metricsChannelId++; + } else { + len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + + // All realtime data channels processed --> try alarm data + metricsStep = metricsStateAlarmData; + } + break; + + case metricsStateAlarmData: // Alarm Info loop + iv = mSys->getInverterByPos(metricsInverterId); + rec = iv->getRecordStruct(AlarmData); + // simple hack : there is only one channel with alarm data + // TODO: find the right one channel with the alarm id + alarmChannelId = 0; + // printf("AlarmData Length %d\n",rec->length); + if (alarmChannelId < rec->length) { + //uint8_t channel = rec->assign[alarmChannelId].ch; + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); + len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); + } else { + len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + } + // alarm channel processed --> try next inverter + metricsInverterId++; + metricsStep = metricsStateInverter; + break; + + case metricsStateEnd: + default: // end of transmission + len = 0; + break; + } + return len; + }); + request->send(response); + } + + String radioStatistic(String statistic, uint32_t value) { + char type[60], topic[80], val[25]; + snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); + snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str()); + snprintf(val, sizeof(val), "%d", value); + return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n"); + } + + std::pair convertToPromUnits(String shortUnit) { + if(shortUnit == "A") return {"_ampere", "gauge"}; + if(shortUnit == "V") return {"_volt", "gauge"}; + if(shortUnit == "%") return {"_ratio", "gauge"}; + if(shortUnit == "W") return {"_watt", "gauge"}; + if(shortUnit == "Wh") return {"_wattHours", "counter"}; + if(shortUnit == "kWh") return {"_kilowattHours", "counter"}; + if(shortUnit == "°C") return {"_celsius", "gauge"}; + if(shortUnit == "var") return {"_var", "gauge"}; + if(shortUnit == "Hz") return {"_hertz", "gauge"}; + return {"", "gauge"}; + } +#endif + AsyncWebServer mWeb; + AsyncEventSource mEvts; + bool mProtected; + uint32_t mLogoutTimeout; + IApp *mApp; + HMSYSTEM *mSys; + + settings_t *mConfig; + + bool mSerialAddTime; + char mSerialBuf[WEB_SERIAL_BUF_SIZE]; + uint16_t mSerialBufFill; + bool mSerialClientConnnected; + + File mUploadFp; + bool mUploadFail; +}; + +#endif /*__WEB_H__*/ diff --git a/src/wifi/ahoywifi.cpp b/src/wifi/ahoywifi.cpp new file mode 100644 index 00000000..0acdf9e2 --- /dev/null +++ b/src/wifi/ahoywifi.cpp @@ -0,0 +1,413 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#if defined(ESP32) && defined(F) + #undef F + #define F(sl) (sl) +#endif +#include "ahoywifi.h" + +// NTP CONFIG +#define NTP_PACKET_SIZE 48 + +//----------------------------------------------------------------------------- +ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {} + + +//----------------------------------------------------------------------------- +void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) { + mConfig = config; + mUtcTimestamp = utcTimestamp; + mAppWifiCb = cb; + + mStaConn = DISCONNECTED; + mCnt = 0; + mScanActive = false; + + #if defined(ESP8266) + wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1)); + wifiGotIPHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onGotIP, this, std::placeholders::_1)); + wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1)); + #else + WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1)); + #endif + + setupWifi(true); +} + + +//----------------------------------------------------------------------------- +void ahoywifi::setupWifi(bool startAP = false) { + #if !defined(FB_WIFI_OVERRIDDEN) + if(startAP) { + setupAp(); + delay(1000); + } + #endif + #if !defined(AP_ONLY) + if(mConfig->valid) { + #if !defined(FB_WIFI_OVERRIDDEN) + if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0) + setupStation(); + #else + setupStation(); + #endif + } + #endif +} + + +//----------------------------------------------------------------------------- +void ahoywifi::tickWifiLoop() { + #if !defined(AP_ONLY) + if(mStaConn != GOT_IP) { + if (WiFi.softAPgetStationNum() > 0) { // do not reconnect if any AP connection exists + if(mStaConn != IN_AP_MODE) { + mStaConn = IN_AP_MODE; + // first time switch to AP Mode + if (mScanActive) { + WiFi.scanDelete(); + mScanActive = false; + } + DBGPRINTLN(F("AP client connected")); + welcome(mApIp.toString(), ""); + WiFi.mode(WIFI_AP); + mDns.start(53, "*", mApIp); + mAppWifiCb(true); + } + mDns.processNextRequest(); + return; + } + else if(mStaConn == IN_AP_MODE) { + mCnt = 0; + mDns.stop(); + WiFi.mode(WIFI_AP_STA); + mStaConn = DISCONNECTED; + } + mCnt++; + + uint8_t timeout = (mStaConn == DISCONNECTED) ? 10 : 20; // seconds + if (mStaConn == CONNECTED) // connected but no ip + timeout = 20; + + if(!mScanActive && mBSSIDList.empty() && (mStaConn == DISCONNECTED)) { // start scanning APs with the given SSID + DBGPRINT(F("scanning APs with SSID ")); + DBGPRINTLN(String(mConfig->sys.stationSsid)); + mScanCnt = 0; + mScanActive = true; + #if defined(ESP8266) + WiFi.scanNetworks(true, false, 0U, (uint8_t *)mConfig->sys.stationSsid); + #else + WiFi.scanNetworks(true, false, false, 300U, 0U, mConfig->sys.stationSsid); + #endif + return; + } + DBGPRINT(F("reconnect in ")); + DBGPRINT(String(timeout-mCnt)); + DBGPRINTLN(F(" seconds")); + if(mScanActive) { + getBSSIDs(); + if(!mScanActive) // scan completed + if ((mCnt % timeout) < timeout - 2) + mCnt = timeout - 2; + } + if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection + mStaConn = CONNECTING; + WiFi.disconnect(); + + if(mBSSIDList.size() > 0) { // get first BSSID in list + DBGPRINT(F("try to connect to AP with BSSID:")); + uint8_t bssid[6]; + for (int j = 0; j < 6; j++) { + bssid[j] = mBSSIDList.front(); + mBSSIDList.pop_front(); + DBGPRINT(" " + String(bssid[j], HEX)); + } + DBGPRINTLN(""); + WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); + } + else + mStaConn = DISCONNECTED; + + mCnt = 0; + } + } + #endif +} + + +//----------------------------------------------------------------------------- +void ahoywifi::setupAp(void) { + DPRINTLN(DBG_VERBOSE, F("wifi::setupAp")); + + DBGPRINTLN(F("\n---------\nAhoyDTU Info:")); + DBGPRINT(F("Version: ")); + DBGPRINT(String(VERSION_MAJOR)); + DBGPRINT(F(".")); + DBGPRINT(String(VERSION_MINOR)); + DBGPRINT(F(".")); + DBGPRINTLN(String(VERSION_PATCH)); + DBGPRINT(F("Github Hash: ")); + DBGPRINTLN(String(AUTO_GIT_HASH)); + + DBGPRINT(F("\n---------\nAP MODE\nSSID: ")); + DBGPRINTLN(WIFI_AP_SSID); + DBGPRINT(F("PWD: ")); + DBGPRINTLN(WIFI_AP_PWD); + DBGPRINT(F("IP Address: http://")); + DBGPRINTLN(mApIp.toString()); + DBGPRINTLN(F("---------\n")); + + WiFi.mode(WIFI_AP_STA); + WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0)); + WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PWD); +} + + +//----------------------------------------------------------------------------- +void ahoywifi::setupStation(void) { + DPRINTLN(DBG_VERBOSE, F("wifi::setupStation")); + if(mConfig->sys.ip.ip[0] != 0) { + IPAddress ip(mConfig->sys.ip.ip); + IPAddress mask(mConfig->sys.ip.mask); + IPAddress dns1(mConfig->sys.ip.dns1); + IPAddress dns2(mConfig->sys.ip.dns2); + IPAddress gateway(mConfig->sys.ip.gateway); + if(!WiFi.config(ip, gateway, mask, dns1, dns2)) + DPRINTLN(DBG_ERROR, F("failed to set static IP!")); + } + mBSSIDList.clear(); + if(String(mConfig->sys.deviceName) != "") + WiFi.hostname(mConfig->sys.deviceName); + WiFi.mode(WIFI_AP_STA); + + + DBGPRINT(F("connect to network '")); + DBGPRINT(mConfig->sys.stationSsid); + DBGPRINTLN(F("' ...")); +} + + +//----------------------------------------------------------------------------- +bool ahoywifi::getNtpTime(void) { + if(GOT_IP != mStaConn) + return false; + + IPAddress timeServer; + uint8_t buf[NTP_PACKET_SIZE]; + uint8_t retry = 0; + + if (WiFi.hostByName(mConfig->ntp.addr, timeServer) != 1) + return false; + + mUdp.begin(mConfig->ntp.port); + sendNTPpacket(timeServer); + + while(retry++ < 5) { + int wait = 150; + while(--wait) { + if(NTP_PACKET_SIZE <= mUdp.parsePacket()) { + uint64_t secsSince1900; + mUdp.read(buf, NTP_PACKET_SIZE); + secsSince1900 = (buf[40] << 24); + secsSince1900 |= (buf[41] << 16); + secsSince1900 |= (buf[42] << 8); + secsSince1900 |= (buf[43] ); + + *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time + DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC"); + return true; + } else + delay(10); + } + } + + DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed")); + return false; +} + + +//----------------------------------------------------------------------------- +void ahoywifi::sendNTPpacket(IPAddress& address) { + //DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket")); + uint8_t buf[NTP_PACKET_SIZE] = {0}; + + buf[0] = B11100011; // LI, Version, Mode + buf[1] = 0; // Stratum + buf[2] = 6; // Max Interval between messages in seconds + buf[3] = 0xEC; // Clock Precision + // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset + buf[12] = 49; // four-byte reference ID identifying + buf[13] = 0x4E; + buf[14] = 49; + buf[15] = 52; + + mUdp.beginPacket(address, 123); // NTP request, port 123 + mUdp.write(buf, NTP_PACKET_SIZE); + mUdp.endPacket(); +} + +//----------------------------------------------------------------------------- +void ahoywifi::sortRSSI(int *sort, int n) { + for (int i = 0; i < n; i++) + sort[i] = i; + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n; j++) + if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) + std::swap(sort[i], sort[j]); +} + +//----------------------------------------------------------------------------- +void ahoywifi::scanAvailNetworks(void) { + if(!mScanActive) { + mScanActive = true; + if(WIFI_AP == WiFi.getMode()) + WiFi.mode(WIFI_AP_STA); + WiFi.scanNetworks(true); + } +} + +//----------------------------------------------------------------------------- +void ahoywifi::getAvailNetworks(JsonObject obj) { + JsonArray nets = obj.createNestedArray("networks"); + + int n = WiFi.scanComplete(); + if (n < 0) + return; + if(n > 0) { + int sort[n]; + sortRSSI(&sort[0], n); + for (int i = 0; i < n; ++i) { + nets[i]["ssid"] = WiFi.SSID(sort[i]); + nets[i]["rssi"] = WiFi.RSSI(sort[i]); + } + } + mScanActive = false; + WiFi.scanDelete(); + if(mStaConn == IN_AP_MODE) + WiFi.mode(WIFI_AP); +} + +//----------------------------------------------------------------------------- +void ahoywifi::getBSSIDs() { + int n = WiFi.scanComplete(); + if (n < 0) { + mScanCnt++; + if (mScanCnt < 20) + return; + } + if(n > 0) { + mBSSIDList.clear(); + int sort[n]; + sortRSSI(&sort[0], n); + for (int i = 0; i < n; i++) { + DBGPRINT("BSSID " + String(i) + ":"); + uint8_t *bssid = WiFi.BSSID(sort[i]); + for (int j = 0; j < 6; j++){ + DBGPRINT(" " + String(bssid[j], HEX)); + mBSSIDList.push_back(bssid[j]); + } + DBGPRINTLN(""); + } + } + mScanActive = false; + WiFi.scanDelete(); +} + +//----------------------------------------------------------------------------- +void ahoywifi::connectionEvent(WiFiStatus_t status) { + DPRINTLN(DBG_INFO, "connectionEvent"); + + switch(status) { + case CONNECTED: + if(mStaConn != CONNECTED) { + mStaConn = CONNECTED; + DBGPRINTLN(F("\n[WiFi] Connected")); + } + break; + + case GOT_IP: + mStaConn = GOT_IP; + if (mScanActive) { // maybe another scan has started + WiFi.scanDelete(); + mScanActive = false; + } + welcome(WiFi.localIP().toString(), F(" (Station)")); + WiFi.softAPdisconnect(); + WiFi.mode(WIFI_STA); + DBGPRINTLN(F("[WiFi] AP disabled")); + delay(100); + mAppWifiCb(true); + break; + + case DISCONNECTED: + if(mStaConn != CONNECTING) { + mStaConn = DISCONNECTED; + mCnt = 5; // try to reconnect in 5 sec + setupWifi(); // reconnect with AP / Station setup + mAppWifiCb(false); + DPRINTLN(DBG_INFO, "[WiFi] Connection Lost"); + } + break; + + default: + break; + } +} + + +//----------------------------------------------------------------------------- +#if defined(ESP8266) + //------------------------------------------------------------------------- + void ahoywifi::onConnect(const WiFiEventStationModeConnected& event) { + connectionEvent(CONNECTED); + } + + //------------------------------------------------------------------------- + void ahoywifi::onGotIP(const WiFiEventStationModeGotIP& event) { + connectionEvent(GOT_IP); + } + + //------------------------------------------------------------------------- + void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) { + connectionEvent(DISCONNECTED); + } + +#else + //------------------------------------------------------------------------- + void ahoywifi::onWiFiEvent(WiFiEvent_t event) { + DBGPRINT(F("Wifi event: ")); + DBGPRINTLN(String(event)); + + switch(event) { + case SYSTEM_EVENT_STA_CONNECTED: + connectionEvent(CONNECTED); + break; + + case SYSTEM_EVENT_STA_GOT_IP: + connectionEvent(GOT_IP); + break; + + case SYSTEM_EVENT_STA_DISCONNECTED: + connectionEvent(DISCONNECTED); + break; + + default: + break; + } + } +#endif + + +//----------------------------------------------------------------------------- +void ahoywifi::welcome(String ip, String mode) { + DBGPRINTLN(F("\n\n--------------------------------")); + DBGPRINTLN(F("Welcome to AHOY!")); + DBGPRINT(F("\npoint your browser to http://")); + DBGPRINT(ip); + DBGPRINTLN(mode); + DBGPRINTLN(F("to configure your device")); + DBGPRINTLN(F("--------------------------------\n")); +} diff --git a/src/wifi/ahoywifi.h b/src/wifi/ahoywifi.h new file mode 100644 index 00000000..3a3a35a8 --- /dev/null +++ b/src/wifi/ahoywifi.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __AHOYWIFI_H__ +#define __AHOYWIFI_H__ + +#include "../utils/dbg.h" +#include +#include +#include +#include "ESPAsyncWebServer.h" + +#include "../config/settings.h" + +class app; + +class ahoywifi { + public: + typedef std::function appWifiCb; + + ahoywifi(); + + + void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb); + void tickWifiLoop(void); + bool getNtpTime(void); + void scanAvailNetworks(void); + void getAvailNetworks(JsonObject obj); + + private: + typedef enum WiFiStatus { + DISCONNECTED = 0, + CONNECTING, + CONNECTED, + IN_AP_MODE, + GOT_IP + } WiFiStatus_t; + + void setupWifi(bool startAP); + void setupAp(void); + void setupStation(void); + void sendNTPpacket(IPAddress& address); + void sortRSSI(int *sort, int n); + void getBSSIDs(void); + void connectionEvent(WiFiStatus_t status); + #if defined(ESP8266) + void onConnect(const WiFiEventStationModeConnected& event); + void onGotIP(const WiFiEventStationModeGotIP& event); + void onDisconnect(const WiFiEventStationModeDisconnected& event); + #else + void onWiFiEvent(WiFiEvent_t event); + #endif + void welcome(String ip, String mode); + + + settings_t *mConfig; + appWifiCb mAppWifiCb; + + DNSServer mDns; + IPAddress mApIp; + WiFiUDP mUdp; // for time server + #if defined(ESP8266) + WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler; + #endif + + WiFiStatus_t mStaConn; + uint8_t mCnt; + uint32_t *mUtcTimestamp; + + uint8_t mScanCnt; + bool mScanActive; + std::list mBSSIDList; +}; + +#endif /*__AHOYWIFI_H__*/ diff --git a/tools/cases/EKD_ESPNRF_Case/EKDESPNRFCase.jpg b/tools/cases/EKD_ESPNRF_Case/EKDESPNRFCase.jpg new file mode 100644 index 00000000..8b049509 Binary files /dev/null and b/tools/cases/EKD_ESPNRF_Case/EKDESPNRFCase.jpg differ diff --git a/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Body.3mf b/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Body.3mf new file mode 100644 index 00000000..b637edc9 Binary files /dev/null and b/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Body.3mf differ diff --git a/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Lid.3mf b/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Lid.3mf new file mode 100644 index 00000000..e8c13870 Binary files /dev/null and b/tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Lid.3mf differ diff --git a/tools/cases/EKD_ESPNRF_Case/Readme.md b/tools/cases/EKD_ESPNRF_Case/Readme.md new file mode 100644 index 00000000..898224ed --- /dev/null +++ b/tools/cases/EKD_ESPNRF_Case/Readme.md @@ -0,0 +1,30 @@ +# EKD ESPNRF Case + + + + EKD ESPNRF Case + + +### Print Details: +- Print with 0.2 mm Layers +- use 100% infill +- no supports needed + +### Things needed: +- 3D Printer +- Wemos D1 Mini (format style) +- NRF24L01+ Board +- ~ 15cm wire +- Soldering Iron + Solder +- Suction pump to free the NRF Board from the pins. +(Solder wick works too but i do not recommend =) +- If you want to go for a wall mounted device, add some screws. + + +Unsolder the Pins from the NRF Board and use short wires instead. I went this way to keep the design as flat as possible. + + EKD ESPNRF Case + +If you got questions or need help feel free to ask on discord. +or find me on github.com/subdancer +Cheers. \ No newline at end of file diff --git a/tools/esp8266/.vscode/settings.json b/tools/esp8266/.vscode/settings.json deleted file mode 100644 index a04cb695..00000000 --- a/tools/esp8266/.vscode/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - // identify that settings is loaded - "workbench.colorCustomizations": { - "editorLineNumber.foreground": "#00ff00" - }, - - "editor.wordWrap": "off", - "files.eol" : "\n", - "files.trimTrailingWhitespace" : true, - - "diffEditor.ignoreTrimWhitespace": true, - "files.autoSave": "afterDelay", - - "editor.tabSize": 4, - "editor.insertSpaces": true, - // `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. - // Set to false to keep the values you've explicitly set, above. - "editor.detectIndentation": false, - - // https://clang.llvm.org/docs/ClangFormatStyleOptions.html - "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}", -} \ No newline at end of file diff --git a/tools/esp8266/CHANGES.md b/tools/esp8266/CHANGES.md deleted file mode 100644 index 85cd7aea..00000000 --- a/tools/esp8266/CHANGES.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -- v0.5.17 - * Bug fix for 1 channel inverters (HM300, HM400) see #246 - * Bug fix for read back the active power limit from inverter #243 (before version 0.5.16 the reported limit was just a copy of the user set point, now it is the actual value which the inverter uses) - * Update the [user manual](https://github.com/grindylow/ahoy/blob/main/tools/esp8266/User_Manual.md); added section aobut the published data on mqtt; section about zero export control; added section about code implementation command queue - * Added tx-Id number to packet payload struct. (eg. can be 0x95 or 0xD1) --> less messages fails and faster handling of changing power limit diff --git a/tools/esp8266/CircularBuffer.h b/tools/esp8266/CircularBuffer.h deleted file mode 100644 index 65c9e768..00000000 --- a/tools/esp8266/CircularBuffer.h +++ /dev/null @@ -1,161 +0,0 @@ -/* - CircularBuffer - An Arduino circular buffering library for arbitrary types. - - Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef CircularBuffer_h -#define CircularBuffer_h - -#if defined(ESP8266) || defined(ESP32) -#define DISABLE_IRQ noInterrupts() -#define RESTORE_IRQ interrupts() -#else -#define DISABLE_IRQ \ - uint8_t sreg = SREG; \ - cli(); - -#define RESTORE_IRQ \ - SREG = sreg; -#endif - -template -class CircularBuffer { - - typedef BUFFERTYPE BufferType; - BufferType Buffer[BUFFERSIZE]; - - public: - CircularBuffer() : m_buff(Buffer) { - m_size = BUFFERSIZE; - clear(); - } - - /** Clear all entries in the circular buffer. */ - void clear(void) - { - m_front = 0; - m_fill = 0; - } - - /** Test if the circular buffer is empty */ - inline bool empty(void) const - { - return !m_fill; - } - - /** Return the number of records stored in the buffer */ - inline uint8_t available(void) const - { - return m_fill; - } - - /** Test if the circular buffer is full */ - inline bool full(void) const - { - return m_fill == m_size; - } - - inline uint8_t getFill(void) const { - return m_fill; - } - - /** Aquire record on front of the buffer, for writing. - * After filling the record, it has to be pushed to actually - * add it to the buffer. - * @return Pointer to record, or NULL when buffer is full. - */ - BUFFERTYPE* getFront(void) const - { - DISABLE_IRQ; - BUFFERTYPE* f = NULL; - if (!full()) - f = get(m_front); - RESTORE_IRQ; - return f; - } - - /** Push record to front of the buffer - * @param record Record to push. If record was aquired previously (using getFront) its - * data will not be copied as it is already present in the buffer. - * @return True, when record was pushed successfully. - */ - bool pushFront(BUFFERTYPE* record) - { - bool ok = false; - DISABLE_IRQ; - if (!full()) - { - BUFFERTYPE* f = get(m_front); - if (f != record) - *f = *record; - m_front = (m_front+1) % m_size; - m_fill++; - ok = true; - } - RESTORE_IRQ; - return ok; - } - - /** Aquire record on back of the buffer, for reading. - * After reading the record, it has to be pop'ed to actually - * remove it from the buffer. - * @return Pointer to record, or NULL when buffer is empty. - */ - BUFFERTYPE* getBack(void) const - { - BUFFERTYPE* b = NULL; - DISABLE_IRQ; - if (!empty()) - b = get(back()); - RESTORE_IRQ; - return b; - } - - /** Remove record from back of the buffer. - * @return True, when record was pop'ed successfully. - */ - bool popBack(void) - { - bool ok = false; - DISABLE_IRQ; - if (!empty()) - { - m_fill--; - ok = true; - } - RESTORE_IRQ; - return ok; - } - - protected: - inline BUFFERTYPE * get(const uint8_t idx) const - { - return &(m_buff[idx]); - } - inline uint8_t back(void) const - { - return (m_front - m_fill + m_size) % m_size; - } - - uint8_t m_size; // Total number of records that can be stored in the buffer. - BUFFERTYPE* const m_buff; - volatile uint8_t m_front; // Index of front element (not pushed yet). - volatile uint8_t m_fill; // Amount of records currently pushed. -}; - -#endif // CircularBuffer_h diff --git a/tools/esp8266/ahoywifi.cpp b/tools/esp8266/ahoywifi.cpp deleted file mode 100644 index 852d792e..00000000 --- a/tools/esp8266/ahoywifi.cpp +++ /dev/null @@ -1,260 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif -#include "ahoywifi.h" - - -// NTP CONFIG -#define NTP_PACKET_SIZE 48 - - -//----------------------------------------------------------------------------- -ahoywifi::ahoywifi(app *main, sysConfig_t *sysCfg, config_t *config) { - mMain = main; - mSysCfg = sysCfg; - mConfig = config; - - mDns = new DNSServer(); - mUdp = new WiFiUDP(); - - mWifiStationTimeout = 10; - wifiWasEstablished = false; - mNextTryTs = 0; - mApLastTick = 0; - mApActive = false; -} - - -//----------------------------------------------------------------------------- -void ahoywifi::setup(uint32_t timeout, bool settingValid) { - mWifiStationTimeout = timeout; - #ifndef AP_ONLY - if(false == mApActive) - mApActive = setupStation(mWifiStationTimeout); - #endif - - if(!settingValid) { - DPRINTLN(DBG_WARN, F("your settings are not valid! check [IP]/setup")); - mApActive = true; - mApLastTick = millis(); - mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); - setupAp(WIFI_AP_SSID, WIFI_AP_PWD); - } - else { - DPRINTLN(DBG_INFO, F("\n\n----------------------------------------")); - DPRINTLN(DBG_INFO, F("Welcome to AHOY!")); - DPRINT(DBG_INFO, F("\npoint your browser to http://")); - if(mApActive) - DBGPRINTLN(F("192.168.1.1")); - else - DBGPRINTLN(WiFi.localIP().toString()); - DPRINTLN(DBG_INFO, F("to configure your device")); - DPRINTLN(DBG_INFO, F("----------------------------------------\n")); - } -} - - -//----------------------------------------------------------------------------- -bool ahoywifi::loop(void) { - if(mApActive) { - mDns->processNextRequest(); -#ifndef AP_ONLY - if(mMain->checkTicker(&mNextTryTs, (WIFI_AP_ACTIVE_TIME * 1000))) { - mApActive = setupStation(mWifiStationTimeout); - if(mApActive) { - if(strlen(WIFI_AP_PWD) < 8) - DPRINTLN(DBG_ERROR, F("password must be at least 8 characters long")); - mApLastTick = millis(); - mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); - setupAp(WIFI_AP_SSID, WIFI_AP_PWD); - } - } - else { - if(millis() - mApLastTick > 10000) { - uint8_t cnt = WiFi.softAPgetStationNum(); - if(cnt > 0) { - DPRINTLN(DBG_INFO, String(cnt) + F(" client connected, resetting AP timeout")); - mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000)); - } - mApLastTick = millis(); - DPRINTLN(DBG_INFO, F("AP will be closed in ") + String((mNextTryTs - mApLastTick) / 1000) + F(" seconds")); - } - } -#endif - } - - if((WiFi.status() != WL_CONNECTED) && wifiWasEstablished) { - if(!mApActive) { - DPRINTLN(DBG_INFO, "[WiFi]: Connection Lost"); - mApActive = setupStation(mWifiStationTimeout); - } - } - - return mApActive; -} - - -//----------------------------------------------------------------------------- -void ahoywifi::setupAp(const char *ssid, const char *pwd) { - DPRINTLN(DBG_VERBOSE, F("app::setupAp")); - IPAddress apIp(192, 168, 1, 1); - - DPRINTLN(DBG_INFO, F("\n---------\nAP MODE\nSSID: ") - + String(ssid) + F("\nPWD: ") - + String(pwd) + F("\nActive for: ") - + String(WIFI_AP_ACTIVE_TIME) + F(" seconds") - + F("\n---------\n")); - DPRINTLN(DBG_DEBUG, String(mNextTryTs)); - - WiFi.mode(WIFI_AP); - WiFi.softAPConfig(apIp, apIp, IPAddress(255, 255, 255, 0)); - WiFi.softAP(ssid, pwd); - - mDns->start(53, "*", apIp); -} - - -//----------------------------------------------------------------------------- -bool ahoywifi::setupStation(uint32_t timeout) { - DPRINTLN(DBG_VERBOSE, F("app::setupStation")); - int32_t cnt; - bool startAp = false; - - if(timeout >= 3) - cnt = (timeout - 3) / 2 * 10; - else { - timeout = 1; - cnt = 1; - } - - WiFi.mode(WIFI_STA); - WiFi.begin(mSysCfg->stationSsid, mSysCfg->stationPwd); - if(String(mSysCfg->deviceName) != "") - WiFi.hostname(mSysCfg->deviceName); - - delay(2000); - DPRINTLN(DBG_INFO, F("connect to network '") + String(mSysCfg->stationSsid) + F("' ...")); - while (WiFi.status() != WL_CONNECTED) { - delay(100); - if(cnt % 40 == 0) - Serial.println("."); - else - Serial.print("."); - - if(timeout > 0) { // limit == 0 -> no limit - if(--cnt <= 0) { - if(WiFi.status() != WL_CONNECTED) { - startAp = true; - WiFi.disconnect(); - } - delay(100); - break; - } - } - } - Serial.println("."); - - if(false == startAp) - wifiWasEstablished = true; - - delay(1000); - return startAp; -} - - -//----------------------------------------------------------------------------- -bool ahoywifi::getApActive(void) { - return mApActive; -} - -//----------------------------------------------------------------------------- -time_t ahoywifi::getNtpTime(void) { - //DPRINTLN(DBG_VERBOSE, F("wifi::getNtpTime")); - time_t date = 0; - IPAddress timeServer; - uint8_t buf[NTP_PACKET_SIZE]; - uint8_t retry = 0; - - WiFi.hostByName(mConfig->ntpAddr, timeServer); - mUdp->begin(mConfig->ntpPort); - - sendNTPpacket(timeServer); - - while(retry++ < 5) { - int wait = 150; - while(--wait) { - if(NTP_PACKET_SIZE <= mUdp->parsePacket()) { - uint64_t secsSince1900; - mUdp->read(buf, NTP_PACKET_SIZE); - secsSince1900 = (buf[40] << 24); - secsSince1900 |= (buf[41] << 16); - secsSince1900 |= (buf[42] << 8); - secsSince1900 |= (buf[43] ); - - date = secsSince1900 - 2208988800UL; // UTC time - break; - } - else - delay(10); - } - } - - return date; -} - - -//----------------------------------------------------------------------------- -void ahoywifi::scanAvailNetworks(void) { - int n = WiFi.scanComplete(); - if(n == -2) - WiFi.scanNetworks(true); -} - - -//----------------------------------------------------------------------------- -void ahoywifi::getAvailNetworks(JsonObject obj) { - JsonArray nets = obj.createNestedArray("networks"); - - int n = WiFi.scanComplete(); - if(n > 0) { - int sort[n]; - for (int i = 0; i < n; i++) - sort[i] = i; - for (int i = 0; i < n; i++) - for (int j = i + 1; j < n; j++) - if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) - std::swap(sort[i], sort[j]); - for (int i = 0; i < n; ++i) { - nets[i]["ssid"] = WiFi.SSID(sort[i]); - nets[i]["rssi"] = WiFi.RSSI(sort[i]); - } - WiFi.scanDelete(); - } -} - - -//----------------------------------------------------------------------------- -void ahoywifi::sendNTPpacket(IPAddress& address) { - //DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket")); - uint8_t buf[NTP_PACKET_SIZE] = {0}; - - buf[0] = B11100011; // LI, Version, Mode - buf[1] = 0; // Stratum - buf[2] = 6; // Max Interval between messages in seconds - buf[3] = 0xEC; // Clock Precision - // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset - buf[12] = 49; // four-byte reference ID identifying - buf[13] = 0x4E; - buf[14] = 49; - buf[15] = 52; - - mUdp->beginPacket(address, 123); // NTP request, port 123 - mUdp->write(buf, NTP_PACKET_SIZE); - mUdp->endPacket(); -} diff --git a/tools/esp8266/ahoywifi.h b/tools/esp8266/ahoywifi.h deleted file mode 100644 index cf3c779d..00000000 --- a/tools/esp8266/ahoywifi.h +++ /dev/null @@ -1,53 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __AHOYWIFI_H__ -#define __AHOYWIFI_H__ - -#include "dbg.h" - -// NTP -#include -#include -#include - -#include "defines.h" - -#include "app.h" - -class app; - -class ahoywifi { - public: - ahoywifi(app *main, sysConfig_t *sysCfg, config_t *config); - ~ahoywifi() {} - - void setup(uint32_t timeout, bool settingValid); - bool loop(void); - void setupAp(const char *ssid, const char *pwd); - bool setupStation(uint32_t timeout); - bool getApActive(void); - time_t getNtpTime(void); - void scanAvailNetworks(void); - void getAvailNetworks(JsonObject obj); - - private: - void sendNTPpacket(IPAddress& address); - - config_t *mConfig; - sysConfig_t *mSysCfg; - app *mMain; - - DNSServer *mDns; - WiFiUDP *mUdp; // for time server - - uint32_t mWifiStationTimeout; - uint32_t mNextTryTs; - uint32_t mApLastTick; - bool mApActive; - bool wifiWasEstablished; -}; - -#endif /*__AHOYWIFI_H__*/ diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp deleted file mode 100644 index 31607c0f..00000000 --- a/tools/esp8266/app.cpp +++ /dev/null @@ -1,907 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#if defined(ESP32) && defined(F) -#undef F -#define F(sl) (sl) -#endif - -#include "app.h" - -#include - -//----------------------------------------------------------------------------- -app::app() { - Serial.begin(115200); - DPRINTLN(DBG_VERBOSE, F("app::app")); - mEep = new eep(); - mWifi = new ahoywifi(this, &mSysConfig, &mConfig); - - resetSystem(); - loadDefaultConfig(); - - mSys = new HmSystemType(); - mSys->enableDebug(); - mShouldReboot = false; -} - -//----------------------------------------------------------------------------- -void app::setup(uint32_t timeout) { - DPRINTLN(DBG_VERBOSE, F("app::setup")); - - mWifiSettingsValid = checkEEpCrc(ADDR_START, ADDR_WIFI_CRC, ADDR_WIFI_CRC); - mSettingsValid = checkEEpCrc(ADDR_START_SETTINGS, ((ADDR_NEXT) - (ADDR_START_SETTINGS)), ADDR_SETTINGS_CRC); - loadEEpconfig(); - - mWifi->setup(timeout, mWifiSettingsValid); - -#ifndef AP_ONLY - setupMqtt(); -#endif - mSys->setup(mConfig.amplifierPower, mConfig.pinIrq, mConfig.pinCe, mConfig.pinCs); - - mWebInst = new web(this, &mSysConfig, &mConfig, &mStat, mVersion); - mWebInst->setup(); -} - -//----------------------------------------------------------------------------- -void app::loop(void) { - DPRINTLN(DBG_VERBOSE, F("app::loop")); - - bool apActive = mWifi->loop(); - mWebInst->loop(); - - if (millis() - mPrevMillis >= 1000) { - mPrevMillis += 1000; - mUptimeSecs++; - if (0 != mUtcTimestamp) - mUtcTimestamp++; - } - - if (checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) { - if (!apActive) - mUpdateNtp = true; - } - - if (mUpdateNtp) { - mUpdateNtp = false; - mUtcTimestamp = mWifi->getNtpTime(); - DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC")); - } - - if (mFlagSendDiscoveryConfig) { - mFlagSendDiscoveryConfig = false; - sendMqttDiscoveryConfig(); - } - - if (mShouldReboot) { - DPRINTLN(DBG_INFO, F("Rebooting...")); - ESP.restart(); - } - - mSys->Radio.loop(); - - yield(); - - if (checkTicker(&mRxTicker, 5)) { - bool rxRdy = mSys->Radio.switchRxCh(); - - if (!mSys->BufCtrl.empty()) { - uint8_t len; - packet_t *p = mSys->BufCtrl.getBack(); - - if (mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) { - // process buffer only on first occurrence - if (mConfig.serialDebug) { - DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | "); - mSys->Radio.dumpBuf(NULL, p->packet, len); - } - - mStat.frmCnt++; - - if (0 != len) { - Inverter<> *iv = mSys->findInverter(&p->packet[1]); - if ((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES))) { // response from get information command - mPayload[iv->id].txId = p->packet[0]; - DPRINTLN(DBG_DEBUG, F("Response from info request received")); - uint8_t *pid = &p->packet[9]; - if (*pid == 0x00) { - DPRINT(DBG_DEBUG, F("fragment number zero received and ignored")); - } else { - DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); - if ((*pid & 0x7F) < 5) { - memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11); - mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11; - } - - if ((*pid & ALL_FRAMES) == ALL_FRAMES) { - // Last packet - if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) { - mPayload[iv->id].maxPackId = (*pid & 0x7f); - if (*pid > 0x81) - mLastPacketId = *pid; - } - } - } - } - if ((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES))) { // response from dev control command - DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); - - mPayload[iv->id].txId = p->packet[0]; - iv->devControlRequest = false; - - if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { - String msg = (p->packet[10] == 0x00 && p->packet[11] == 0x00) ? "" : "NOT "; - DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); - } - iv->devControlCmd = Init; - } - } - } - mSys->BufCtrl.popBack(); - } - yield(); - - if (rxRdy) { - processPayload(true); - } - } - - if (mMqttActive) - mMqtt.loop(); - - if (checkTicker(&mTicker, 1000)) { - if (mUtcTimestamp > 946684800 && mConfig.sunLat && mConfig.sunLon && (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 != (mLatestSunTimestamp + mCalculatedTimezoneOffset) / 86400) { // update on reboot or midnight - if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight - mCalculatedTimezoneOffset = (int8_t)((mConfig.sunLon >= 0 ? mConfig.sunLon + 7.5 : mConfig.sunLon - 7.5) / 15) * 3600; - } - calculateSunriseSunset(); - mLatestSunTimestamp = mUtcTimestamp; - } - - if ((++mMqttTicker >= mMqttInterval) && (mMqttInterval != 0xffff) && mMqttActive) { - mMqttTicker = 0; - sendMqtt(); - } - - if (mConfig.serialShowIv) { - if (++mSerialTicker >= mConfig.serialInterval) { - mSerialTicker = 0; - char topic[30], val[10]; - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv->isAvailable(mUtcTimestamp, rec)) { - DPRINTLN(DBG_INFO, "Inverter: " + String(id)); - for (uint8_t i = 0; i < rec->length; i++) { - if (0.0f != iv->getValue(i, rec)) { - snprintf(topic, 30, "%s/ch%d/%s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(val, 10, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec)); - DPRINTLN(DBG_INFO, String(topic) + ": " + String(val)); - } - yield(); - } - DPRINTLN(DBG_INFO, ""); - } - } - } - } - } - - if (++mSendTicker >= mConfig.sendInterval) { - mSendTicker = 0; - - if (mUtcTimestamp > 946684800 && (!mConfig.sunDisNightCom || !mLatestSunTimestamp || (mUtcTimestamp >= mSunrise && mUtcTimestamp <= mSunset))) { // Timestamp is set and (inverter communication only during the day if the option is activated and sunrise/sunset is set) - if (mConfig.serialDebug) - DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX)); - - if (!mSys->BufCtrl.empty()) { - if (mConfig.serialDebug) - DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill())); - } - - int8_t maxLoop = MAX_NUM_INVERTERS; - Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId); - do { - // if(NULL != iv) - // mPayload[iv->id].requested = false; - mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; - iv = mSys->getInverterByPos(mSendLastIvId); - } while ((NULL == iv) && ((maxLoop--) > 0)); - - if (NULL != iv) { - if (!mPayload[iv->id].complete) - processPayload(false); - - if (!mPayload[iv->id].complete) { - if (0 == mPayload[iv->id].maxPackId) - mStat.rxFailNoAnser++; - else - mStat.rxFail++; - - iv->setQueuedCmdFinished(); // command failed - if (mConfig.serialDebug) - DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); - if (mConfig.serialDebug) { - DPRINT(DBG_INFO, F("Inverter #") + String(iv->id) + " "); - DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")"); - } - } - - resetPayload(iv); - mPayload[iv->id].requested = true; - - yield(); - if (mConfig.serialDebug) { - DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status())); - DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX)); - } - - if (iv->devControlRequest) { - if (mConfig.serialDebug) - DPRINTLN(DBG_INFO, F("Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0])); - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit); - mPayload[iv->id].txCmd = iv->devControlCmd; - iv->clearCmdQueue(); - iv->enqueCommand(SystemConfigPara); - } else { - uint8_t cmd = iv->getQueuedCmd(); - mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex); - mPayload[iv->id].txCmd = cmd; - mRxTicker = 0; - } - } - } else if (mConfig.serialDebug) - DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!")); - yield(); - } - } -} - -//----------------------------------------------------------------------------- -void app::handleIntr(void) { - DPRINTLN(DBG_VERBOSE, F("app::handleIntr")); - mSys->Radio.handleIntr(); -} - -//----------------------------------------------------------------------------- -bool app::buildPayload(uint8_t id) { - DPRINTLN(DBG_VERBOSE, F("app::buildPayload")); - uint16_t crc = 0xffff, crcRcv = 0x0000; - if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) - mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if (mPayload[id].len[i] > 0) { - if (i == (mPayload[id].maxPackId - 1)) { - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); - crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); - } else - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); - } - yield(); - } - - return (crc == crcRcv) ? true : false; -} - -//----------------------------------------------------------------------------- -void app::processPayload(bool retransmit) { - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { - // no processing needed if txId is not 0x95 - // DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX)); - mPayload[iv->id].complete = true; - } - - if (!mPayload[iv->id].complete) { - if (!buildPayload(iv->id)) { // payload not complete - if ((mPayload[iv->id].requested) && (retransmit)) { - if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { - // This is required to prevent retransmissions without answer. - DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); - mPayload[iv->id].retransmits = mConfig.maxRetransPerPyld; - } else { - if (mPayload[iv->id].retransmits < mConfig.maxRetransPerPyld) { - mPayload[iv->id].retransmits++; - if (mPayload[iv->id].maxPackId != 0) { - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { - if (mPayload[iv->id].len[i] == 0) { - if (mConfig.serialDebug) - DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i + 1) + F(" missing: Request Retransmit")); - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); - break; // only retransmit one frame per loop - } - yield(); - } - } else { - if (mConfig.serialDebug) - DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit")); - if (0x00 != mLastPacketId) - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true); - else { - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex); - } - } - mSys->Radio.switchRxCh(100); - } - } - } - } else { // payload complete - DPRINTLN(DBG_INFO, F("procPyld: cmd: ") + String(mPayload[iv->id].txCmd)); - DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); - DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); - record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser - mPayload[iv->id].complete = true; - - uint8_t payload[128]; - uint8_t payloadLen = 0; - - memset(payload, 0, 128); - - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { - memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); - payloadLen += (mPayload[iv->id].len[i]); - yield(); - } - payloadLen -= 2; - - if (mConfig.serialDebug) { - DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); - mSys->Radio.dumpBuf(NULL, payload, payloadLen); - } - - if (NULL == rec) { - DPRINTLN(DBG_ERROR, F("record is NULL!")); - } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { - if (mPayload[iv->id].txId == (TX_REQ_INFO + 0x80)) - mStat.rxSuccess++; - - rec->ts = mPayload[iv->id].ts; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, payload, rec); - yield(); - } - iv->doCalculations(); - - mMqttSendList.push(mPayload[iv->id].txCmd); - } else { - DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); - mStat.rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - - yield(); - - } - - // ist MQTT aktiviert und es wurden Daten vom einem oder mehreren WR aufbereitet - // dann die den mMqttTicker auf mMqttIntervall -2 setzen, also - // MQTT aussenden in 2 sek aktivieren - if ((mMqttInterval != 0xffff) && (!mMqttSendList.empty())) { - mMqttTicker = mMqttInterval - 2; - } -} - -//----------------------------------------------------------------------------- -void app::cbMqtt(char *topic, byte *payload, unsigned int length) { - // callback handling on subscribed devcontrol topic - DPRINTLN(DBG_INFO, F("app::cbMqtt")); - // subcribed topics are mTopic + "/devcontrol/#" where # is / - // eg. mypvsolar/devcontrol/1/11 with payload "400" --> inverter 1 active power limit 400 Watt - const char *token = strtok(topic, "/"); - while (token != NULL) { - if (strcmp(token, "devcontrol") == 0) { - token = strtok(NULL, "/"); - uint8_t iv_id = std::stoi(token); - - if (iv_id >= 0 && iv_id <= MAX_NUM_INVERTERS) { - Inverter<> *iv = this->mSys->getInverterByPos(iv_id); - if (NULL != iv) { - if (!iv->devControlRequest) { // still pending - token = strtok(NULL, "/"); - - switch (std::stoi(token)) { - // Active Power Control - case ActivePowerContr: - token = strtok(NULL, "/"); // get ControlMode aka "PowerPF.Desc" in DTU-Pro Code from topic string - if (token == NULL) // default via mqtt ommit the LimitControlMode - iv->powerLimit[1] = AbsolutNonPersistent; - else - iv->powerLimit[1] = std::stoi(token); - if (length <= 5) { // if (std::stoi((char*)payload) > 0) more error handling powerlimit needed? - if (iv->powerLimit[1] >= AbsolutNonPersistent && iv->powerLimit[1] <= RelativPersistent) { - iv->devControlCmd = ActivePowerContr; - iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); // THX to @silversurfer - /*if (iv->powerLimit[1] & 0x0001) - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%")); - else - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));*/ - - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + String(iv->powerLimit[1] & 0x0001) ? F("%") : F("W")); - } - iv->devControlRequest = true; - } else { - DPRINTLN(DBG_INFO, F("Invalid mqtt payload recevied: ") + String((char *)payload)); - } - break; - - // Turn On - case TurnOn: - iv->devControlCmd = TurnOn; - DPRINTLN(DBG_INFO, F("Turn on inverter ") + String(iv->id)); - iv->devControlRequest = true; - break; - - // Turn Off - case TurnOff: - iv->devControlCmd = TurnOff; - DPRINTLN(DBG_INFO, F("Turn off inverter ") + String(iv->id)); - iv->devControlRequest = true; - break; - - // Restart - case Restart: - iv->devControlCmd = Restart; - DPRINTLN(DBG_INFO, F("Restart inverter ") + String(iv->id)); - iv->devControlRequest = true; - break; - - // Reactive Power Control - case ReactivePowerContr: - iv->devControlCmd = ReactivePowerContr; - if (true) { // if (std::stoi((char*)payload) > 0) error handling powerlimit needed? - iv->devControlCmd = ReactivePowerContr; - iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); - iv->powerLimit[1] = 0x0000; // if reactivepower limit is set via external interface --> set it temporay - DPRINTLN(DBG_DEBUG, F("Reactivepower limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W")); - iv->devControlRequest = true; - } - break; - - // Set Power Factor - case PFSet: - // iv->devControlCmd = PFSet; - // uint16_t power_factor = std::stoi(strtok(NULL, "/")); - DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id)); - break; - - // CleanState lock & alarm - case CleanState_LockAndAlarm: - iv->devControlCmd = CleanState_LockAndAlarm; - DPRINTLN(DBG_INFO, F("CleanState lock & alarm for inverter ") + String(iv->id)); - iv->devControlRequest = true; - break; - - default: - DPRINTLN(DBG_INFO, "Not implemented"); - break; - } - } - } - } - break; - } - token = strtok(NULL, "/"); - } - DPRINTLN(DBG_INFO, F("app::cbMqtt finished")); -} - -//----------------------------------------------------------------------------- -bool app::getWifiApActive(void) { - return mWifi->getApActive(); -} - -//----------------------------------------------------------------------------- -void app::scanAvailNetworks(void) { - mWifi->scanAvailNetworks(); -} - -//----------------------------------------------------------------------------- -void app::getAvailNetworks(JsonObject obj) { - mWifi->getAvailNetworks(obj); -} - -//----------------------------------------------------------------------------- -void app::sendMqttDiscoveryConfig(void) { - DPRINTLN(DBG_VERBOSE, F("app::sendMqttDiscoveryConfig")); - - char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32]; - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - DynamicJsonDocument deviceDoc(128); - deviceDoc["name"] = iv->name; - deviceDoc["ids"] = String(iv->serial.u64, HEX); - deviceDoc["cu"] = F("http://") + String(WiFi.localIP().toString()); - deviceDoc["mf"] = "Hoymiles"; - deviceDoc["mdl"] = iv->name; - JsonObject deviceObj = deviceDoc.as(); - DynamicJsonDocument doc(384); - - for (uint8_t i = 0; i < rec->length; i++) { - if (rec->assign[i].ch == CH0) { - snprintf(name, 32, "%s %s", iv->name, iv->getFieldName(i, rec)); - } else { - snprintf(name, 32, "%s CH%d %s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - } - snprintf(stateTopic, 64, "%s/%s/ch%d/%s", mConfig.mqtt.topic, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec)); - const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId); - const char *stateCls = getFieldStateClass(rec->assign[i].fieldId); - - doc["name"] = name; - doc["stat_t"] = stateTopic; - doc["unit_of_meas"] = iv->getUnit(i, rec); - doc["uniq_id"] = String(iv->serial.u64, HEX) + "_" + uniq_id; - doc["dev"] = deviceObj; - doc["exp_aft"] = mMqttInterval + 5; // add 5 sec if connection is bad or ESP too slow - if (devCls != NULL) - doc["dev_cla"] = devCls; - if (stateCls != NULL) - doc["stat_cla"] = stateCls; - - serializeJson(doc, buffer); - mMqtt.sendMsg2(discoveryTopic, buffer, true); - // DPRINTLN(DBG_INFO, F("mqtt sent")); - doc.clear(); - } - - // TODO: remove this field, obsolete? - mMqttConfigSendState[id] = true; - - yield(); - } - } -} - -//----------------------------------------------------------------------------- -void app::sendMqtt(void) { - mMqtt.isConnected(true); // really needed? See comment from HorstG-57 #176 - char topic[32 + MAX_NAME_LENGTH], val[32]; - float total[4]; - bool sendTotal = false; - memset(total, 0, sizeof(float) * 4); - snprintf(val, 32, "%ld", millis() / 1000); - - mMqtt.sendMsg("uptime", val); - - if(mMqttSendList.empty()) - return; - - while(!mMqttSendList.empty()) { - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - record_t<> *rec = iv->getRecordStruct(mMqttSendList.front()); - - if(mMqttSendList.front() == RealTimeRunData_Debug) { - // inverter status - uint8_t status = MQTT_STATUS_AVAIL_PROD; - if (!iv->isAvailable(mUtcTimestamp, rec)) - status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; - if (!iv->isProducing(mUtcTimestamp, rec)) { - if (MQTT_STATUS_AVAIL_PROD == status) - status = MQTT_STATUS_AVAIL_NOT_PROD; - } - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name); - snprintf(val, 32, "%s%s%s%s", - (MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "", - "available and ", - (MQTT_STATUS_NOT_AVAIL_NOT_PROD || MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "", - "producing" - ); - mMqtt.sendMsg(topic, val); - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->name); - snprintf(val, 32, "%d", status); - mMqtt.sendMsg(topic, val); - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->name); - snprintf(val, 48, "%i", iv->getLastTs(rec) * 1000); - mMqtt.sendMsg(topic, val); - } - - // data - for (uint8_t i = 0; i < rec->length; i++) { - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); - snprintf(val, 10, "%.3f", iv->getValue(i, rec)); - mMqtt.sendMsg(topic, val); - - // calculate total values for RealTimeRunData_Debug - if (mMqttSendList.front() == RealTimeRunData_Debug) { - if (CH0 == rec->assign[i].ch) { - switch (rec->assign[i].fieldId) { - case FLD_PAC: - total[0] += iv->getValue(i, rec); - break; - case FLD_YT: - total[1] += iv->getValue(i, rec); - break; - case FLD_YD: - total[2] += iv->getValue(i, rec); - break; - case FLD_PDC: - total[3] += iv->getValue(i, rec); - break; - } - } - sendTotal = true; - } - yield(); - } - } - - mMqttSendList.pop(); // remove from list once all inverters were processed - } - - if (true == sendTotal) { - uint8_t fieldId; - for (uint8_t i = 0; i < 4; i++) { - switch (i) { - default: - case 0: - fieldId = FLD_PAC; - break; - case 1: - fieldId = FLD_YT; - break; - case 2: - fieldId = FLD_YD; - break; - case 3: - fieldId = FLD_PDC; - break; - } - snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); - snprintf(val, 10, "%.3f", total[i]); - mMqtt.sendMsg(topic, val); - } - } -} - -//----------------------------------------------------------------------------- -const char *app::getFieldDeviceClass(uint8_t fieldId) { - uint8_t pos = 0; - for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { - if (deviceFieldAssignment[pos].fieldId == fieldId) - break; - } - return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : deviceClasses[deviceFieldAssignment[pos].deviceClsId]; -} - -//----------------------------------------------------------------------------- -const char *app::getFieldStateClass(uint8_t fieldId) { - uint8_t pos = 0; - for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { - if (deviceFieldAssignment[pos].fieldId == fieldId) - break; - } - return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; -} - -//----------------------------------------------------------------------------- -void app::resetSystem(void) { - mUptimeSecs = 0; - mPrevMillis = 0; - mUpdateNtp = false; - mFlagSendDiscoveryConfig = false; - - mNtpRefreshTicker = 0; - mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms] - -#ifdef AP_ONLY - mUtcTimestamp = 1; -#else - mUtcTimestamp = 0; -#endif - - mHeapStatCnt = 0; - - mSendTicker = 0xffff; - mMqttTicker = 0xffff; - mMqttInterval = MQTT_INTERVAL; - mSerialTicker = 0xffff; - mMqttActive = false; - - mTicker = 0; - mRxTicker = 0; - - mSendLastIvId = 0; - - mShowRebootRequest = false; - - memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); - memset(&mStat, 0, sizeof(statistics_t)); - mLastPacketId = 0x00; -} - -//----------------------------------------------------------------------------- -void app::loadDefaultConfig(void) { - memset(&mSysConfig, 0, sizeof(sysConfig_t)); - memset(&mConfig, 0, sizeof(config_t)); - snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); - - snprintf(mSysConfig.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME); - - // wifi - snprintf(mSysConfig.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID); - snprintf(mSysConfig.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD); - - // nrf24 - mConfig.sendInterval = SEND_INTERVAL; - mConfig.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; - mConfig.pinCs = DEF_CS_PIN; - mConfig.pinCe = DEF_CE_PIN; - mConfig.pinIrq = DEF_IRQ_PIN; - mConfig.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; - - // ntp - snprintf(mConfig.ntpAddr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); - mConfig.ntpPort = DEF_NTP_PORT; - - // Latitude + Longitude - mConfig.sunLat = 0.0; - mConfig.sunLon = 0.0; - mConfig.sunDisNightCom = false; - - // mqtt - snprintf(mConfig.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); - mConfig.mqtt.port = DEF_MQTT_PORT; - snprintf(mConfig.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); - snprintf(mConfig.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); - snprintf(mConfig.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); - - // serial - mConfig.serialInterval = SERIAL_INTERVAL; - mConfig.serialShowIv = false; - mConfig.serialDebug = false; - - // Disclaimer - mConfig.disclaimer = false; -} - -//----------------------------------------------------------------------------- -void app::loadEEpconfig(void) { - DPRINTLN(DBG_VERBOSE, F("app::loadEEpconfig")); - - if (mWifiSettingsValid) - mEep->read(ADDR_CFG_SYS, (uint8_t *)&mSysConfig, CFG_SYS_LEN); - if (mSettingsValid) { - mEep->read(ADDR_CFG, (uint8_t *)&mConfig, CFG_LEN); - - mSendTicker = mConfig.sendInterval; - mSerialTicker = 0; - - // inverter - uint64_t invSerial; - char name[MAX_NAME_LENGTH + 1] = {0}; - uint16_t modPwr[4]; - Inverter<> *iv; - for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial); - mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), name, MAX_NAME_LENGTH); - mEep->read(ADDR_INV_CH_PWR + (i * 2 * 4), modPwr, 4); - if (0ULL != invSerial) { - iv = mSys->addInverter(name, invSerial, modPwr); - if (NULL != iv) { // will run once on every dtu boot - for (uint8_t j = 0; j < 4; j++) { - mEep->read(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); - } - } - - // TODO: the original mqttinterval value is not needed any more - mMqttInterval += mConfig.sendInterval; - } - } - - for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - iv = mSys->getInverterByPos(i, false); - if (NULL != iv) - resetPayload(iv); - } - } -} - -//----------------------------------------------------------------------------- -void app::saveValues(void) { - DPRINTLN(DBG_VERBOSE, F("app::saveValues")); - - mEep->write(ADDR_CFG_SYS, (uint8_t *)&mSysConfig, CFG_SYS_LEN); - mEep->write(ADDR_CFG, (uint8_t *)&mConfig, CFG_LEN); - Inverter<> *iv; - for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - iv = mSys->getInverterByPos(i, false); - mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64); - mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH); - // max channel power / name - for (uint8_t j = 0; j < 4; j++) { - mEep->write(ADDR_INV_CH_PWR + (i * 2 * 4) + (j * 2), iv->chMaxPwr[j]); - mEep->write(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); - } - } - - updateCrc(); - - // update sun - mLatestSunTimestamp = 0; -} - -//----------------------------------------------------------------------------- -void app::setupMqtt(void) { - if (mSettingsValid) { - if (mConfig.mqtt.broker[0] > 0) { - mMqttActive = true; - if (mMqttInterval < MIN_MQTT_INTERVAL) mMqttInterval = MIN_MQTT_INTERVAL; - } else { - mMqttInterval = 0xffff; - } - - mMqttTicker = 0; - mMqtt.setup(&mConfig.mqtt, mSysConfig.deviceName); - mMqtt.setCallback(std::bind(&app::cbMqtt, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - - if (mMqttActive) { - mMqtt.sendMsg("version", mVersion); - if (mMqtt.isConnected()) { - mMqtt.sendMsg("device", mSysConfig.deviceName); - mMqtt.sendMsg("uptime", "0"); - } - } - } -} - -//----------------------------------------------------------------------------- -void app::resetPayload(Inverter<> *iv) { - DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id)); - memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES); - mPayload[iv->id].txCmd = 0; - mPayload[iv->id].retransmits = 0; - mPayload[iv->id].maxPackId = 0; - mPayload[iv->id].complete = false; - mPayload[iv->id].requested = false; - mPayload[iv->id].ts = mUtcTimestamp; -} - -//----------------------------------------------------------------------------- -void app::calculateSunriseSunset() { - // Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth - - // Julian day since 1.1.2000 12:00 + correction 69.12s - double n_JulianDay = (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 - 10957.0 + 0.0008; - // Mean solar time - double J = n_JulianDay - mConfig.sunLon / 360; - // Solar mean anomaly - double M = fmod((357.5291 + 0.98560028 * J), 360); - // Equation of the center - double C = 1.9148 * SIN(M) + 0.02 * SIN(2 * M) + 0.0003 * SIN(3 * M); - // Ecliptic longitude - double lambda = fmod((M + C + 180 + 102.9372), 360); - // Solar transit - double Jtransit = 2451545.0 + J + 0.0053 * SIN(M) - 0.0069 * SIN(2 * lambda); - // Declination of the sun - double delta = ASIN(SIN(lambda) * SIN(23.44)); - // Hour angle - double omega = ACOS(SIN(-0.83) - SIN(mConfig.sunLat) * SIN(delta) / COS(mConfig.sunLat) * COS(delta)); - // Calculate sunrise and sunset - double Jrise = Jtransit - omega / 360; - double Jset = Jtransit + omega / 360; - // Julian sunrise/sunset to UTC unix timestamp (days incl. fraction to seconds + unix offset 1.1.2000 12:00) - mSunrise = (Jrise - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line - mSunset = (Jset - 2451545.0) * 86400 + 946728000; // OPTIONAL: Add an offset of +-seconds to the end of the line -} diff --git a/tools/esp8266/app.h b/tools/esp8266/app.h deleted file mode 100644 index 6bdb4652..00000000 --- a/tools/esp8266/app.h +++ /dev/null @@ -1,303 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __APP_H__ -#define __APP_H__ - -#include "dbg.h" -#include "Arduino.h" - - -#include -#include -#include -#include - -#include "eep.h" -#include "defines.h" -#include "crc.h" - -#include "CircularBuffer.h" -#include "hmSystem.h" -#include "mqtt.h" -#include "ahoywifi.h" -#include "web.h" - -// convert degrees and radians for sun calculation -#define SIN(x) (sin(radians(x))) -#define COS(x) (cos(radians(x))) -#define ASIN(x) (degrees(asin(x))) -#define ACOS(x) (degrees(acos(x))) - -typedef HmSystem HmSystemType; - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint8_t invId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - uint8_t retransmits; - bool requested; -} invPayload_t; - -class ahoywifi; -class web; - -class app { - public: - app(); - ~app() {} - - void setup(uint32_t timeout); - void loop(void); - void handleIntr(void); - void cbMqtt(char* topic, byte* payload, unsigned int length); - void saveValues(void); - void resetPayload(Inverter<>* iv); - bool getWifiApActive(void); - void scanAvailNetworks(void); - void getAvailNetworks(JsonObject obj); - - uint8_t getIrqPin(void) { - return mConfig.pinIrq; - } - - uint64_t Serial2u64(const char *val) { - char tmp[3]; - uint64_t ret = 0ULL; - uint64_t u64; - memset(tmp, 0, 3); - for(uint8_t i = 0; i < 6; i++) { - tmp[0] = val[i*2]; - tmp[1] = val[i*2 + 1]; - if((tmp[0] == '\0') || (tmp[1] == '\0')) - break; - u64 = strtol(tmp, NULL, 16); - ret |= (u64 << ((5-i) << 3)); - } - return ret; - } - - String getDateTimeStr(time_t t) { - char str[20]; - if(0 == t) - sprintf(str, "n/a"); - else - sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); - return String(str); - } - - String getTimeStr(uint32_t offset = 0) { - char str[10]; - if(0 == mUtcTimestamp) - sprintf(str, "n/a"); - else - sprintf(str, "%02d:%02d:%02d ", hour(mUtcTimestamp + offset), minute(mUtcTimestamp + offset), second(mUtcTimestamp + offset)); - return String(str); - } - - inline uint32_t getUptime(void) { - return mUptimeSecs; - } - - inline uint32_t getTimestamp(void) { - return mUtcTimestamp; - } - - void setTimestamp(uint32_t newTime) { - DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime)); - if(0 == newTime) - mUpdateNtp = true; - else - { - mUtcTimestamp = newTime; - } - } - - inline uint32_t getSunrise(void) { - return mSunrise; - } - inline uint32_t getSunset(void) { - return mSunset; - } - inline uint32_t getLatestSunTimestamp(void) { - return mLatestSunTimestamp; - } - - void eraseSettings(bool all = false) { - //DPRINTLN(DBG_VERBOSE, F("main.h:eraseSettings")); - uint8_t buf[64]; - uint16_t addr = (all) ? ADDR_START : ADDR_START_SETTINGS; - uint16_t end; - - memset(buf, 0xff, 64); - do { - end = addr + 64; - if(end > (ADDR_SETTINGS_CRC + 2)) - end = (ADDR_SETTINGS_CRC + 2); - DPRINTLN(DBG_DEBUG, F("erase: 0x") + String(addr, HEX) + " - 0x" + String(end, HEX)); - mEep->write(addr, buf, (end-addr)); - addr = end; - } while(addr < (ADDR_SETTINGS_CRC + 2)); - mEep->commit(); - } - - inline bool checkTicker(uint32_t *ticker, uint32_t interval) { - uint32_t mil = millis(); - if(mil >= *ticker) { - *ticker = mil + interval; - return true; - } - else if(mil < (*ticker - interval)) { - *ticker = mil + interval; - return true; - } - - return false; - } - - inline bool mqttIsConnected(void) { return mMqtt.isConnected(); } - inline bool getSettingsValid(void) { return mSettingsValid; } - inline bool getRebootRequestState(void) { return mShowRebootRequest; } - inline uint32_t getMqttTxCnt(void) { return mMqtt.getTxCnt(); } - - HmSystemType *mSys; - bool mShouldReboot; - bool mFlagSendDiscoveryConfig; - - private: - void resetSystem(void); - void loadDefaultConfig(void); - void loadEEpconfig(void); - void setupMqtt(void); - - void sendMqttDiscoveryConfig(void); - void sendMqtt(void); - - bool buildPayload(uint8_t id); - void processPayload(bool retransmit); - - const char* getFieldDeviceClass(uint8_t fieldId); - const char* getFieldStateClass(uint8_t fieldId); - - inline uint16_t buildEEpCrc(uint32_t start, uint32_t length) { - DPRINTLN(DBG_VERBOSE, F("main.h:buildEEpCrc")); - uint8_t buf[32]; - uint16_t crc = 0xffff; - uint8_t len; - - while(length > 0) { - len = (length < 32) ? length : 32; - mEep->read(start, buf, len); - crc = ah::crc16(buf, len, crc); - start += len; - length -= len; - } - return crc; - } - - void updateCrc(void) { - DPRINTLN(DBG_VERBOSE, F("app::updateCrc")); - uint16_t crc; - - crc = buildEEpCrc(ADDR_START, ADDR_WIFI_CRC); - DPRINTLN(DBG_DEBUG, F("new Wifi CRC: ") + String(crc, HEX)); - mEep->write(ADDR_WIFI_CRC, crc); - - crc = buildEEpCrc(ADDR_START_SETTINGS, ((ADDR_NEXT) - (ADDR_START_SETTINGS))); - DPRINTLN(DBG_DEBUG, F("new Settings CRC: ") + String(crc, HEX)); - mEep->write(ADDR_SETTINGS_CRC, crc); - - mEep->commit(); - } - - bool checkEEpCrc(uint32_t start, uint32_t length, uint32_t crcPos) { - DPRINTLN(DBG_VERBOSE, F("main.h:checkEEpCrc")); - DPRINTLN(DBG_DEBUG, F("start: ") + String(start) + F(", length: ") + String(length)); - uint16_t crcRd, crcCheck; - crcCheck = buildEEpCrc(start, length); - mEep->read(crcPos, &crcRd); - DPRINTLN(DBG_DEBUG, "CRC RD: " + String(crcRd, HEX) + " CRC CALC: " + String(crcCheck, HEX)); - return (crcCheck == crcRd); - } - - void stats(void) { - DPRINTLN(DBG_VERBOSE, F("main.h:stats")); - #ifdef ESP8266 - uint32_t free; - uint16_t max; - uint8_t frag; - ESP.getHeapStats(&free, &max, &frag); - #elif defined(ESP32) - uint32_t free; - uint32_t max; - uint8_t frag; - free = ESP.getFreeHeap(); - max = ESP.getMaxAllocHeap(); - frag = 0; - #endif - DPRINT(DBG_VERBOSE, F("free: ") + String(free)); - DPRINT(DBG_VERBOSE, F(" - max: ") + String(max) + "%"); - DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag)); - } - - void calculateSunriseSunset(void); - - uint32_t mUptimeSecs; - uint32_t mPrevMillis; - uint8_t mHeapStatCnt; - uint32_t mNtpRefreshTicker; - uint32_t mNtpRefreshInterval; - - - bool mWifiSettingsValid; - bool mSettingsValid; - - eep *mEep; - uint32_t mUtcTimestamp; - bool mUpdateNtp; - - bool mShowRebootRequest; - - ahoywifi *mWifi; - web *mWebInst; - sysConfig_t mSysConfig; - config_t mConfig; - char mVersion[12]; - - uint16_t mSendTicker; - uint8_t mSendLastIvId; - - invPayload_t mPayload[MAX_NUM_INVERTERS]; - statistics_t mStat; - uint8_t mLastPacketId; - - // timer - uint32_t mTicker; - uint32_t mRxTicker; - - // mqtt - mqtt mMqtt; - uint16_t mMqttTicker; - uint16_t mMqttInterval; - bool mMqttActive; - bool mMqttConfigSendState[MAX_NUM_INVERTERS]; - std::queue mMqttSendList; - - // serial - uint16_t mSerialTicker; - - // sun - int32_t mCalculatedTimezoneOffset; - uint32_t mSunrise; - uint32_t mSunset; - uint32_t mLatestSunTimestamp; -}; - -#endif /*__APP_H__*/ diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h deleted file mode 100644 index 3acb352a..00000000 --- a/tools/esp8266/defines.h +++ /dev/null @@ -1,199 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __DEFINES_H__ -#define __DEFINES_H__ - -#include "config.h" - -//------------------------------------- -// VERSION -//------------------------------------- -#define VERSION_MAJOR 0 -#define VERSION_MINOR 5 -#define VERSION_PATCH 28 - -//------------------------------------- -typedef struct { - uint8_t rxCh; - uint8_t packet[MAX_RF_PAYLOAD_SIZE]; -} packet_t; - -typedef enum { - InverterDevInform_Simple = 0, // 0x00 - InverterDevInform_All = 1, // 0x01 - GridOnProFilePara = 2, // 0x02 - HardWareConfig = 3, // 0x03 - SimpleCalibrationPara = 4, // 0x04 - SystemConfigPara = 5, // 0x05 - RealTimeRunData_Debug = 11, // 0x0b - RealTimeRunData_Reality = 12, // 0x0c - RealTimeRunData_A_Phase = 13, // 0x0d - RealTimeRunData_B_Phase = 14, // 0x0e - RealTimeRunData_C_Phase = 15, // 0x0f - AlarmData = 17, // 0x11, Alarm data - all unsent alarms - AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms - RecordData = 19, // 0x13 - InternalData = 20, // 0x14 - GetLossRate = 21, // 0x15 - GetSelfCheckState = 30, // 0x1e - InitDataState = 0xff -} InfoCmdType; - -typedef enum { - TurnOn = 0, // 0x00 - TurnOff = 1, // 0x01 - Restart = 2, // 0x02 - Lock = 3, // 0x03 - Unlock = 4, // 0x04 - ActivePowerContr = 11, // 0x0b - ReactivePowerContr = 12, // 0x0c - PFSet = 13, // 0x0d - CleanState_LockAndAlarm = 20, // 0x14 - SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files - Init = 0xff -} DevControlCmdType; - -typedef enum { - AbsolutNonPersistent = 0UL, // 0x0000 - RelativNonPersistent = 1UL, // 0x0001 - AbsolutPersistent = 256UL, // 0x0100 - RelativPersistent = 257UL // 0x0101 -} PowerLimitControlType; - -#define MIN_SERIAL_INTERVAL 5 -#define MIN_SEND_INTERVAL 15 -#define MIN_MQTT_INTERVAL 60 - - -#define MQTT_STATUS_NOT_AVAIL_NOT_PROD 0 -#define MQTT_STATUS_AVAIL_NOT_PROD 1 -#define MQTT_STATUS_AVAIL_PROD 2 - -//------------------------------------- -// EEPROM -//------------------------------------- -#define SSID_LEN 32 -#define PWD_LEN 64 -#define DEVNAME_LEN 16 -#define CRC_LEN 2 // uint16_t -#define DISCLAIMER 1 - -#define INV_ADDR_LEN MAX_NUM_INVERTERS * 8 // uint64_t -#define INV_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH // char[] -#define INV_CH_CH_PWR_LEN MAX_NUM_INVERTERS * 2 * 4 // uint16_t (4 channels) -#define INV_CH_CH_NAME_LEN MAX_NUM_INVERTERS * MAX_NAME_LENGTH * 4 // (4 channels) -#define INV_INTERVAL_LEN 2 // uint16_t -#define INV_MAX_RTRY_LEN 1 // uint8_t - -#define CFG_SUN_LEN 9 // 2x float(4+4) + bool(1) - -#define NTP_ADDR_LEN 32 // DNS Name - -#define MQTT_ADDR_LEN 32 // DNS Name -#define MQTT_USER_LEN 16 -#define MQTT_PWD_LEN 32 -#define MQTT_TOPIC_LEN 32 -#define MQTT_DISCOVERY_PREFIX "homeassistant" -#define MQTT_MAX_PACKET_SIZE 384 -#define MQTT_RECONNECT_DELAY 5000 - -#pragma pack(push) // push current alignment to stack -#pragma pack(1) // set alignment to 1 byte boundary -typedef struct { - char broker[MQTT_ADDR_LEN]; - uint16_t port; - char user[MQTT_USER_LEN]; - char pwd[MQTT_PWD_LEN]; - char topic[MQTT_TOPIC_LEN]; -} mqttConfig_t; -#pragma pack(pop) // restore original alignment from stack - - -typedef struct { - char deviceName[DEVNAME_LEN]; - - // wifi - char stationSsid[SSID_LEN]; - char stationPwd[PWD_LEN]; -} sysConfig_t; - -#pragma pack(push) // push current alignment to stack -#pragma pack(1) // set alignment to 1 byte boundary -typedef struct { - // nrf24 - uint16_t sendInterval; - uint8_t maxRetransPerPyld; - uint8_t pinCs; - uint8_t pinCe; - uint8_t pinIrq; - uint8_t amplifierPower; - - // Disclaimer - bool disclaimer; - - // ntp - char ntpAddr[NTP_ADDR_LEN]; - uint16_t ntpPort; - - // mqtt - mqttConfig_t mqtt; - - // sun - float sunLat; - float sunLon; - bool sunDisNightCom; // disable night communication - - // serial - uint16_t serialInterval; - bool serialShowIv; - bool serialDebug; -} config_t; -#pragma pack(pop) // restore original alignment from stack - -typedef struct { - uint32_t rxFail; - uint32_t rxFailNoAnser; - uint32_t rxSuccess; - uint32_t frmCnt; -} statistics_t; - - -#define CFG_MQTT_LEN MQTT_ADDR_LEN + 2 + MQTT_USER_LEN + MQTT_PWD_LEN +MQTT_TOPIC_LEN -#define CFG_SYS_LEN DEVNAME_LEN + SSID_LEN + PWD_LEN + 1 -#define CFG_LEN 7 + NTP_ADDR_LEN + 2 + CFG_MQTT_LEN + CFG_SUN_LEN + 4 + DISCLAIMER - -#define ADDR_START 0 -#define ADDR_CFG_SYS ADDR_START -#define ADDR_WIFI_CRC ADDR_CFG_SYS + CFG_SYS_LEN -#define ADDR_START_SETTINGS ADDR_WIFI_CRC + CRC_LEN - -#define ADDR_CFG ADDR_START_SETTINGS -#define ADDR_CFG_INVERTER ADDR_CFG + CFG_LEN - -#define ADDR_INV_ADDR ADDR_CFG_INVERTER -#define ADDR_INV_NAME ADDR_INV_ADDR + INV_ADDR_LEN -#define ADDR_INV_CH_PWR ADDR_INV_NAME + INV_NAME_LEN -#define ADDR_INV_CH_NAME ADDR_INV_CH_PWR + INV_CH_CH_PWR_LEN -#define ADDR_INV_INTERVAL ADDR_INV_CH_NAME + INV_CH_CH_NAME_LEN -#define ADDR_INV_MAX_RTRY ADDR_INV_INTERVAL + INV_INTERVAL_LEN - -#define ADDR_NEXT ADDR_INV_MAX_RTRY + INV_INTERVAL_LEN - - -#define ADDR_SETTINGS_CRC ADDR_NEXT + 2 - -#if(ADDR_SETTINGS_CRC <= ADDR_NEXT) -#pragma error "address overlap! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", ADDR_NEXT="+ ADDR_NEXT +")" -#endif - -#if(ADDR_SETTINGS_CRC >= 4096 - CRC_LEN) -#pragma error "EEPROM size exceeded! (ADDR_SETTINGS_CRC="+ ADDR_SETTINGS_CRC +", CRC_LEN="+ CRC_LEN +")" -#pragma error "Configure less inverters? (MAX_NUM_INVERTERS=" + MAX_NUM_INVERTERS +")" -#endif - - - -#endif /*__DEFINES_H__*/ diff --git a/tools/esp8266/eep.h b/tools/esp8266/eep.h deleted file mode 100644 index 69aff7f5..00000000 --- a/tools/esp8266/eep.h +++ /dev/null @@ -1,161 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __EEP_H__ -#define __EEP_H__ - -#include "Arduino.h" -#include -#ifdef ESP32 - #include -#endif - -class eep { - public: - eep() { - - #ifdef ESP32 - if(!EEPROM.begin(4096)) { - nvs_flash_init(); - EEPROM.begin(4096); - } - #else - EEPROM.begin(4096); - #endif - - } - ~eep() { - EEPROM.end(); - } - - void read(uint32_t addr, char *str, uint8_t length) { - for(uint8_t i = 0; i < length; i ++) { - *(str++) = (char)EEPROM.read(addr++); - } - } - - void read(uint32_t addr, float *value) { - uint8_t *p = (uint8_t*)value; - for(uint8_t i = 0; i < 4; i ++) { - *(p++) = (uint8_t)EEPROM.read(addr++); - } - } - - void read(uint32_t addr, bool *value) { - uint8_t intVal = 0x00; - intVal = EEPROM.read(addr++); - *value = (intVal == 0x01); - } - - void read(uint32_t addr, uint8_t *value) { - *value = (EEPROM.read(addr++)); - } - - void read(uint32_t addr, uint8_t data[], uint16_t length) { - for(uint16_t i = 0; i < length; i ++) { - *(data++) = EEPROM.read(addr++); - } - } - - void read(uint32_t addr, uint16_t *value) { - *value = (EEPROM.read(addr++) << 8); - *value |= (EEPROM.read(addr++)); - } - - void read(uint32_t addr, uint16_t data[], uint16_t length) { - for(uint16_t i = 0; i < length; i ++) { - *(data) = (EEPROM.read(addr++) << 8); - *(data++) |= (EEPROM.read(addr++)); - } - } - - void read(uint32_t addr, uint32_t *value) { - *value = (EEPROM.read(addr++) << 24); - *value |= (EEPROM.read(addr++) << 16); - *value |= (EEPROM.read(addr++) << 8); - *value |= (EEPROM.read(addr++)); - } - - void read(uint32_t addr, uint64_t *value) { - read(addr, (uint32_t *)value); - *value <<= 32; - uint32_t tmp; - read(addr+4, &tmp); - *value |= tmp; - /**value = (EEPROM.read(addr++) << 56); - *value |= (EEPROM.read(addr++) << 48); - *value |= (EEPROM.read(addr++) << 40); - *value |= (EEPROM.read(addr++) << 32); - *value |= (EEPROM.read(addr++) << 24); - *value |= (EEPROM.read(addr++) << 16); - *value |= (EEPROM.read(addr++) << 8); - *value |= (EEPROM.read(addr++));*/ - } - - void write(uint32_t addr, const char *str, uint8_t length) { - for(uint8_t i = 0; i < length; i ++) { - EEPROM.write(addr++, str[i]); - } - } - - void write(uint32_t addr, uint8_t data[], uint16_t length) { - for(uint16_t i = 0; i < length; i ++) { - EEPROM.write(addr++, data[i]); - } - } - - void write(uint32_t addr, float value) { - uint8_t *p = (uint8_t*)&value; - for(uint8_t i = 0; i < 4; i ++) { - EEPROM.write(addr++, p[i]); - } - } - - void write(uint32_t addr, bool value) { - uint8_t intVal = (value) ? 0x01 : 0x00; - EEPROM.write(addr++, intVal); - } - - void write(uint32_t addr, uint8_t value) { - EEPROM.write(addr++, value); - } - - void write(uint32_t addr, uint16_t value) { - EEPROM.write(addr++, (value >> 8) & 0xff); - EEPROM.write(addr++, (value ) & 0xff); - } - - - void write(uint32_t addr, uint16_t data[], uint16_t length) { - for(uint16_t i = 0; i < length; i ++) { - EEPROM.write(addr++, (data[i] >> 8) & 0xff); - EEPROM.write(addr++, (data[i] ) & 0xff); - } - } - - void write(uint32_t addr, uint32_t value) { - EEPROM.write(addr++, (value >> 24) & 0xff); - EEPROM.write(addr++, (value >> 16) & 0xff); - EEPROM.write(addr++, (value >> 8) & 0xff); - EEPROM.write(addr++, (value ) & 0xff); - } - - void write(uint32_t addr, uint64_t value) { - EEPROM.write(addr++, (value >> 56) & 0xff); - EEPROM.write(addr++, (value >> 48) & 0xff); - EEPROM.write(addr++, (value >> 40) & 0xff); - EEPROM.write(addr++, (value >> 32) & 0xff); - EEPROM.write(addr++, (value >> 24) & 0xff); - EEPROM.write(addr++, (value >> 16) & 0xff); - EEPROM.write(addr++, (value >> 8) & 0xff); - EEPROM.write(addr++, (value ) & 0xff); - } - - void commit(void) { - EEPROM.commit(); - } -}; - -#endif /*__EEP_H__*/ diff --git a/tools/esp8266/hmRadio.h b/tools/esp8266/hmRadio.h deleted file mode 100644 index 048c5255..00000000 --- a/tools/esp8266/hmRadio.h +++ /dev/null @@ -1,376 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __RADIO_H__ -#define __RADIO_H__ - -#include "dbg.h" -#include -#include "crc.h" -#ifndef DISABLE_IRQ - #if defined(ESP8266) || defined(ESP32) - #define DISABLE_IRQ noInterrupts() - #define RESTORE_IRQ interrupts() - #else - #define DISABLE_IRQ \ - uint8_t sreg = SREG; \ - cli(); - - #define RESTORE_IRQ \ - SREG = sreg; - #endif -#endif -//#define CHANNEL_HOP // switch between channels or use static channel to send - -#define DEFAULT_RECV_CHANNEL 3 -#define SPI_SPEED 1000000 - -#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) - -#define RF_CHANNELS 5 -#define RF_LOOP_CNT 300 - -#define TX_REQ_INFO 0x15 -#define TX_REQ_DEVCONTROL 0x51 -#define ALL_FRAMES 0x80 -#define SINGLE_FRAME 0x81 - -const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; - - -//----------------------------------------------------------------------------- -// MACROS -//----------------------------------------------------------------------------- -#define CP_U32_LittleEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[0] = ((v >> 24) & 0xff); \ - b[1] = ((v >> 16) & 0xff); \ - b[2] = ((v >> 8) & 0xff); \ - b[3] = ((v ) & 0xff); \ -}) - -#define CP_U32_BigEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[3] = ((v >> 24) & 0xff); \ - b[2] = ((v >> 16) & 0xff); \ - b[1] = ((v >> 8) & 0xff); \ - b[0] = ((v ) & 0xff); \ -}) - -#define BIT_CNT(x) ((x)<<3) - - -//----------------------------------------------------------------------------- -// HM Radio class -//----------------------------------------------------------------------------- -template -class HmRadio { - public: - HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { - DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); - DPRINT(DBG_VERBOSE, String(CE_PIN)); - DPRINT(DBG_VERBOSE, F(", CS_PIN: ")); - DPRINT(DBG_VERBOSE, String(CS_PIN)); - DPRINT(DBG_VERBOSE, F(", SPI_SPEED: ")); - DPRINTLN(DBG_VERBOSE, String(SPI_SPEED) + ")"); - - // Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz. - // Channel List 2403, 2423, 2440, 2461, 2475MHz - mRfChLst[0] = 03; - mRfChLst[1] = 23; - mRfChLst[2] = 40; - mRfChLst[3] = 61; - mRfChLst[4] = 75; - - mTxChIdx = 2; // Start TX with 40 - mRxChIdx = 0; // Start RX with 03 - mRxLoopCnt = RF_LOOP_CNT; - - mSendCnt = 0; - - mSerialDebug = false; - mIrqRcvd = false; - } - ~HmRadio() {} - - void setup(BUFFER *ctrl, uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) { - DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); - pinMode(irq, INPUT_PULLUP); - mBufCtrl = ctrl; - - - uint32_t dtuSn = 0x87654321; - uint32_t chipID = 0; // will be filled with last 3 bytes of MAC - #ifdef ESP32 - uint64_t MAC = ESP.getEfuseMac(); - chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); - #else - chipID = ESP.getChipId(); - #endif - if(chipID) { - dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal - for(int i = 0; i < 7; i++) { - dtuSn |= (chipID % 10) << (i * 4); - chipID /= 10; - } - } - // change the byte order of the DTU serial number and append the required 0x01 at the end - DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; - - mNrf24.begin(ce, cs); - mNrf24.setRetries(0, 0); - - mNrf24.setChannel(DEFAULT_RECV_CHANNEL); - mNrf24.setDataRate(RF24_250KBPS); - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.setAutoAck(false); - mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); - mNrf24.setAddressWidth(5); - mNrf24.openReadingPipe(1, DTU_RADIO_ID); - mNrf24.enableDynamicPayloads(); - - // enable only receiving interrupts - mNrf24.maskIRQ(true, true, false); - - DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); - DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); - mNrf24.setPALevel(ampPwr & 0x03); - mNrf24.startListening(); - - DPRINTLN(DBG_INFO, F("Radio Config:")); - mNrf24.printPrettyDetails(); - - mTxCh = setDefaultChannels(); - - if(!mNrf24.isChipConnected()) { - DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); - } - } - - void loop(void) { - DISABLE_IRQ; - if(mIrqRcvd) { - mIrqRcvd = false; - bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - RESTORE_IRQ; - uint8_t pipe, len; - packet_t *p; - while(mNrf24.available(&pipe)) { - if(!mBufCtrl->full()) { - p = mBufCtrl->getFront(); - p->rxCh = mRfChLst[mRxChIdx]; - len = mNrf24.getPayloadSize(); - if(len > MAX_RF_PAYLOAD_SIZE) - len = MAX_RF_PAYLOAD_SIZE; - - mNrf24.read(p->packet, len); - mBufCtrl->pushFront(p); - yield(); - } - else - break; - } - mNrf24.flush_rx(); // drop the packet - } - else - RESTORE_IRQ; - } - - void enableDebug() { - mSerialDebug = true; - } - - void handleIntr(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:handleIntr")); - mIrqRcvd = true; - } - - uint8_t setDefaultChannels(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels")); - mTxChIdx = 2; // Start TX with 40 - mRxChIdx = 0; // Start RX with 03 - return mRfChLst[mTxChIdx]; - } - - void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { - DPRINTLN(DBG_INFO, F("sendControlPacket cmd: ") + String(cmd)); - sendCmdPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME, false); - uint8_t cnt = 0; - mTxBuf[10 + cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor - mTxBuf[10 + cnt++] = 0x00; - if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[10 + cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[10 + cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[10 + cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[10 + cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling - } - - // crc control data - uint16_t crc = ah::crc16(&mTxBuf[10], cnt); - mTxBuf[10 + cnt++] = (crc >> 8) & 0xff; - mTxBuf[10 + cnt++] = (crc ) & 0xff; - - // crc over all - mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt); - - sendPacket(invId, mTxBuf, 10 + cnt + 1, true); - } - - void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) { - DPRINTLN(DBG_INFO, F("sendTimePacket")); - sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false); - mTxBuf[10] = cmd; // cid - mTxBuf[11] = 0x00; - CP_U32_LittleEndian(&mTxBuf[12], ts); - if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { - mTxBuf[18] = (alarmMesId >> 8) & 0xff; - mTxBuf[19] = (alarmMesId ) & 0xff; - } - uint16_t crc = ah::crc16(&mTxBuf[10], 14); - mTxBuf[24] = (crc >> 8) & 0xff; - mTxBuf[25] = (crc ) & 0xff; - mTxBuf[26] = ah::crc8(mTxBuf, 26); - - sendPacket(invId, mTxBuf, 27, true); - } - - void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) { - DPRINTLN(DBG_VERBOSE, F("sendCmdPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); - memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); - mTxBuf[0] = mid; // message id - CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); - CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); - mTxBuf[9] = pid; - if(calcCrc) { - mTxBuf[10] = ah::crc8(mTxBuf, 10); - sendPacket(invId, mTxBuf, 11, false); - } - } - - bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { - //DPRINTLN(DBG_INFO, F("hmRadio.h:checkPaketCrc")); - *len = (buf[0] >> 2); - if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) - *len = MAX_RF_PAYLOAD_SIZE - 2; - for(uint8_t i = 1; i < (*len + 1); i++) { - buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7); - } - - uint8_t crc = ah::crc8(buf, *len-1); - bool valid = (crc == buf[*len-1]); - - return valid; - } - - bool switchRxCh(uint16_t addLoop = 0) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:switchRxCh")); - mRxLoopCnt += addLoop; - if(mRxLoopCnt != 0) { - mRxLoopCnt--; - DISABLE_IRQ; - mNrf24.stopListening(); - mNrf24.setChannel(getRxNxtChannel()); - mNrf24.startListening(); - RESTORE_IRQ; - } - return (0 == mRxLoopCnt); // receive finished - } - - void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); - if(NULL != info) - DBGPRINT(String(info)); - for(uint8_t i = 0; i < len; i++) { - DHEX(buf[i]); - DBGPRINT(" "); - } - DBGPRINTLN(""); - } - - bool isChipConnected(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); - return mNrf24.isChipConnected(); - } - - - - uint32_t mSendCnt; - - bool mSerialDebug; - - private: - void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); - //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); - //dumpBuf("SEN ", buf, len); - if(mSerialDebug) { - DPRINT(DBG_INFO, "TX " + String(len) + "B Ch" + String(mRfChLst[mTxChIdx]) + " | "); - dumpBuf(NULL, buf, len); - } - - DISABLE_IRQ; - mNrf24.stopListening(); - - if(clear) - mRxLoopCnt = RF_LOOP_CNT; - - mNrf24.setChannel(mRfChLst[mTxChIdx]); - mTxCh = getTxNxtChannel(); // switch channel for next packet - mNrf24.openWritingPipe(invId); // TODO: deprecated - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.enableDynamicPayloads(); - mNrf24.setAutoAck(true); - mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms - mNrf24.write(buf, len); - - // Try to avoid zero payload acks (has no effect) - mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated - mRxChIdx = 0; - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.setAutoAck(false); - mNrf24.setRetries(0, 0); - mNrf24.disableDynamicPayloads(); - mNrf24.setCRCLength(RF24_CRC_DISABLED); - mNrf24.startListening(); - - RESTORE_IRQ; - mSendCnt++; - } - - uint8_t getTxNxtChannel(void) { - - if(++mTxChIdx >= RF_CHANNELS) - mTxChIdx = 0; - return mRfChLst[mTxChIdx]; - } - - uint8_t getRxNxtChannel(void) { - - if(++mRxChIdx >= RF_CHANNELS) - mRxChIdx = 0; - return mRfChLst[mRxChIdx]; - } - - uint64_t DTU_RADIO_ID; - - uint8_t mTxCh; - uint8_t mTxChIdx; - - uint8_t mRfChLst[RF_CHANNELS]; - - uint8_t mRxChIdx; - uint16_t mRxLoopCnt; - - RF24 mNrf24; - BUFFER *mBufCtrl; - uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; - - DevControlCmdType DevControlCmd; - - volatile bool mIrqRcvd; -}; - -#endif /*__RADIO_H__*/ diff --git a/tools/esp8266/hmSystem.h b/tools/esp8266/hmSystem.h deleted file mode 100644 index ee7c180c..00000000 --- a/tools/esp8266/hmSystem.h +++ /dev/null @@ -1,112 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __HM_SYSTEM_H__ -#define __HM_SYSTEM_H__ - -#include "hmInverter.h" -#include "hmRadio.h" -#include "CircularBuffer.h" - -typedef CircularBuffer BufferType; -typedef HmRadio RadioType; - -template > -class HmSystem { - public: - typedef RADIO RadioType; - RadioType Radio; - typedef BUFFER BufferType; - BufferType BufCtrl; - //DevControlCmdType DevControlCmd; - - HmSystem() { - mNumInv = 0; - } - ~HmSystem() { - // TODO: cleanup - } - - void setup() { - Radio.setup(&BufCtrl); - } - - void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) { - Radio.setup(&BufCtrl, ampPwr, irqPin, cePin, csPin); - } - - INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint16_t chMaxPwr[]) { - DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); - if(MAX_INVERTER <= mNumInv) { - DPRINT(DBG_WARN, F("max number of inverters reached!")); - return NULL; - } - INVERTERTYPE *p = &mInverter[mNumInv]; - p->id = mNumInv; - p->serial.u64 = serial; - memcpy(p->chMaxPwr, chMaxPwr, (4*2)); - DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->serial.b[5], HEX)); - DPRINTLN(DBG_VERBOSE, " " + String(p->serial.b[4], HEX)); - if(p->serial.b[5] == 0x11) { - switch(p->serial.b[4]) { - case 0x21: p->type = INV_TYPE_1CH; break; - case 0x41: p->type = INV_TYPE_2CH; break; - case 0x61: p->type = INV_TYPE_4CH; break; - default: - DPRINT(DBG_ERROR, F("unknown inverter type: 11")); - DPRINTLN(DBG_ERROR, String(p->serial.b[4], HEX)); - break; - } - } - else - DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); - - p->init(); - uint8_t len = (uint8_t)strlen(name); - strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); - - mNumInv ++; - return p; - } - - INVERTERTYPE *findInverter(uint8_t buf[]) { - DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); - INVERTERTYPE *p; - for(uint8_t i = 0; i < mNumInv; i++) { - p = &mInverter[i]; - if((p->serial.b[3] == buf[0]) - && (p->serial.b[2] == buf[1]) - && (p->serial.b[1] == buf[2]) - && (p->serial.b[0] == buf[3])) - return p; - } - return NULL; - } - - INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { - DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); - if(pos >= MAX_INVERTER) - return NULL; - else if((mInverter[pos].initialized && mInverter[pos].serial.u64 != 0ULL) || false == check) - return &mInverter[pos]; - else - return NULL; - } - - uint8_t getNumInverters(void) { - DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getNumInverters")); - return mNumInv; - } - - void enableDebug() { - Radio.enableDebug(); - } - - private: - INVERTERTYPE mInverter[MAX_INVERTER]; - uint8_t mNumInv; -}; - -#endif /*__HM_SYSTEM_H__*/ diff --git a/tools/esp8266/html/api.js b/tools/esp8266/html/api.js deleted file mode 100644 index fe9d543c..00000000 --- a/tools/esp8266/html/api.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * GENERIC FUNCTIONS - */ - -function topnav() { - toggle("topnav"); -} - -function parseMenu(obj) { - var e = document.getElementById("topnav"); - e.innerHTML = ""; - for(var i = 0; i < obj["name"].length; i ++) { - if(obj["name"][i] == "-") - e.appendChild(span("", ["seperator"])); - else { - var l = link(obj["link"][i], obj["name"][i], obj["trgt"][i]); - if(obj["link"][i] == window.location.pathname) - l.classList.add("active"); - e.appendChild(l); - } - } -} - -function parseVersion(obj) { - document.getElementById("version").appendChild( - link("https://github.com/lumapu/ahoy/commits/" + obj["build"], "Git SHA: " + obj["build"] + " :: " + obj["version"], "_blank") - ); -} - -function setHide(id, hide) { - var elm = document.getElementById(id); - if(hide) { - if(!elm.classList.contains("hide")) - elm.classList.add("hide"); - } - else - elm.classList.remove('hide'); -} - - -function toggle(id) { - var e = document.getElementById(id); - if(!e.classList.contains("hide")) - e.classList.add("hide"); - else - e.classList.remove('hide'); -} - -function getAjax(url, ptr, method="GET", json=null) { - var xhr = new XMLHttpRequest(); - if(xhr != null) { - xhr.open(method, url, true); - xhr.onreadystatechange = p; - if("POST" == method) - xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - xhr.send(json); - } - function p() { - if(xhr.readyState == 4) { - if(null != xhr.responseText) { - if(null != ptr) - ptr(JSON.parse(xhr.responseText)); - } - } - } -} - -/** - * CREATE DOM FUNCTIONS - */ - -function des(val) { - e = document.createElement('p'); - e.classList.add("subdes"); - e.innerHTML = val; - return e; -} - -function lbl(htmlfor, val, cl=null, id=null) { - e = document.createElement('label'); - e.htmlFor = htmlfor; - e.innerHTML = val; - if(null != cl) e.classList.add(...cl); - if(null != id) e.id = id; - return e; -} - -function inp(name, val, max=32, cl=["text"], id=null, type=null) { - e = document.createElement('input'); - e.classList.add(...cl); - e.name = name; - e.value = val; - if(null != type) e.maxLength = max; - if(null != id) e.id = id; - if(null != type) e.type = type; - return e; -} - -function sel(name, opt, selId) { - e = document.createElement('select'); - e.name = name; - for(it of opt) { - o = document.createElement('option'); - o.value = it[0]; - o.innerHTML = it[1]; - if(it[0] == selId) - o.selected = true; - e.appendChild(o); - } - return e; -} - -function selDelAllOpt(sel) { - var i, l = sel.options.length - 1; - for(i = l; i >= 0; i--) { - sel.remove(i); - } -} - -function opt(val, html) { - o = document.createElement('option'); - o.value = val; - o.innerHTML = html; - e.appendChild(o); - return o; -} - -function div(cl) { - e = document.createElement('div'); - e.classList.add(...cl); - return e; -} - -function span(val, cl=null, id=null) { - e = document.createElement('span'); - e.innerHTML = val; - if(null != cl) e.classList.add(...cl); - if(null != id) e.id = id; - return e; -} - -function br() { - return document.createElement('br'); -} - -function link(dst, text, target=null) { - var a = document.createElement('a'); - var t = document.createTextNode(text); - a.href = dst; - if(null != target) - a.target = target; - a.appendChild(t); - return a; -} diff --git a/tools/esp8266/html/convert.py b/tools/esp8266/html/convert.py deleted file mode 100755 index eb41fb7b..00000000 --- a/tools/esp8266/html/convert.py +++ /dev/null @@ -1,85 +0,0 @@ -import re -import os -import gzip -import glob - -from pathlib import Path - -def convert2Header(inFile, compress): - fileType = inFile.split(".")[1] - define = inFile.split(".")[0].upper() - define2 = inFile.split(".")[1].upper() - inFileVarName = inFile.replace(".", "_") - print(inFile + ", compress: " + str(compress)) - - if os.getcwd()[-4:] != "html": - outName = "html/" + "h/" + inFileVarName + ".h" - inFile = "html/" + inFile - Path("html/h").mkdir(exist_ok=True) - else: - outName = "h/" + inFileVarName + ".h" - Path("h").mkdir(exist_ok=True) - - f = open(inFile, "r") - data = f.read() - f.close() - - if fileType == "html": - if False == compress: - data = data.replace('\n', '') - data = re.sub(r"\>\s+\<", '><', data) # whitespaces between xml tags - data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript - length = len(data) # get unescaped length - if False == compress: - data = re.sub(r"\"", '\\\"', data) # escape quotation marks - elif fileType == "js": - #data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript - #data = re.sub(r"\s?(\=|\!\=|\{|,)+\s?", r'\1', data) # whitespaces inner javascript - length = len(data) # get unescaped length - if False == compress: - data = re.sub(r"\"", '\\\"', data) # escape quotation marks - else: - data = data.replace('\n', '') - data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css - length = len(data) # get unescaped length # get unescaped length - - f = open(outName, "w") - f.write("#ifndef __{}_{}_H__\n".format(define, define2)) - f.write("#define __{}_{}_H__\n".format(define, define2)) - if compress: - zipped = gzip.compress(bytes(data, 'utf-8')) - zippedStr = "" - for i in range(len(zipped)): - zippedStr += "0x{:02x}".format(zipped[i]) #hex(zipped[i]) - if (i + 1) != len(zipped): - zippedStr += ", " - if (i + 1) % 16 == 0 and i != 0: - zippedStr += "\n" - f.write("#define {}_len {}\n".format(inFileVarName, len(zipped))) - f.write("const uint8_t {}[] PROGMEM = {{\n{}}};\n".format(inFileVarName, zippedStr)) - else: - f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFileVarName, data)) - f.write("const uint32_t {}_len = {};\n".format(inFileVarName, length)) - f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) - f.close() - -# delete all files in the 'h' dir, but ignore 'favicon_ico_gz.h' -dir = 'h' -if os.getcwd()[-4:] != "html": - dir = "html/" + dir - -for f in os.listdir(dir): - if not f.startswith('favicon_ico_gz'): - os.remove(os.path.join(dir, f)) - -# grab all files with following extensions -if os.getcwd()[-4:] != "html": - os.chdir('./html') -types = ('*.html', '*.css', '*.js') # the tuple of file types -files_grabbed = [] -for files in types: - files_grabbed.extend(glob.glob(files)) - -# go throw the array -for val in files_grabbed: - convert2Header(val, True) diff --git a/tools/esp8266/html/h/favicon_ico_gz.h b/tools/esp8266/html/h/favicon_ico_gz.h deleted file mode 100644 index b978fb85..00000000 --- a/tools/esp8266/html/h/favicon_ico_gz.h +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef __FAVICON_ICO_GZ_H__ -#define __FAVICON_ICO_GZ_H__ -#define favicon_ico_gz_len 1533 -const uint8_t favicon_ico_gz[] PROGMEM = {0x1f, 0x8b, 0x08, 0x08, 0xf2, 0xc5, 0xd5, 0x62, 0x04, 0x00, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, -0x6e, 0x2e, 0x69, 0x63, 0x6f, 0x00, 0xed, 0x5c, 0x49, 0x68, 0x13, 0x51, 0x18, 0xfe, 0x62, 0xa3, -0x51, 0x28, 0xd6, 0x83, 0x82, 0xa0, 0x98, 0xb8, 0x1c, 0xbc, 0x59, 0x11, 0x5c, 0x50, 0xac, 0x88, -0x8a, 0xb8, 0xdd, 0x3c, 0x89, 0xd0, 0x93, 0x7a, 0x53, 0x51, 0x9b, 0x80, 0x4b, 0x46, 0xad, 0xfb, -0xd2, 0xb4, 0x2e, 0xb8, 0xa3, 0xc6, 0xba, 0xe1, 0x02, 0xae, 0xad, 0x0a, 0x26, 0x3d, 0xe8, 0xc5, -0x83, 0x57, 0x31, 0x2d, 0xc1, 0x8b, 0xb7, 0x92, 0x63, 0x0e, 0xa1, 0xcf, 0xff, 0xcf, 0xbc, 0xc9, -0x32, 0xa4, 0x66, 0xcf, 0x4b, 0xf3, 0xfa, 0xc3, 0xc7, 0x97, 0xcc, 0xcc, 0xcb, 0xf7, 0xbe, 0x6f, -0x26, 0x6f, 0x26, 0xf3, 0x92, 0x00, 0x0e, 0x34, 0x61, 0xda, 0x34, 0x66, 0x0f, 0xf6, 0x38, 0x81, -0xa5, 0x00, 0x3c, 0x1e, 0xf3, 0xf9, 0x53, 0x5a, 0x7e, 0x8f, 0x96, 0xad, 0x59, 0x63, 0x3e, 0x5f, -0xb8, 0x16, 0xd8, 0x30, 0x03, 0x58, 0x48, 0xdb, 0xd0, 0x2a, 0x5a, 0x62, 0x2e, 0x1f, 0xad, 0x06, -0xc3, 0x5d, 0xc6, 0x60, 0x38, 0x20, 0x08, 0x5e, 0xf1, 0x08, 0x77, 0x09, 0x42, 0xf4, 0xe2, 0xd7, -0x67, 0x2f, 0x56, 0xf5, 0x7b, 0x21, 0x18, 0xfc, 0xb8, 0x63, 0x0b, 0x7e, 0x11, 0x84, 0x77, 0x0b, -0xee, 0x2e, 0x9a, 0x03, 0x6f, 0xab, 0x1b, 0x82, 0x31, 0x38, 0x10, 0x38, 0xce, 0xaf, 0x51, 0x72, -0x7b, 0xd2, 0xe6, 0xf6, 0xbf, 0xbf, 0xf6, 0xcc, 0x2e, 0xbd, 0x7d, 0xf2, 0x35, 0x76, 0x88, 0x20, -0xf6, 0x96, 0xde, 0x3e, 0xe0, 0xa5, 0x76, 0xad, 0xd4, 0xfe, 0x64, 0xc9, 0xed, 0xa9, 0xff, 0xd4, -0xd6, 0x4f, 0x68, 0x2f, 0xb9, 0x3d, 0xf5, 0x9f, 0x70, 0xb8, 0xaf, 0x03, 0x4b, 0x4b, 0x6e, 0x4f, -0xfd, 0xe7, 0x7d, 0x4b, 0x6d, 0x4f, 0x96, 0xd3, 0xde, 0x7a, 0x8d, 0x72, 0xda, 0x73, 0x7d, 0xdb, -0x8f, 0x29, 0x45, 0xb5, 0x7f, 0x84, 0x4e, 0xfb, 0x71, 0xda, 0xb1, 0x19, 0x9d, 0x05, 0xb5, 0xef, -0xc5, 0xa5, 0xd1, 0x8e, 0x75, 0xef, 0x56, 0x9c, 0xfa, 0x6f, 0xfb, 0x87, 0x58, 0x86, 0x3c, 0x75, -0x70, 0x13, 0x96, 0xe5, 0x6a, 0x1f, 0x19, 0xe8, 0x6a, 0x47, 0x81, 0xd5, 0xea, 0x41, 0x7b, 0x56, -0xfb, 0x50, 0xe0, 0x72, 0xae, 0xed, 0xbe, 0x1a, 0x98, 0xcc, 0xc8, 0xf9, 0x1a, 0x73, 0x70, 0x9a, -0xda, 0x8f, 0xda, 0xf6, 0xb9, 0x81, 0x49, 0xfd, 0x3e, 0xbc, 0xa5, 0xfd, 0xd7, 0xcf, 0xf9, 0x8f, -0xf6, 0x1a, 0xff, 0xeb, 0xe7, 0x04, 0x42, 0x13, 0xc3, 0x20, 0x84, 0x00, 0x57, 0x14, 0x68, 0x89, -0x01, 0xee, 0x38, 0xb0, 0xfa, 0x08, 0x70, 0x64, 0xb5, 0x39, 0xce, 0x78, 0x08, 0x6b, 0x0a, 0x1f, -0x67, 0x2c, 0x0c, 0xf3, 0x72, 0xf1, 0x18, 0xab, 0xe4, 0x3e, 0x4f, 0x23, 0x88, 0x37, 0xbc, 0xae, -0xcf, 0x07, 0xaf, 0xf9, 0x1e, 0x48, 0x83, 0x97, 0xf1, 0xba, 0x43, 0x5b, 0xf0, 0x86, 0x8f, 0x87, -0x4c, 0xd0, 0xfe, 0x5d, 0xc5, 0xeb, 0x28, 0x97, 0x61, 0x33, 0xdf, 0x34, 0x78, 0x39, 0x8f, 0x55, -0xaa, 0xf4, 0x23, 0x03, 0x81, 0x53, 0x56, 0x1f, 0x54, 0xe8, 0xb3, 0xe6, 0x50, 0x28, 0x70, 0xde, -0xea, 0x83, 0x0a, 0xfd, 0x64, 0x1f, 0xc2, 0x81, 0x93, 0xbc, 0x0d, 0xf1, 0x01, 0x15, 0xfa, 0x49, -0x0c, 0x74, 0x9d, 0xe3, 0xed, 0x94, 0xe9, 0x13, 0x78, 0x1f, 0x24, 0xf5, 0x7b, 0xb1, 0x54, 0x89, -0x3e, 0xef, 0x7f, 0x59, 0x7c, 0xce, 0x53, 0xa5, 0x2f, 0x7a, 0xe0, 0x92, 0x7d, 0x38, 0xad, 0x44, -0x9f, 0xf6, 0x3f, 0x9f, 0xb3, 0x65, 0x1f, 0x3a, 0x95, 0xe8, 0x9b, 0xbe, 0x8f, 0x59, 0x7d, 0x50, -0xa4, 0x9f, 0xea, 0x03, 0xe9, 0xaf, 0x57, 0xa4, 0x9f, 0xea, 0x83, 0x42, 0x7d, 0xbe, 0x7e, 0xd9, -0xcf, 0xeb, 0x3e, 0xf9, 0xd0, 0xae, 0x44, 0x9f, 0xf7, 0xbf, 0x2c, 0x3a, 0x87, 0x9f, 0x56, 0xa9, -0x6f, 0xf5, 0x41, 0xa5, 0xbe, 0xbc, 0x0e, 0x3d, 0xa3, 0x52, 0xdf, 0xea, 0x83, 0x4a, 0x7d, 0xae, -0x0f, 0x87, 0x30, 0x5f, 0xa5, 0x3e, 0x57, 0x0d, 0xf4, 0x8d, 0x7c, 0xfa, 0xa4, 0x67, 0x54, 0x49, -0xff, 0x86, 0x10, 0x74, 0x29, 0x9b, 0xbf, 0x1c, 0xa4, 0xd9, 0x5d, 0x51, 0xfd, 0x20, 0x6e, 0x16, -0xa8, 0x9d, 0xd5, 0x87, 0x8a, 0xe8, 0x17, 0xaf, 0x9d, 0xea, 0x03, 0xe9, 0xf6, 0x94, 0xa9, 0x7f, -0xab, 0x44, 0xed, 0xac, 0x3e, 0x94, 0xa4, 0xff, 0x1c, 0xcd, 0x65, 0x6a, 0xa7, 0xfa, 0x60, 0x6c, -0x47, 0x73, 0xb1, 0xfa, 0xd5, 0xa8, 0x42, 0xf4, 0x23, 0xe1, 0xc0, 0x15, 0x54, 0xa9, 0x16, 0xbb, -0x71, 0xe5, 0x7f, 0xfa, 0x91, 0x50, 0xf7, 0x6d, 0x21, 0x8c, 0x09, 0xa8, 0x5e, 0x39, 0x48, 0xf3, -0x6a, 0x86, 0x7e, 0x59, 0xda, 0xfd, 0x07, 0x31, 0x97, 0x81, 0xe2, 0x2a, 0xdd, 0x87, 0xb4, 0xfe, -0x9d, 0x62, 0xb5, 0xe9, 0xde, 0xcb, 0x1c, 0x3a, 0xef, 0x0c, 0xf5, 0x77, 0xe0, 0xcf, 0x17, 0x1f, -0xe6, 0xa1, 0xb8, 0xe2, 0x3e, 0x5c, 0x23, 0x94, 0xa4, 0xfd, 0xd1, 0x07, 0x0f, 0x5d, 0x03, 0x45, -0xad, 0x73, 0x3f, 0x3f, 0xe6, 0x65, 0x28, 0xae, 0x92, 0x7d, 0x70, 0x4e, 0x9e, 0xe6, 0xe9, 0x24, -0xac, 0x23, 0x2c, 0x20, 0x4c, 0x27, 0x34, 0x13, 0x78, 0xf9, 0x24, 0xc2, 0x44, 0x7e, 0x7c, 0x96, -0xb0, 0x91, 0xb0, 0x90, 0x30, 0xd3, 0x5c, 0xe7, 0x6c, 0x26, 0x4c, 0x25, 0xb4, 0x30, 0x02, 0x84, -0x9f, 0x84, 0x58, 0x1a, 0xee, 0xb8, 0xc7, 0xd9, 0x96, 0xf0, 0x38, 0xfd, 0x23, 0x1e, 0xa7, 0x10, -0x1e, 0x97, 0x10, 0x51, 0x0b, 0x2d, 0x23, 0xfe, 0x98, 0x3b, 0xd1, 0x16, 0x6f, 0x8b, 0xbb, 0x13, -0x0b, 0xe5, 0x3d, 0x0a, 0xa3, 0xf8, 0xfb, 0x14, 0xb9, 0x3e, 0xbb, 0x45, 0xac, 0xed, 0xe5, 0x67, -0x38, 0x2f, 0x8f, 0xa1, 0x79, 0xd1, 0x8b, 0x5d, 0xb6, 0xeb, 0x89, 0x61, 0xce, 0x35, 0x0f, 0x86, -0x6d, 0xf7, 0xc1, 0x76, 0xf1, 0x58, 0x9b, 0x17, 0x9b, 0xe1, 0xb5, 0x8d, 0x09, 0x91, 0xf4, 0xfb, -0x21, 0x8d, 0x02, 0xee, 0x93, 0x68, 0xe7, 0x3f, 0x6f, 0x06, 0x1a, 0xf8, 0xe7, 0x1a, 0x0a, 0x75, -0x9d, 0xd0, 0xcd, 0xff, 0xe0, 0x40, 0xf7, 0xa5, 0xbc, 0x19, 0x34, 0xb2, 0x7f, 0xf3, 0xda, 0xe0, -0xa2, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x80, 0xef, 0x5b, 0xea, 0xe4, 0xdf, 0xcc, 0xa0, 0xeb, -0x82, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x20, 0x12, 0xee, 0xee, 0xd4, 0xc9, 0x3f, 0x21, 0x35, -0x77, 0x91, 0xce, 0x20, 0xb0, 0x5b, 0x27, 0xff, 0xe9, 0xb9, 0x93, 0x74, 0xe9, 0xe6, 0xdf, 0xca, -0x40, 0x67, 0xff, 0xd6, 0xdc, 0x91, 0xd6, 0xfe, 0x93, 0xe8, 0xf6, 0xdb, 0xfc, 0xb7, 0xe7, 0xf5, -0xde, 0x48, 0xfe, 0x6d, 0xe3, 0xbf, 0x35, 0x7f, 0xa5, 0xb3, 0xff, 0x82, 0x32, 0x68, 0x70, 0xff, -0x32, 0x83, 0x33, 0xba, 0xf9, 0x17, 0x0f, 0x30, 0x2b, 0xeb, 0x79, 0x10, 0x97, 0xb5, 0xf2, 0x4f, -0xe3, 0x3f, 0xc1, 0x6f, 0xcb, 0xe0, 0xac, 0x56, 0xfe, 0xe5, 0xdc, 0xad, 0x3d, 0x03, 0xcd, 0xfc, -0x67, 0x65, 0xc0, 0xf3, 0x0a, 0xf4, 0xbc, 0x5b, 0x33, 0xff, 0x3c, 0x97, 0x74, 0xd4, 0x9e, 0x81, -0x56, 0xfe, 0x73, 0x67, 0xd0, 0xa3, 0x95, 0x7f, 0x13, 0x87, 0x6d, 0x19, 0xcc, 0xd6, 0xcc, 0x3f, -0x1f, 0xf3, 0x47, 0x32, 0xb7, 0xd5, 0xce, 0xbf, 0x3c, 0x0e, 0x34, 0xf7, 0x9f, 0xcc, 0x40, 0x73, -0xff, 0x3c, 0x26, 0x6e, 0xb3, 0xda, 0xc8, 0xef, 0x57, 0x5d, 0xd1, 0xca, 0xbf, 0x6d, 0xfc, 0x17, -0x34, 0x15, 0xd9, 0xe7, 0xc5, 0x55, 0x5d, 0xfd, 0x17, 0x98, 0x41, 0x43, 0xfb, 0xb7, 0x32, 0xa0, -0xef, 0xd8, 0x5d, 0xd3, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf, 0xcc, 0x40, 0x57, 0xff, 0x56, -0x06, 0xe4, 0xf9, 0xba, 0xae, 0xfe, 0x33, 0x33, 0xd0, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf, -0x18, 0x0f, 0x4e, 0xeb, 0xea, 0xdf, 0xaa, 0x71, 0xff, 0xe3, 0xfe, 0x1b, 0xc4, 0xff, 0x4e, 0x94, -0x59, 0x1d, 0x5b, 0xb1, 0x33, 0xaf, 0xf7, 0x7a, 0xf4, 0x1f, 0xc4, 0x13, 0xf1, 0x9c, 0x7e, 0x4e, -0x5a, 0x66, 0x19, 0x06, 0x26, 0x50, 0x06, 0xf7, 0xc7, 0x94, 0xff, 0x0a, 0x79, 0x2f, 0x2a, 0x83, -0xfa, 0xf1, 0xff, 0x54, 0x7c, 0x85, 0x13, 0x15, 0x2e, 0xce, 0x80, 0x7e, 0x83, 0xf2, 0xa0, 0xce, -0xfd, 0x57, 0xc5, 0xbb, 0x55, 0xdb, 0xb7, 0xa3, 0x89, 0x33, 0xa8, 0x53, 0xff, 0xcf, 0xaa, 0xe9, -0x3d, 0x33, 0x03, 0xf2, 0xfb, 0xb0, 0xce, 0xfc, 0xd7, 0xc4, 0xbb, 0x3d, 0x83, 0x3a, 0xf1, 0x5f, -0x53, 0xef, 0x59, 0xef, 0x85, 0xad, 0x08, 0x2a, 0xf6, 0xff, 0x5c, 0x85, 0x77, 0x7b, 0x06, 0x8a, -0xfc, 0x2b, 0xf5, 0x6e, 0xcf, 0xa0, 0xa6, 0xfe, 0x83, 0x58, 0x54, 0xc9, 0xf3, 0x7b, 0xb9, 0xc5, -0x19, 0xd0, 0xff, 0x9c, 0x2c, 0xaa, 0x95, 0xff, 0xb1, 0x50, 0xe3, 0xfe, 0xc7, 0xfd, 0xb7, 0x56, -0xc8, 0xff, 0x50, 0xb8, 0xeb, 0x3e, 0xc6, 0x58, 0xd1, 0x7f, 0xfd, 0xdc, 0x6f, 0xad, 0x8c, 0xff, -0x17, 0x3f, 0x7e, 0xdc, 0x9c, 0x88, 0x31, 0x56, 0xdb, 0xe9, 0x2f, 0x8c, 0xc8, 0x5f, 0x6f, 0x99, -0xfe, 0xc7, 0xa4, 0xf7, 0x62, 0x32, 0x68, 0x54, 0xef, 0xb6, 0x0c, 0x1e, 0x17, 0xe9, 0xff, 0x65, -0x23, 0x78, 0x2f, 0x24, 0x03, 0x95, 0xde, 0x69, 0x1e, 0xf8, 0x14, 0x03, 0x35, 0x28, 0xce, 0x60, -0x91, 0x1b, 0x4f, 0x6c, 0xfe, 0xd5, 0x79, 0xf7, 0xe1, 0x44, 0x6a, 0xce, 0xab, 0x03, 0xe7, 0x51, -0x83, 0xca, 0x91, 0x41, 0xe6, 0x77, 0xfc, 0xdf, 0xfd, 0xfe, 0xd0, 0xe3, 0x42, 0x0d, 0xea, 0x93, -0x17, 0xc7, 0xed, 0xf3, 0xbe, 0x74, 0x1c, 0x9c, 0x43, 0x0d, 0x6a, 0xc9, 0x12, 0x4c, 0x24, 0xdf, -0xaf, 0x6c, 0xfe, 0xdf, 0xab, 0xf4, 0xae, 0x28, 0x83, 0xd7, 0x84, 0xba, 0xf1, 0x6e, 0x81, 0xb7, -0x41, 0x0d, 0xca, 0xca, 0x00, 0xcb, 0x11, 0x75, 0xd1, 0xd7, 0x14, 0x0d, 0xc9, 0x21, 0xc9, 0x51, -0xc9, 0xfb, 0x24, 0x2f, 0x97, 0x3c, 0x5b, 0xf2, 0x54, 0xc9, 0x2e, 0xc9, 0x4d, 0x7d, 0x26, 0x3b, -0xe2, 0x26, 0xc3, 0x62, 0x9f, 0xe4, 0xe5, 0x92, 0x57, 0x49, 0x5e, 0x2d, 0xb9, 0x4d, 0xf2, 0x4a, -0xc3, 0xe4, 0x15, 0x21, 0xb9, 0x7d, 0x54, 0x72, 0xbb, 0xe4, 0x56, 0xc9, 0x33, 0x25, 0x37, 0x4b, -0x76, 0x49, 0x6e, 0x92, 0xec, 0xb0, 0xf4, 0xec, 0x1c, 0x93, 0x1c, 0x97, 0x9c, 0x90, 0x3c, 0x22, -0x59, 0x58, 0x7c, 0x46, 0xf2, 0x77, 0xc9, 0x7f, 0x25, 0x8b, 0x82, 0xd8, 0x41, 0x7f, 0x3b, 0x91, -0xec, 0x8f, 0x10, 0x21, 0x66, 0xfe, 0x67, 0x06, 0xe6, 0x16, 0x21, 0x62, 0xcc, 0x6e, 0x21, 0xe2, -0xcc, 0x6d, 0x42, 0x24, 0x98, 0xfd, 0x42, 0x8c, 0x30, 0x0b, 0x2a, 0xf6, 0xcf, 0x7c, 0x87, 0x72, -0x61, 0x4e, 0xe4, 0x67, 0x3f, 0xf3, 0x08, 0x7d, 0x28, 0x4f, 0x16, 0x1c, 0x26, 0x1b, 0x4d, 0xb4, -0x90, 0x56, 0x85, 0x5c, 0xb4, 0x11, 0x6d, 0x1a, 0x6d, 0x21, 0x55, 0x52, 0x8e, 0x31, 0xf8, 0x31, -0x2f, 0xe3, 0x75, 0xff, 0x00, 0xd3, 0x39, 0x74, 0x2c, 0x6e, 0x57, 0x00, 0x00}; -#endif /*__FAVICON_ICO_GZ_H__*/ diff --git a/tools/esp8266/html/index.html b/tools/esp8266/html/index.html deleted file mode 100644 index 8b8157b4..00000000 --- a/tools/esp8266/html/index.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - Index - - - - - - -
-
- -

Uptime:

-

ESP-Time:

-
- Sunrise:
- Sunset: -
-

WiFi RSSI: dBm

-

- Statistics: -


-                    

-                    

-                

-

Every seconds the values are updated

- -
- Discuss with us on Discord
-

Documentation

- ahoydtu.de - -

Support this project:

- -

- This project was started from this discussion. (Mikrocontroller.net) -

-
-
-
- - - - diff --git a/tools/esp8266/html/setup.html b/tools/esp8266/html/setup.html deleted file mode 100644 index 44b7578a..00000000 --- a/tools/esp8266/html/setup.html +++ /dev/null @@ -1,451 +0,0 @@ - - - - Setup - - - - - - - -
-
- ERASE SETTINGS (not WiFi) - -
-
- Device Host Name - - - -
- - -
-
- WiFi -

Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

- -
- - - - - - -
-
- - -
-
- Inverter -

- -

General

- - - - -
-
- - -
-
- NTP Server - - - - - - - - -
-
- - -
-
- Sunrise & Sunset - - - - -
- -
-
-
- - -
-
- MQTT - - - - - - - - - - - - - -
-
- - -
-
- System Config -

Pinout (Wemos)

-
- -

Radio (NRF24L01+)

-
- -

Serial Console

- -
- -
- - -
-
- - - -
-
- Download your settings (JSON file) (only saved values) -
-
-
- - - - diff --git a/tools/esp8266/html/style.css b/tools/esp8266/html/style.css deleted file mode 100644 index eab8da8a..00000000 --- a/tools/esp8266/html/style.css +++ /dev/null @@ -1,416 +0,0 @@ -html, body { - font-family: Arial; - margin: 0; - padding: 0; - height: 100%; - min-height: 100%; -} - -h2 { - padding-left: 10px; -} - -.topnav { - background-color: #333; - position: fixed; - top: 0; - width: 100%; -} - -.topnav a { - color: #fff; - padding: 14px 14px; - text-decoration: none; - font-size: 17px; - display: block; - height: 20px; -} - -#topnav a { - color: #fff; -} - -.topnav a.icon { - background: #333; - display: block; - position: absolute; - left: 0; - top: 0; -} - -.topnav a:hover { - background-color: #044e86 !important; - color: #000; -} - -.title { - background-color: #006ec0; - color: #fff !important; - padding-left: 80px !important -} - -.topnav .icon span { - display: block; - width: 30px; - height: 3px; - margin-bottom: 5px; - position: relative; - background: #fff; - border-radius: 2px; -} - -.topnav .active { - background-color: #555; -} - -span.seperator { - width: 100%; - height: 1px; - margin: 5px 0px 5px; - background-color: #494949; - display: block; -} - -#wrapper { - min-height: 100%; -} - -#content { - padding: 50px 20px 120px 20px; - overflow: auto; -} - -#footer { - height: 121px; - margin-top: -121px; - background-color: #555; - width: 100%; - font-size: 13px; -} - -#footer .right { - color: #bbb; - margin: 23px 25px; - text-align: right; -} - -#footer .left { - color: #bbb; - margin: 23px 0px 0px 25px; -} - -#footer ul { - list-style-type: none; - margin: 20px auto; - padding: 0; -} - -#footer ul li, #footer a { - color: #bbb; - margin-bottom: 10px; - padding-left: 5px; - font-size: 13px; -} - -#footer a:hover { - color: #fff; -} - -.hide { - display: none; -} - -@media only screen and (min-width: 992px) { - .topnav { - width: 230px !important; - height: 100%; - } - - .topnav a.icon { - display: none !important; - } - - .topnav a { - padding: 14px 24px; - } - - .topnav .title { - padding-left: 24px !important; - } - - .topnav .hide { - display: block; - } - - #content { - padding: 15px 15px 120px 250px; - } - - #footer .left { - margin-left: 250px !important; - } -} - -/** old CSS below **/ - -p { - text-align: justify; - font-size: 13pt; -} - -p.lic, p.lic a { - font-size: 8pt; - color: #999; -} - -.des { - margin-top: 20px; - font-size: 13pt; - color: #006ec0; -} - -.s_active, .s_collapsible:hover { - background-color: #044e86; - color: #fff; -} - -.s_content { - display: none; - overflow: hidden; -} - -.s_collapsible { - background-color: #006ec0; - color: white; - cursor: pointer; - padding: 18px; - width: 100%; - border: none; - text-align: left; - outline: none; - font-size: 15px; - margin-bottom: 4px; -} - -.subdes { - font-size: 12pt; - color: #006ec0; - margin-left: 7px; -} - -.subsubdes { - font-size:12pt; - color:#006ec0; - margin: 0 0 7px 12px; -} - -a:link, a:visited { - text-decoration: none; - font-size: 13pt; - color: #006ec0; -} - -a:hover, a:focus { - color: #f00; -} - -a.btn { - background-color: #006ec0; - color: #fff; - padding: 7px 15px 7px 15px; - display: inline-block; -} - -a.btn:hover { - background-color: #044e86 !important; -} - -input, select { - padding: 7px; - font-size: 13pt; -} - -input.text, select { - width: 70%; - box-sizing: border-box; - margin-bottom: 10px; - border: 1px solid #ccc; -} - -input.sh { - max-width: 150px !important; - margin-right: 10px; -} - -input.btnDel { - background-color: #c00 !important; -} - -input.btn { - background-color: #006ec0; - color: #fff; - border: 0px; - padding: 7px 20px 7px 20px; - margin-bottom: 10px; - text-transform: uppercase; - cursor: pointer; -} - -input.btn:hover { - background-color: #044e86; -} - -input.cb { - margin-bottom: 20px; -} - -label { - width: 20%; - display: inline-block; - font-size: 12pt; - padding-right: 10px; - margin: 10px 0px 0px 15px; - vertical-align: top; -} - -pre { - white-space: pre-wrap; -} - -fieldset { - margin-bottom: 15px; -} - -.left { - float: left; -} - -.right { - float: right; -} - -div.ch-iv { - width: 100%; - background-color: #32b004; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch { - width: 220px; - min-height: 350px; - background-color: #006ec0; - display: inline-block; - margin: 0 20px 10px 0px; - overflow: auto; - padding-bottom: 20px; -} - -div.ch-all { - width: 100%; - background-color: #b06e04; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head { - color: #fff; - display: block; - width: 100%; - text-align: center; -} - -.subgrp { - float: left; - width: 220px; -} - -div.ch .unit, div.ch-iv .unit, div.ch-all .unit { - font-size: 19px; - margin-left: 10px; -} - -div.ch .value, div.ch-iv .value, div.ch-all .value { - margin-top: 20px; - font-size: 24px; -} - -div.ch .info, div.ch-iv .info, div.ch-all .info { - margin-top: 3px; - font-size: 10px; -} - -div.ch .head { - background-color: #003c80; - padding: 10px 0 10px 0; -} - -div.ch-all .head { - background-color: #8e5903; - padding: 10px 0 10px 0; -} - -div.ch-iv .head { - background-color: #1c6800; - padding: 10px 0 10px 0; -} - -div.iv { - max-width: 960px; - margin-bottom: 40px; -} - -div.ts { - font-size: 13px; - background-color: #ddd; - border-top: 7px solid #999; - padding: 7px; -} - -div.ModPwr, div.ModName { - width:70%; - display: inline-block; -} - -#note { - margin: 50px 10px 10px 10px; - padding-top: 10px; - width: 100%; - border-top: 1px solid #bbb; -} - -@media(max-width: 500px) { - div.ch .unit, div.ch-iv .unit { - font-size: 18px; - } - - div.ch { - width: 170px; - min-height: 100px - } - - .subgrp { - width: 180px; - } -} - -#serial { - width: 100%; -} - -#content .serial { - max-width: 1000px; -} - -.dot { - height: 15px; - width: 15px; - background-color: #f00; - border-radius: 50%; - display: inline-block; - margin-top: 15px; -} diff --git a/tools/esp8266/html/system.html b/tools/esp8266/html/system.html deleted file mode 100644 index 8a2a8ccc..00000000 --- a/tools/esp8266/html/system.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - System - - - - - - -
- -
- - - - diff --git a/tools/esp8266/html/update.html b/tools/esp8266/html/update.html deleted file mode 100644 index 39ad2a61..00000000 --- a/tools/esp8266/html/update.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - Update - - - - - - -
-
-
- Make sure that you have noted all your settings before starting an update. New versions may have changed their memory layout which can break your existing settings.
-
- Download your settings (JSON file) -
-

-
- -
-
-
- - - - diff --git a/tools/esp8266/html/visualization.html b/tools/esp8266/html/visualization.html deleted file mode 100644 index 51adf43b..00000000 --- a/tools/esp8266/html/visualization.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - Live - - - - - - - -
-
-
-

Every seconds the values are updated

-
-
- - - - diff --git a/tools/esp8266/mqtt.h b/tools/esp8266/mqtt.h deleted file mode 100644 index 6f6e997e..00000000 --- a/tools/esp8266/mqtt.h +++ /dev/null @@ -1,133 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __MQTT_H__ -#define __MQTT_H__ - -#ifdef ESP8266 - #include -#elif defined(ESP32) - #include -#endif - -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif -#include -#include "defines.h" - -class mqtt { - public: - mqtt() { - mClient = new PubSubClient(mEspClient); - mAddressSet = false; - - mLastReconnect = 0; - mTxCnt = 0; - - memset(mDevName, 0, DEVNAME_LEN); - } - - ~mqtt() { } - - void setup(mqttConfig_t *cfg, const char *devname) { - DPRINTLN(DBG_VERBOSE, F("mqtt.h:setup")); - mAddressSet = true; - - mCfg = cfg; - snprintf(mDevName, DEVNAME_LEN, "%s", devname); - - mClient->setServer(mCfg->broker, mCfg->port); - mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); - } - - void setCallback(MQTT_CALLBACK_SIGNATURE){ - mClient->setCallback(callback); - } - - void sendMsg(const char *topic, const char *msg) { - //DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg")); - char top[64]; - snprintf(top, 64, "%s/%s", mCfg->topic, topic); - sendMsg2(top, msg, false); - mTxCnt++; - } - - void sendMsg2(const char *topic, const char *msg, boolean retained) { - if(mAddressSet) { - if(!mClient->connected()) - reconnect(); - if(mClient->connected()) - mClient->publish(topic, msg, retained); - } - } - - bool isConnected(bool doRecon = false) { - //DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected")); - if(doRecon && !mClient->connected()) - reconnect(); - return mClient->connected(); - } - - void loop() { - //DPRINT(F("m")); - if(!mClient->connected()) - reconnect(); - mClient->loop(); - } - - uint32_t getTxCnt(void) { - return mTxCnt; - } - - private: - void reconnect(void) { - DPRINTLN(DBG_DEBUG, F("mqtt.h:reconnect")); - DPRINTLN(DBG_DEBUG, F("MQTT mClient->_state ") + String(mClient->state()) ); - - #ifdef ESP8266 - DPRINTLN(DBG_DEBUG, F("WIFI mEspClient.status ") + String(mEspClient.status()) ); - #endif - - boolean resub = false; - if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) { - mLastReconnect = millis(); - if(strlen(mDevName) > 0) { - // der Server und der Port müssen neu gesetzt werden, - // da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat. - mClient->setServer(mCfg->broker, mCfg->port); - mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); - - char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte - snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg->topic); - - if((strlen(mCfg->user) > 0) && (strlen(mCfg->pwd) > 0)) - resub = mClient->connect(mDevName, mCfg->user, mCfg->pwd, lwt, 0, false, "offline"); - else - resub = mClient->connect(mDevName, lwt, 0, false, "offline"); - // ein Subscribe ist nur nach einem connect notwendig - if(resub) { - char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte - // ToDo: "/devcontrol/#" is hardcoded - snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg->topic); - DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); - mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#" - } - } - } - } - - WiFiClient mEspClient; - PubSubClient *mClient; - - bool mAddressSet; - mqttConfig_t *mCfg; - char mDevName[DEVNAME_LEN]; - uint32_t mLastReconnect; - uint32_t mTxCnt; -}; - -#endif /*__MQTT_H_*/ diff --git a/tools/esp8266/platformio.ini b/tools/esp8266/platformio.ini deleted file mode 100644 index d9770802..00000000 --- a/tools/esp8266/platformio.ini +++ /dev/null @@ -1,109 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -[platformio] -src_dir = . - -[env] -framework = arduino - -build_flags = - -include "config.h" -; ;;;;; Possible Debug options ;;;;;; -; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level - ;-DDEBUG_ESP_PORT=Serial - ;-DDEBUG_ESP_CORE - ;-DDEBUG_ESP_WIFI - ;-DDEBUG_ESP_HTTP_CLIENT - ;-DDEBUG_ESP_HTTP_SERVER - ;-DDEBUG_ESP_OOM - -monitor_speed = 115200 - -extra_scripts = - pre:scripts/auto_firmware_version.py - pre:html/convert.py - -lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - knolleary/PubSubClient - bblanchon/ArduinoJson - ;esp8266/DNSServer - ;esp8266/EEPROM - ;esp8266/ESP8266WiFi - ;esp8266/SPI - ;esp8266/Ticker - -[env:esp8266-release] -platform = espressif8266 -board = esp12e -board_build.f_cpu = 80000000L -build_flags = -D RELEASE -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory - -[env:esp8266-debug] -platform = espressif8266 -board = esp12e -board_build.f_cpu = 80000000L -build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -build_type = debug -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory - -[env:esp8266-1m-release] -platform = espressif8266 -board = esp8285 -board_build.ldscript = eagle.flash.1m64.ld -board_build.f_cpu = 80000000L -build_flags = -D RELEASE -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory - -[env:esp8266-1m-debug] -platform = espressif8266 -board = esp8285 -board_build.ldscript = eagle.flash.1m64.ld -board_build.f_cpu = 80000000L -build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -build_type = debug -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory - -[env:esp32-wroom32-release] -platform = espressif32 -board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 -build_unflags = -std=gnu++11 -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory - -[env:esp32-wroom32-debug] -platform = espressif32 -board = lolin_d32 -build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14 -build_unflags = -std=gnu++11 -build_type = debug -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory diff --git a/tools/esp8266/scripts/getVersion.py b/tools/esp8266/scripts/getVersion.py deleted file mode 100644 index 84b45d92..00000000 --- a/tools/esp8266/scripts/getVersion.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from datetime import date - -def readVersion(path, infile): - f = open(path + infile, "r") - lines = f.readlines() - f.close() - - today = date.today() - search = ["_MAJOR", "_MINOR", "_PATCH"] - version = today.strftime("%y%m%d") + "_ahoy_" - versionnumber = "ahoy_v" - for line in lines: - if(line.find("VERSION_") != -1): - for s in search: - p = line.find(s) - if(p != -1): - version += line[p+13:].rstrip() + "." - versionnumber += line[p+13:].rstrip() + "." - - os.mkdir(path + ".pio/build/out/") - sha = os.getenv("SHA",default="sha") - versionout = version[:-1] + "_esp8266_" + sha + ".bin" - src = path + ".pio/build/esp8266-release/firmware.bin" - dst = path + ".pio/build/out/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_esp8266_1m_" + sha + ".bin" - src = path + ".pio/build/esp8266-1m-release/firmware.bin" - dst = path + ".pio/build/out/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_esp32_" + sha + ".bin" - src = path + ".pio/build/esp32-wroom32-release/firmware.bin" - dst = path + ".pio/build/out/" + versionout - os.rename(src, dst) - - print("::set-output name=name::" + versionnumber[:-1] ) - - -readVersion("../", "defines.h") diff --git a/tools/esp8266/web.cpp b/tools/esp8266/web.cpp deleted file mode 100644 index cbaf5f08..00000000 --- a/tools/esp8266/web.cpp +++ /dev/null @@ -1,512 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif - -#include "web.h" - -#include "html/h/index_html.h" -#include "html/h/style_css.h" -#include "html/h/api_js.h" -#include "html/h/favicon_ico_gz.h" -#include "html/h/setup_html.h" -#include "html/h/visualization_html.h" -#include "html/h/update_html.h" -#include "html/h/serial_html.h" -#include "html/h/system_html.h" - -const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"}; - -//----------------------------------------------------------------------------- -web::web(app *main, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]) { - mMain = main; - mSysCfg = sysCfg; - mConfig = config; - mStat = stat; - mVersion = version; - mWeb = new AsyncWebServer(80); - mEvts = new AsyncEventSource("/events"); - mApi = new webApi(mWeb, main, sysCfg, config, stat, version); - - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); - mSerialBufFill = 0; - mWebSerialTicker = 0; - mWebSerialInterval = 1000; // [ms] - mSerialAddTime = true; -} - - -//----------------------------------------------------------------------------- -void web::setup(void) { - DPRINTLN(DBG_VERBOSE, F("app::setup-begin")); - mWeb->begin(); - DPRINTLN(DBG_VERBOSE, F("app::setup-on")); - mWeb->on("/", HTTP_GET, std::bind(&web::onIndex, this, std::placeholders::_1)); - mWeb->on("/style.css", HTTP_GET, std::bind(&web::onCss, this, std::placeholders::_1)); - mWeb->on("/api.js", HTTP_GET, std::bind(&web::onApiJs, this, std::placeholders::_1)); - mWeb->on("/favicon.ico", HTTP_GET, std::bind(&web::onFavicon, this, std::placeholders::_1)); - mWeb->onNotFound ( std::bind(&web::showNotFound, this, std::placeholders::_1)); - mWeb->on("/reboot", HTTP_ANY, std::bind(&web::onReboot, this, std::placeholders::_1)); - mWeb->on("/system", HTTP_ANY, std::bind(&web::onSystem, this, std::placeholders::_1)); - mWeb->on("/erase", HTTP_ANY, std::bind(&web::showErase, this, std::placeholders::_1)); - mWeb->on("/factory", HTTP_ANY, std::bind(&web::showFactoryRst, this, std::placeholders::_1)); - - mWeb->on("/setup", HTTP_GET, std::bind(&web::onSetup, this, std::placeholders::_1)); - mWeb->on("/save", HTTP_ANY, std::bind(&web::showSave, this, std::placeholders::_1)); - - mWeb->on("/live", HTTP_ANY, std::bind(&web::onLive, this, std::placeholders::_1)); - mWeb->on("/api1", HTTP_POST, std::bind(&web::showWebApi, this, std::placeholders::_1)); - - - mWeb->on("/update", HTTP_GET, std::bind(&web::onUpdate, this, std::placeholders::_1)); - mWeb->on("/update", HTTP_POST, std::bind(&web::showUpdate, this, std::placeholders::_1), - std::bind(&web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - mWeb->on("/serial", HTTP_GET, std::bind(&web::onSerial, this, std::placeholders::_1)); - - - mEvts->onConnect(std::bind(&web::onConnect, this, std::placeholders::_1)); - mWeb->addHandler(mEvts); - - mApi->setup(); - - registerDebugCb(std::bind(&web::serialCb, this, std::placeholders::_1)); -} - - -//----------------------------------------------------------------------------- -void web::loop(void) { - mApi->loop(); - - if(mMain->checkTicker(&mWebSerialTicker, mWebSerialInterval)) { - if(mSerialBufFill > 0) { - mEvts->send(mSerialBuf, "serial", millis()); - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); - mSerialBufFill = 0; - } - } -} - - -//----------------------------------------------------------------------------- -void web::onConnect(AsyncEventSourceClient *client) { - DPRINTLN(DBG_VERBOSE, "onConnect"); - - if(client->lastId()) - DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId())); - - client->send("hello!", NULL, millis(), 1000); -} - - -//----------------------------------------------------------------------------- -void web::onIndex(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onIndex")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::onCss(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - - -//----------------------------------------------------------------------------- -void web::onApiJs(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onApiJs")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::onFavicon(AsyncWebServerRequest *request) { - static const char favicon_type[] PROGMEM = "image/x-icon"; - AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico_gz, favicon_ico_gz_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::showNotFound(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + request->url()); - String msg = F("File Not Found\n\nURL: "); - msg += request->url(); - msg += F("\nMethod: "); - msg += ( request->method() == HTTP_GET ) ? "GET" : "POST"; - msg += F("\nArguments: "); - msg += request->args(); - msg += "\n"; - - for(uint8_t i = 0; i < request->args(); i++ ) { - msg += " " + request->argName(i) + ": " + request->arg(i) + "\n"; - } - - request->send(404, F("text/plain"), msg); -} - - -//----------------------------------------------------------------------------- -void web::onReboot(AsyncWebServerRequest *request) { - mMain->mShouldReboot = true; - request->send(200, F("text/html"), F("Rebootreboot. Autoreload after 10 seconds")); -} - - -//----------------------------------------------------------------------------- -void web::onSystem(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSystem")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::showErase(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("showErase")); - mMain->eraseSettings(); - onReboot(request); -} - - -//----------------------------------------------------------------------------- -void web::showFactoryRst(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); - String content = ""; - int refresh = 3; - if(request->args() > 0) { - if(request->arg("reset").toInt() == 1) { - mMain->eraseSettings(true); - content = F("factory reset: success\n\nrebooting ... "); - refresh = 10; - } - else { - content = F("factory reset: aborted"); - refresh = 3; - } - } - else { - content = F("

Factory Reset

" - "

RESET

CANCEL

"); - refresh = 120; - } - request->send(200, F("text/html"), F("Factory Reset") + content + F("")); - if(refresh == 10) { - delay(1000); - ESP.restart(); - } -} - - -//----------------------------------------------------------------------------- -void web::onSetup(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSetup")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::showSave(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("showSave")); - - if(request->args() > 0) { - char buf[20] = {0}; - - // general - if(request->arg("ssid") != "") - request->arg("ssid").toCharArray(mSysCfg->stationSsid, SSID_LEN); - if(request->arg("pwd") != "{PWD}") - request->arg("pwd").toCharArray(mSysCfg->stationPwd, PWD_LEN); - if(request->arg("device") != "") - request->arg("device").toCharArray(mSysCfg->deviceName, DEVNAME_LEN); - - // inverter - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mMain->mSys->getInverterByPos(i, false); - // address - request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); - if(strlen(buf) == 0) - memset(buf, 0, 20); - iv->serial.u64 = mMain->Serial2u64(buf); - switch(iv->serial.b[4]) { - case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break; - case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break; - case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break; - default: break; - } - - // name - request->arg("inv" + String(i) + "Name").toCharArray(iv->name, MAX_NAME_LENGTH); - - // max channel power / name - for(uint8_t j = 0; j < 4; j++) { - iv->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; - request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->chName[j], MAX_NAME_LENGTH); - } - iv->initialized = true; - } - if(request->arg("invInterval") != "") - mConfig->sendInterval = request->arg("invInterval").toInt(); - if(request->arg("invRetry") != "") - mConfig->maxRetransPerPyld = request->arg("invRetry").toInt(); - - // Disclaimer - if(request->arg("disclaimer") != "") - mConfig->disclaimer = strcmp("true", request->arg("disclaimer").c_str()) == 0 ? true : false; - DPRINTLN(DBG_INFO, request->arg("disclaimer").c_str()); - - // pinout - uint8_t pin; - for(uint8_t i = 0; i < 3; i ++) { - pin = request->arg(String(pinArgNames[i])).toInt(); - switch(i) { - default: mConfig->pinCs = pin; break; - case 1: mConfig->pinCe = pin; break; - case 2: mConfig->pinIrq = pin; break; - } - } - - // nrf24 amplifier power - mConfig->amplifierPower = request->arg("rf24Power").toInt() & 0x03; - - // ntp - if(request->arg("ntpAddr") != "") { - request->arg("ntpAddr").toCharArray(mConfig->ntpAddr, NTP_ADDR_LEN); - mConfig->ntpPort = request->arg("ntpPort").toInt() & 0xffff; - } - - // sun - if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { - mConfig->sunLat = 0.0; - mConfig->sunLon = 0.0; - mConfig->sunDisNightCom = false; - } else { - mConfig->sunLat = request->arg("sunLat").toFloat(); - mConfig->sunLon = request->arg("sunLon").toFloat(); - mConfig->sunDisNightCom = (request->arg("sunDisNightCom") == "on"); - } - - - // mqtt - if(request->arg("mqttAddr") != "") { - String addr = request->arg("mqttAddr"); - addr.trim(); - addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); - request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); - if(request->arg("mqttPwd") != "{PWD}") - request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); - request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); - mConfig->mqtt.port = request->arg("mqttPort").toInt(); - } - - // serial console - if(request->arg("serIntvl") != "") { - mConfig->serialInterval = request->arg("serIntvl").toInt() & 0xffff; - - mConfig->serialDebug = (request->arg("serDbg") == "on"); - mConfig->serialShowIv = (request->arg("serEn") == "on"); - // Needed to log TX buffers to serial console - mMain->mSys->Radio.mSerialDebug = mConfig->serialDebug; - } - - mMain->saveValues(); - - if(request->arg("reboot") == "on") - onReboot(request); - else - request->send(200, F("text/html"), F("Setup saved" - "

saved

")); - } -} - - -//----------------------------------------------------------------------------- -void web::onLive(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onLive")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::showWebApi(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); - DPRINTLN(DBG_DEBUG, request->arg("plain")); - const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. - DynamicJsonDocument response(capacity); - - // Parse JSON object - deserializeJson(response, request->arg("plain")); - // ToDo: error handling for payload - uint8_t iv_id = response["inverter"]; - uint8_t cmd = response["cmd"]; - Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id); - if (NULL != iv) { - if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { - // if the AlarmData is requested set the Alarm Index to the requested one - if (cmd == AlarmData || cmd == AlarmUpdate) { - // set the AlarmMesIndex for the request from user input - iv->alarmMesIndex = response["payload"]; - } - DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); - // process payload from web request corresponding to the cmd - iv->enqueCommand(cmd); - } - - - if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { - if (response["cmd"] == (uint8_t)ActivePowerContr) { - uint16_t webapiPayload = response["payload"]; - uint16_t webapiPayload2 = response["payload2"]; - if (webapiPayload > 0 && webapiPayload < 10000) { - iv->devControlCmd = ActivePowerContr; - iv->powerLimit[0] = webapiPayload; - if (webapiPayload2 > 0) - iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check - else // if not set, set it to 0x0000 default - iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut - if (iv->powerLimit[1] & 0x0001) - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); - else - DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); - iv->devControlRequest = true; // queue it in the request loop - } - } - if (response["cmd"] == (uint8_t)TurnOff) { - iv->devControlCmd = TurnOff; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)TurnOn) { - iv->devControlCmd = TurnOn; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) { - iv->devControlCmd = CleanState_LockAndAlarm; - iv->devControlRequest = true; // queue it in the request loop - } - if (response["cmd"] == (uint8_t)Restart) { - iv->devControlCmd = Restart; - iv->devControlRequest = true; // queue it in the request loop - } - } - } - request->send(200, "text/json", "{success:true}"); -} - - -//----------------------------------------------------------------------------- -void web::onUpdate(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onUpdate")); - - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::showUpdate(AsyncWebServerRequest *request) { - bool reboot = !Update.hasError(); - - String html = F("UpdateUpdate: "); - if(reboot) - html += "success"; - else - html += "failed"; - html += F("

rebooting ... auto reload after 20s"); - - AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); - response->addHeader("Connection", "close"); - request->send(response); - mMain->mShouldReboot = reboot; -} - - -//----------------------------------------------------------------------------- -void web::showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - if(!index) { - Serial.printf("Update Start: %s\n", filename.c_str()); -#ifndef ESP32 - Update.runAsync(true); -#endif - if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - Update.printError(Serial); - } - } - if(!Update.hasError()) { - if(Update.write(data, len) != len){ - Update.printError(Serial); - } - } - if(final) { - if(Update.end(true)) { - Serial.printf("Update Success: %uB\n", index+len); - } else { - Update.printError(Serial); - } - } -} - - -//----------------------------------------------------------------------------- -void web::onSerial(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, F("onSerial")); - - AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len); - response->addHeader(F("Content-Encoding"), "gzip"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void web::serialCb(String msg) { - msg.replace("\r\n", ""); - if(mSerialAddTime) { - if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) { - strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr(mApi->getTimezoneOffset()).c_str(), 9); - mSerialBufFill += 9; - } - else { - mSerialBufFill = 0; - mEvts->send("webSerial, buffer overflow!", "serial", millis()); - } - mSerialAddTime = false; - } - - if(msg.endsWith("")) - mSerialAddTime = true; - - uint16_t length = msg.length(); - if((length + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) { - strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); - mSerialBufFill += length; - } - else { - mSerialBufFill = 0; - mEvts->send("webSerial, buffer overflow!", "serial", millis()); - } - -} diff --git a/tools/esp8266/web.h b/tools/esp8266/web.h deleted file mode 100644 index 934abbbd..00000000 --- a/tools/esp8266/web.h +++ /dev/null @@ -1,76 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __WEB_H__ -#define __WEB_H__ - -#include "dbg.h" -#ifdef ESP32 - #include "AsyncTCP.h" - #include "Update.h" -#else - #include "ESPAsyncTCP.h" -#endif -#include "ESPAsyncWebServer.h" -#include "app.h" -#include "webApi.h" - -#define WEB_SERIAL_BUF_SIZE 2048 - -class app; -class webApi; - -class web { - public: - web(app *main, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]); - ~web() {} - - void setup(void); - void loop(void); - - void onConnect(AsyncEventSourceClient *client); - - void onIndex(AsyncWebServerRequest *request); - void onCss(AsyncWebServerRequest *request); - void onApiJs(AsyncWebServerRequest *request); - void onFavicon(AsyncWebServerRequest *request); - void showNotFound(AsyncWebServerRequest *request); - void onReboot(AsyncWebServerRequest *request); - void showErase(AsyncWebServerRequest *request); - void showFactoryRst(AsyncWebServerRequest *request); - void onSetup(AsyncWebServerRequest *request); - void showSave(AsyncWebServerRequest *request); - - void onLive(AsyncWebServerRequest *request); - void showWebApi(AsyncWebServerRequest *request); - - void onUpdate(AsyncWebServerRequest *request); - void showUpdate(AsyncWebServerRequest *request); - void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); - - void serialCb(String msg); - - private: - void onSerial(AsyncWebServerRequest *request); - void onSystem(AsyncWebServerRequest *request); - - AsyncWebServer *mWeb; - AsyncEventSource *mEvts; - - config_t *mConfig; - sysConfig_t *mSysCfg; - statistics_t *mStat; - char *mVersion; - app *mMain; - webApi *mApi; - - bool mSerialAddTime; - char mSerialBuf[WEB_SERIAL_BUF_SIZE]; - uint16_t mSerialBufFill; - uint32_t mWebSerialTicker; - uint32_t mWebSerialInterval; -}; - -#endif /*__WEB_H__*/ diff --git a/tools/esp8266/webApi.cpp b/tools/esp8266/webApi.cpp deleted file mode 100644 index 1c52e81f..00000000 --- a/tools/esp8266/webApi.cpp +++ /dev/null @@ -1,488 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif - -#include "webApi.h" - -//----------------------------------------------------------------------------- -webApi::webApi(AsyncWebServer *srv, app *app, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]) { - mSrv = srv; - mApp = app; - mSysCfg = sysCfg; - mConfig = config; - mStat = stat; - mVersion = version; - - mTimezoneOffset = 0; -} - - -//----------------------------------------------------------------------------- -void webApi::setup(void) { - mSrv->on("/api", HTTP_GET, std::bind(&webApi::onApi, this, std::placeholders::_1)); - mSrv->on("/api", HTTP_POST, std::bind(&webApi::onApiPost, this, std::placeholders::_1)).onBody( - std::bind(&webApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - - mSrv->on("/get_setup", HTTP_GET, std::bind(&webApi::onDwnldSetup, this, std::placeholders::_1)); -} - - -//----------------------------------------------------------------------------- -void webApi::loop(void) { -} - - -//----------------------------------------------------------------------------- -void webApi::onApi(AsyncWebServerRequest *request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); - JsonObject root = response->getRoot(); - - Inverter<> *iv = mApp->mSys->getInverterByPos(0, false); - String path = request->url().substring(5); - if(path == "system") getSystem(root); - else if(path == "statistics") getStatistics(root); - else if(path == "inverter/list") getInverterList(root); - else if(path == "menu") getMenu(root); - else if(path == "index") getIndex(root); - else if(path == "setup") getSetup(root); - else if(path == "setup/networks") getNetworks(root); - else if(path == "live") getLive(root); - else if(path == "record/info") getRecord(root, iv->getRecordStruct(InverterDevInform_All)); - else if(path == "record/alarm") getRecord(root, iv->getRecordStruct(AlarmData)); - else if(path == "record/config") getRecord(root, iv->getRecordStruct(SystemConfigPara)); - else if(path == "record/live") getRecord(root, iv->getRecordStruct(RealTimeRunData_Debug)); - else - getNotFound(root, F("http://") + request->host() + F("/api/")); - - response->addHeader("Access-Control-Allow-Origin", "*"); - response->addHeader("Access-Control-Allow-Headers", "content-type"); - response->setLength(); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void webApi::onApiPost(AsyncWebServerRequest *request) { - DPRINTLN(DBG_VERBOSE, "onApiPost"); -} - - -//----------------------------------------------------------------------------- -void webApi::onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - DPRINTLN(DBG_VERBOSE, "onApiPostBody"); - DynamicJsonDocument json(200); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); - JsonObject root = response->getRoot(); - - DeserializationError err = deserializeJson(json, (const char *)data); - root[F("success")] = (err) ? false : true; - if(!err) { - String path = request->url().substring(5); - if(path == "ctrl") - root[F("success")] = setCtrl(json, root); - else if(path == "setup") - root[F("success")] = setSetup(json, root); - else { - root[F("success")] = false; - root[F("error")] = "Path not found: " + path; - } - } - else { - switch (err.code()) { - case DeserializationError::Ok: break; - case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; - case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; - default: root[F("error")] = F("Deserialization failed"); break; - } - } - - response->setLength(); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void webApi::getNotFound(JsonObject obj, String url) { - JsonObject ep = obj.createNestedObject("avail_endpoints"); - ep[F("system")] = url + F("system"); - ep[F("statistics")] = url + F("statistics"); - ep[F("inverter/list")] = url + F("inverter/list"); - ep[F("index")] = url + F("index"); - ep[F("setup")] = url + F("setup"); - ep[F("live")] = url + F("live"); - ep[F("record/info")] = url + F("record/info"); - ep[F("record/alarm")] = url + F("record/alarm"); - ep[F("record/config")] = url + F("record/config"); - ep[F("record/live")] = url + F("record/live"); -} - - -//----------------------------------------------------------------------------- -void webApi::onDwnldSetup(AsyncWebServerRequest *request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); - JsonObject root = response->getRoot(); - - getSetup(root); - - response->setLength(); - response->addHeader("Content-Type", "application/octet-stream"); - response->addHeader("Content-Description", "File Transfer"); - response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); - request->send(response); -} - - -//----------------------------------------------------------------------------- -void webApi::getSystem(JsonObject obj) { - obj[F("ssid")] = mSysCfg->stationSsid; - obj[F("device_name")] = mSysCfg->deviceName; - obj[F("version")] = String(mVersion); - obj[F("build")] = String(AUTO_GIT_HASH); - obj[F("ts_uptime")] = mApp->getUptime(); - obj[F("ts_now")] = mApp->getTimestamp(); - obj[F("ts_sunrise")] = mApp->getSunrise(); - obj[F("ts_sunset")] = mApp->getSunset(); - obj[F("ts_sun_upd")] = mApp->getLatestSunTimestamp(); - obj[F("wifi_rssi")] = WiFi.RSSI(); - obj[F("disclaimer")] = mConfig->disclaimer; -#if defined(ESP32) - obj[F("esp_type")] = F("ESP32"); -#else - obj[F("esp_type")] = F("ESP8266"); -#endif -} - - -//----------------------------------------------------------------------------- -void webApi::getStatistics(JsonObject obj) { - obj[F("rx_success")] = mStat->rxSuccess; - obj[F("rx_fail")] = mStat->rxFail; - obj[F("rx_fail_answer")] = mStat->rxFailNoAnser; - obj[F("frame_cnt")] = mStat->frmCnt; - obj[F("tx_cnt")] = mApp->mSys->Radio.mSendCnt; -} - - -//----------------------------------------------------------------------------- -void webApi::getInverterList(JsonObject obj) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mApp->mSys->getInverterByPos(i); - if(NULL != iv) { - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("id")] = i; - obj2[F("name")] = String(iv->name); - obj2[F("serial")] = String(iv->serial.u64, HEX); - obj2[F("channels")] = iv->channels; - obj2[F("version")] = String(iv->fwVersion); - - for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_max_power")][j] = iv->chMaxPwr[j]; - obj2[F("ch_name")][j] = iv->chName[j]; - } - } - } - obj[F("interval")] = String(mConfig->sendInterval); - obj[F("retries")] = String(mConfig->maxRetransPerPyld); - obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; -} - - -//----------------------------------------------------------------------------- -void webApi::getMqtt(JsonObject obj) { - obj[F("broker")] = String(mConfig->mqtt.broker); - obj[F("port")] = String(mConfig->mqtt.port); - obj[F("user")] = String(mConfig->mqtt.user); - obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); - obj[F("topic")] = String(mConfig->mqtt.topic); -} - - -//----------------------------------------------------------------------------- -void webApi::getNtp(JsonObject obj) { - obj[F("addr")] = String(mConfig->ntpAddr); - obj[F("port")] = String(mConfig->ntpPort); -} - -//----------------------------------------------------------------------------- -void webApi::getSun(JsonObject obj) { - obj[F("lat")] = mConfig->sunLat ? String(mConfig->sunLat, 5) : ""; - obj[F("lon")] = mConfig->sunLat ? String(mConfig->sunLon, 5) : ""; - obj[F("disnightcom")] = mConfig->sunDisNightCom; -} - - -//----------------------------------------------------------------------------- -void webApi::getPinout(JsonObject obj) { - obj[F("cs")] = mConfig->pinCs; - obj[F("ce")] = mConfig->pinCe; - obj[F("irq")] = mConfig->pinIrq; -} - - -//----------------------------------------------------------------------------- -void webApi::getRadio(JsonObject obj) { - obj[F("power_level")] = mConfig->amplifierPower; -} - - -//----------------------------------------------------------------------------- -void webApi::getSerial(JsonObject obj) { - obj[F("interval")] = (uint16_t)mConfig->serialInterval; - obj[F("show_live_data")] = mConfig->serialShowIv; - obj[F("debug")] = mConfig->serialDebug; -} - - -//----------------------------------------------------------------------------- -void webApi::getMenu(JsonObject obj) { - obj["name"][0] = "Live"; - obj["link"][0] = "/live"; - obj["name"][1] = "Serial Console"; - obj["link"][1] = "/serial"; - obj["name"][2] = "Settings"; - obj["link"][2] = "/setup"; - obj["name"][3] = "-"; - obj["name"][4] = "REST API"; - obj["link"][4] = "/api"; - obj["trgt"][4] = "_blank"; - obj["name"][5] = "-"; - obj["name"][6] = "Update"; - obj["link"][6] = "/update"; - obj["name"][7] = "System"; - obj["link"][7] = "/system"; -} - - -//----------------------------------------------------------------------------- -void webApi::getIndex(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); - getSystem(obj.createNestedObject(F("system"))); - getStatistics(obj.createNestedObject(F("statistics"))); - obj["refresh_interval"] = SEND_INTERVAL; - - JsonArray inv = obj.createNestedArray(F("inverter")); - Inverter<> *iv; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mApp->mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject invObj = inv.createNestedObject(); - invObj[F("id")] = i; - invObj[F("name")] = String(iv->name); - invObj[F("version")] = String(iv->fwVersion); - invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp(), rec); - invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp(), rec); - invObj[F("ts_last_success")] = iv->getLastTs(rec); - } - } - - JsonArray warn = obj.createNestedArray(F("warnings")); - if(!mApp->mSys->Radio.isChipConnected()) - warn.add(F("your NRF24 module can't be reached, check the wiring and pinout")); - if(!mApp->mqttIsConnected()) - warn.add(F("MQTT is not connected")); - - JsonArray info = obj.createNestedArray(F("infos")); - if(mApp->getRebootRequestState()) - info.add(F("reboot your ESP to apply all your configuration changes!")); - if(!mApp->getSettingsValid()) - info.add(F("your settings are invalid")); - if(mApp->mqttIsConnected()) - info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent")); -} - - -//----------------------------------------------------------------------------- -void webApi::getSetup(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); - getSystem(obj.createNestedObject(F("system"))); - getInverterList(obj.createNestedObject(F("inverter"))); - getMqtt(obj.createNestedObject(F("mqtt"))); - getNtp(obj.createNestedObject(F("ntp"))); - getSun(obj.createNestedObject(F("sun"))); - getPinout(obj.createNestedObject(F("pinout"))); - getRadio(obj.createNestedObject(F("radio"))); - getSerial(obj.createNestedObject(F("serial"))); -} - - -//----------------------------------------------------------------------------- -void webApi::getNetworks(JsonObject obj) { - mApp->getAvailNetworks(obj); -} - - -//----------------------------------------------------------------------------- -void webApi::getLive(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); - getSystem(obj.createNestedObject(F("system"))); - JsonArray invArr = obj.createNestedArray(F("inverter")); - obj["refresh_interval"] = SEND_INTERVAL; - - uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; - - Inverter<> *iv; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mApp->mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("name")] = String(iv->name); - obj2[F("channels")] = iv->channels; - obj2[F("power_limit_read")] = round3(iv->actPowerLimit); - obj2[F("last_alarm")] = String(iv->lastAlarmMsg); - obj2[F("ts_last_success")] = rec->ts; - - JsonArray ch = obj2.createNestedArray("ch"); - JsonArray ch0 = ch.createNestedArray(); - obj2[F("ch_names")][0] = "AC"; - for (uint8_t fld = 0; fld < sizeof(list); fld++) { - pos = (iv->getPosByChFld(CH0, list[fld], rec)); - ch0[fld] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; - obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - - for(uint8_t j = 1; j <= iv->channels; j ++) { - obj2[F("ch_names")][j] = String(iv->chName[j-1]); - JsonArray cur = ch.createNestedArray(); - for (uint8_t k = 0; k < 6; k++) { - switch(k) { - default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; - case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; - case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; - case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break; - case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; - case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; - } - cur[k] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; - if(1 == j) { - obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - } - } - } - } -} - - -//----------------------------------------------------------------------------- -void webApi::getRecord(JsonObject obj, record_t<> *rec) { - JsonArray invArr = obj.createNestedArray(F("inverter")); - - Inverter<> *iv; - uint8_t pos; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - iv = mApp->mSys->getInverterByPos(i); - if(NULL != iv) { - JsonArray obj2 = invArr.createNestedArray(); - for(uint8_t j = 0; j < rec->length; j++) { - byteAssign_t *assign = iv->getByteAssign(j, rec); - pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec)); - obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail; - } - } - } -} - - -//----------------------------------------------------------------------------- -bool webApi::setCtrl(DynamicJsonDocument jsonIn, JsonObject jsonOut) { - uint8_t cmd = jsonIn[F("cmd")]; - - // Todo: num is the inverter number 0-3. For better display in DPRINTLN - uint8_t num = jsonIn[F("inverter")]; - uint8_t tx_request = jsonIn[F("tx_request")]; - - if(TX_REQ_DEVCONTROL == tx_request) - { - DPRINTLN(DBG_INFO, F("devcontrol [") + String(num) + F("], cmd: 0x") + String(cmd, HEX)); - - Inverter<> *iv = getInverter(jsonIn, jsonOut); - JsonArray payload = jsonIn[F("payload")].as(); - - if(NULL != iv) - { - switch (cmd) - { - case TurnOn: - iv->devControlCmd = TurnOn; - iv->devControlRequest = true; - break; - case TurnOff: - iv->devControlCmd = TurnOff; - iv->devControlRequest = true; - break; - case CleanState_LockAndAlarm: - iv->devControlCmd = CleanState_LockAndAlarm; - iv->devControlRequest = true; - break; - case Restart: - iv->devControlCmd = Restart; - iv->devControlRequest = true; - break; - case ActivePowerContr: - iv->devControlCmd = ActivePowerContr; - iv->devControlRequest = true; - iv->powerLimit[0] = payload[0]; - iv->powerLimit[1] = payload[1]; - break; - default: - jsonOut["error"] = "unknown 'cmd' = " + String(cmd); - return false; - } - } else { - return false; - } - } - else { - jsonOut[F("error")] = F("unknown 'tx_request'"); - return false; - } - - return true; -} - - -//----------------------------------------------------------------------------- -bool webApi::setSetup(DynamicJsonDocument jsonIn, JsonObject jsonOut) { - if(F("scan_wifi")) - mApp->scanAvailNetworks(); - else if(F("set_time") == jsonIn[F("cmd")]) - mApp->setTimestamp(jsonIn[F("ts")]); - else if(F("sync_ntp") == jsonIn[F("cmd")]) - mApp->setTimestamp(0); // 0: update ntp flag - else if(F("serial_utc_offset") == jsonIn[F("cmd")]) - mTimezoneOffset = jsonIn[F("ts")]; - else if(F("discovery_cfg") == jsonIn[F("cmd")]) - mApp->mFlagSendDiscoveryConfig = true; // for homeassistant - else { - jsonOut[F("error")] = F("unknown cmd"); - return false; - } - - return true; -} - - -//----------------------------------------------------------------------------- -Inverter<> *webApi::getInverter(DynamicJsonDocument jsonIn, JsonObject jsonOut) { - uint8_t id = jsonIn[F("inverter")]; - Inverter<> *iv = mApp->mSys->getInverterByPos(id); - if(NULL == iv) - jsonOut[F("error")] = F("inverter index to high: ") + String(id); - return iv; -} diff --git a/tools/esp8266/webApi.h b/tools/esp8266/webApi.h deleted file mode 100644 index a45dcf8e..00000000 --- a/tools/esp8266/webApi.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef __WEB_API_H__ -#define __WEB_API_H__ - -#include "dbg.h" -#ifdef ESP32 - #include "AsyncTCP.h" -#else - #include "ESPAsyncTCP.h" -#endif -#include "ESPAsyncWebServer.h" -#include "AsyncJson.h" -#include "app.h" - - -class app; - -class webApi { - public: - webApi(AsyncWebServer *srv, app *app, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]); - - void setup(void); - void loop(void); - - uint32_t getTimezoneOffset() { - return mTimezoneOffset; - } - - private: - void onApi(AsyncWebServerRequest *request); - void onApiPost(AsyncWebServerRequest *request); - void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); - void getNotFound(JsonObject obj, String url); - void onDwnldSetup(AsyncWebServerRequest *request); - - void getSystem(JsonObject obj); - void getStatistics(JsonObject obj); - void getInverterList(JsonObject obj); - void getMqtt(JsonObject obj); - void getNtp(JsonObject obj); - void getSun(JsonObject obj); - void getPinout(JsonObject obj); - void getRadio(JsonObject obj); - void getSerial(JsonObject obj); - - void getMenu(JsonObject obj); - void getIndex(JsonObject obj); - void getSetup(JsonObject obj); - void getNetworks(JsonObject obj); - void getLive(JsonObject obj); - void getRecord(JsonObject obj, record_t<> *rec); - - bool setCtrl(DynamicJsonDocument jsonIn, JsonObject jsonOut); - bool setSetup(DynamicJsonDocument jsonIn, JsonObject jsonOut); - - Inverter<> *getInverter(DynamicJsonDocument jsonIn, JsonObject jsonOut); - - double round3(double value) { - return (int)(value * 1000 + 0.5) / 1000.0; - } - - AsyncWebServer *mSrv; - app *mApp; - - config_t *mConfig; - sysConfig_t *mSysCfg; - statistics_t *mStat; - char *mVersion; - - uint32_t mTimezoneOffset; -}; - -#endif /*__WEB_API_H__*/ diff --git a/tools/pcb-nokia5110/1_Front.jpg b/tools/pcb-nokia5110/1_Front.jpg new file mode 100644 index 00000000..a7f8ece1 Binary files /dev/null and b/tools/pcb-nokia5110/1_Front.jpg differ diff --git a/tools/pcb-nokia5110/2_Bottom.jpg b/tools/pcb-nokia5110/2_Bottom.jpg new file mode 100644 index 00000000..fa49c968 Binary files /dev/null and b/tools/pcb-nokia5110/2_Bottom.jpg differ diff --git a/tools/pcb-nokia5110/3_booting.jpg b/tools/pcb-nokia5110/3_booting.jpg new file mode 100644 index 00000000..c7f96fd4 Binary files /dev/null and b/tools/pcb-nokia5110/3_booting.jpg differ diff --git a/tools/pcb-nokia5110/4_runIP.jpg b/tools/pcb-nokia5110/4_runIP.jpg new file mode 100644 index 00000000..0cc4a27d Binary files /dev/null and b/tools/pcb-nokia5110/4_runIP.jpg differ diff --git a/tools/pcb-nokia5110/5_runTime.jpg b/tools/pcb-nokia5110/5_runTime.jpg new file mode 100644 index 00000000..8f02b137 Binary files /dev/null and b/tools/pcb-nokia5110/5_runTime.jpg differ diff --git a/tools/pcb-nokia5110/6_Wiring_SSD1306.png b/tools/pcb-nokia5110/6_Wiring_SSD1306.png new file mode 100644 index 00000000..f58dde20 Binary files /dev/null and b/tools/pcb-nokia5110/6_Wiring_SSD1306.png differ diff --git a/tools/pcb-nokia5110/JLCPCB_Gerber_PCB_ahoy-dtu_2022-10-31.zip b/tools/pcb-nokia5110/JLCPCB_Gerber_PCB_ahoy-dtu_2022-10-31.zip new file mode 100644 index 00000000..f19f68a6 Binary files /dev/null and b/tools/pcb-nokia5110/JLCPCB_Gerber_PCB_ahoy-dtu_2022-10-31.zip differ diff --git a/tools/pcb-nokia5110/Nokia5110-LCD.jpg b/tools/pcb-nokia5110/Nokia5110-LCD.jpg new file mode 100644 index 00000000..05963ee0 Binary files /dev/null and b/tools/pcb-nokia5110/Nokia5110-LCD.jpg differ diff --git a/tools/pcb-nokia5110/Nokia5110-LCD2.jpg b/tools/pcb-nokia5110/Nokia5110-LCD2.jpg new file mode 100644 index 00000000..779c97d7 Binary files /dev/null and b/tools/pcb-nokia5110/Nokia5110-LCD2.jpg differ diff --git a/tools/pcb-nokia5110/PCB-V1.jpg b/tools/pcb-nokia5110/PCB-V1.jpg new file mode 100644 index 00000000..fbf23450 Binary files /dev/null and b/tools/pcb-nokia5110/PCB-V1.jpg differ diff --git a/tools/pcb-nokia5110/SSD1306.png b/tools/pcb-nokia5110/SSD1306.png new file mode 100644 index 00000000..4ec6b423 Binary files /dev/null and b/tools/pcb-nokia5110/SSD1306.png differ diff --git a/tools/pcb-nokia5110/readme.md b/tools/pcb-nokia5110/readme.md new file mode 100644 index 00000000..d8302954 --- /dev/null +++ b/tools/pcb-nokia5110/readme.md @@ -0,0 +1,52 @@ +# PCB for Wemos D1 + Nokia5110-display + nRF24L01+ + +Simple pcb to plug a nRF24L01+ together with a Wemos D1 Mini / Pro and a NOKIA5110-display (known as PCD8544). +One goal : You can power this pcb via USB-interface of D1-mini. + +attached zip was created with easyeda. You can order easyly on jlcpcb.com. + +## Attention +Be sure you have the right type of nokia-display-pcb ! +Check pin-header-description (There are different versions avail)! +( check picture : LIGHT (or BL) is placed between GND and VCC ) + +![img](Nokia5110-LCD.jpg) or ![img](Nokia5110-LCD2.jpg) + +--- + +A picture of earlier version of pcb. + +![img](PCB-V1.jpg) + +D1-mini and NRF placed on front side, nokia display will soldered bottom. +You can use this PCB also without mounting a Nokia-display. + +Actual version of PCB has mounting holes and additional 5V/Gnd -pin access. + +## after soldering ... + +![img](1_Front.jpg) +NRF & Wemos on Front + +![img](2_Bottom.jpg) +Nokia Display soldered on bottom side + +![img](3_booting.jpg) +while booting (wait for wifi, 1st data, ...) + +![img](4_runIP.jpg) +1st screen show most interesting info and IP in bottom line + +![img](5_runTime.jpg) +this is the normal screen with time in bottom line (refresh all 5 seconds) + +# SSD1306 + +Some changes made possible to use SSD1306 (instead of NOKIA5110 display). +![img](SSD1306.png) + +You can use same PCB. You only have to connect GND (SSD:GND), VCC (SSD:VCC), D/C (SSD:SDA) and SCE (SSD:SCL) +![img](6_Wiring_SSD1306.png) + +On other PCB-layouts, PIN wemos:D1 need connected to SSD:SCL and wemos:D2 has to be connected to SSD:SDA. +much fun. diff --git a/tools/rpi/Dockerfile b/tools/rpi/Dockerfile new file mode 100644 index 00000000..1a62b01b --- /dev/null +++ b/tools/rpi/Dockerfile @@ -0,0 +1,15 @@ +############################ +# build executable binary +############################ + +FROM python:slim-bullseye + +COPY . /hoymiles +WORKDIR /hoymiles + +RUN python3 -m pip install pyrf24 influxdb_client && \ +python3 -m pip list #watch for RF24 module - if its there its installed + +RUN pip install crcmod pyyaml paho-mqtt SunTimes + +CMD python3 -um hoymiles --log-transactions --verbose --config /etc/ahoy.yml diff --git a/tools/rpi/README.md b/tools/rpi/README.md index df91f1dd..b683a294 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -21,14 +21,22 @@ Required Hardware Setup `ahoy.py` has been successfully tested with the following setup -- RaspberryPi Model 2B (any model should work) +- RaspberryPi Model 2B, 4B (any model should work) - NRF24L01+ Radio Module connected as described, e.g., in [2] (Instructions at [3] should work identically, but [2] has more pretty pictures.) +- or the [PaHoy board](https://github.com/DM6JM/PaHoy/) - TMRh20's 'Optimized High Speed nRF24L01+ Driver' [3], installed as per the instructions given in [4] - Python Library Wrapper, as per [5] +- or the easy way, using [pyRF24](https://github.com/nRF24/pyRF24)[6] +How to talk to the nRF24L01+ in Python? +--------------------------------------- +Either you make use of the way proposed in the following, using the NRF24 Python Wrapper and the 'Optimized High Speed nRF24L01+ Driver' OR you just use pip and let it install pyRF24. + +- If you go with pyRF24, all that needs to be done is installing pyRF24 as described in [6]. Please be aware that not all examples provided in this repo are prepared to use pyRF24. It might be nescessary to adjust the imports from RF24 to pyRF24 to get them running. Once you installed pyRF24, go on at 'Required python modules' +- If you go with the RF24 wrapper, do the following steps Building the NRF24 Python Wrapper --------------------------------- @@ -80,13 +88,80 @@ python3 getting_started.py # to test and see whether RF24 class can be loaded as If there are no error messages on the last step, then the NRF24 Wrapper has been installed successfully. + +Building RF24 Wrapper for Debian 11 (bullseye) 64 bit operating system +---------------------------------------------------------------------- +The description above does not work on Debian 11 (bullseye) 64 bit operating system. +Please check first, if you have Debian 11 (bullseye) 64 bit operating system installed: + - `uname -a` search for aarch64 + - `lsb_release -d` + - `cat /etc/debian_version` + +There are 2 possible solutions to install the RF24 wrapper: + +**__1. Solution:__** +```code +sudo apt install cmake git python3-dev libboost-python-dev python3-pip python3-rpi.gpio + +sudo ln -s $(ls /usr/lib/$(ls /usr/lib/gcc | \ + head -1)/libboost_python3*.so | \ + tail -1) /usr/lib/$(ls /usr/lib/gcc | \ + head -1)/libboost_python3.so + +git clone https://github.com/nRF24/RF24.git +cd RF24 + +rm -rf build Makefile.inc +./configure --driver=SPIDEV +``` +> _edit `Makefile.inc` with your prefered editor e.g. nano or vi_ +> +> old: +>```code +> CPUFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard +> CFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard -Ofast -Wall -pthread +>``` +> new: +>```code +> CPUFLAGS= +> CFLAGS=-Ofast -Wall -pthread +>``` +_continue now_ +```code +make +sudo make install + +cd pyRF24 +rm -r ./build/ ./dist/ ./RF24.egg-info/ ./__pycache__/ #just to make sure there is no old stuff +python3 -m pip install --upgrade pip +python3 -m pip install . +python3 -m pip list #watch for RF24 module - if its there its installed +``` + + +**__2. Solution:__** +```code +sudo apt install git python3-dev libboost-python-dev python3-pip python3-rpi.gpio + +git clone --recurse-submodules https://github.com/nRF24/pyRF24.git +cd pyRF24 +python3 -m pip install . -v # this step takes about 5 minutes on my RPI-4 ! +``` + +If you have problems with your radio module from ahoi, e.g.: cannot interpret received data, +please try to reduce the speed of your radio module! +Add the following parameter to your ahoy.yml configuration file in "nrf" section: +`spispeed: 600000` (0.6 MHz) + + + Required python modules ----------------------- Some modules are not installed by default on a RaspberryPi, therefore add them manually: -``` -pip install crcmod pyyaml paho-mqtt +```code +pip install crcmod pyyaml paho-mqtt SunTimes ``` Configuration @@ -112,7 +187,7 @@ Python parameters The application describes itself -``` +```code python3 -m hoymiles --help usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose] @@ -153,7 +228,12 @@ Example injects exactly the same as we normally use to poll data This allows for even faster hacking during runtime - +Running it as a service +----------------------- +If you want to run directly from the start, you might want to install it as a service. +Depending on if you want to run it once a user is logged in or as soon as the system is booted, two service examples are included. +ahoy.service allows you to start it as a user service upon login. +ahoy_system.service allows you to start it as a system service already before login without user interaction. Analysing the Logs ------------------ @@ -239,7 +319,7 @@ Todo - Ability to talk to multiple inverters - MQTT gateway - understand channel hopping -- configurable polling interval +- ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml - commands - picture of setup! - python module @@ -255,3 +335,4 @@ References - [3] https://nrf24.github.io/RF24/index.html - [4] https://nrf24.github.io/RF24/md_docs_linux_install.html - [5] https://nrf24.github.io/RF24/md_docs_python_wrapper.html +- [6] https://github.com/nRF24/pyRF24 diff --git a/tools/rpi/ahoy.service b/tools/rpi/ahoy.service new file mode 100644 index 00000000..c7be5bb2 --- /dev/null +++ b/tools/rpi/ahoy.service @@ -0,0 +1,35 @@ +###################################################################### +# systemd.service configuration for ahoy (lumapu) +# users can modify the lines: +# Description +# ExecStart (example: name of config file) +# WorkingDirectory (absolute path to your private ahoy dir) +# To change other config parameter, please consult systemd documentation +# +# To activate this service, enable and start ahoy.service +# $ systemctl --user enable $(pwd)/ahoy/tools/rpi/ahoy.service +# $ systemctl --user status ahoy +# $ systemctl --user start ahoy +# $ systemctl --user status ahoy +# +# 2023.01 +###################################################################### + +[Unit] + +Description=ahoy (lumapu) as Service +After=network.target local-fs.target time-sync.target + +[Service] +ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml +RestartSec=10 +Restart=on-failure +Type=simple + +# WorkingDirectory must be an absolute path - not relative path +WorkingDirectory=/home/pi/ahoy/tools/rpi +EnvironmentFile=/etc/environment + +[Install] +WantedBy=default.target + diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 870b442f..428219a6 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -1,8 +1,18 @@ --- ahoy: - interval: 0 - sunset: true + interval: 5 + + logging: + filename: 'hoymiles.log' + # DEBUG, INFO, WARNING, ERROR, FATAL + level: 'INFO' + + sunset: + disabled: false + latitude: 51.799118 + longitude: 10.615523 + altitude: 1142 # List of available NRF24 transceivers nrf: @@ -18,6 +28,11 @@ ahoy: password: 'password' useTLS: False insecureTLS: False #set True for e.g. self signed certificates. + QoS: 0 + Retain: True + last_will: + topic: my_DTU_name # Name of DTU - default: hoymiles/{DTU-serial} + payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!" # adding prometheus exporter prometheus: @@ -39,9 +54,7 @@ ahoy: - serial: 114172220003 url: 'http://localhost/middleware/' channels: - - type: 'temperature' - uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' - - type: 'frequency' + - type: 'ac_frequency0' uid: '' - type: 'ac_power0' uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' @@ -49,26 +62,61 @@ ahoy: uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' - type: 'ac_current0' uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' + - type: 'ac_reactive_power0' + uid: '' - type: 'dc_power0' uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' - type: 'dc_voltage0' uid: '' - type: 'dc_current0' uid: '' + - type: 'dc_energy_total0' + uid: '' + - type: 'dc_energy_daily0' + uid: 'c2a93ea0-9a4e-11ed-8000-7d82e3ac8959' + - type: 'dc_irradiation0' + uid: 'c2d887a0-9a4e-11ed-a7ac-0dab944fd82d' - type: 'dc_power1' uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' - type: 'dc_voltage1' uid: '' - type: 'dc_current1' uid: '' + - type: 'dc_energy_total1' + uid: '' + - type: 'dc_energy_daily1' + uid: 'c3c04df0-9a4e-11ed-82c6-a15a9aba54a3' + - type: 'dc_irradiation1' + uid: 'c3f3efd0-9a4e-11ed-9a77-3fd3187e6237' + - type: 'temperature' + uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' + - type: 'powerfactor' + uid: '' + - type: 'yield_total' + uid: '' + - type: 'yield_today' + uid: 'c4a76dd0-9a4e-11ed-b79f-2de013d39150' + - type: 'efficiency' + uid: 'c4d8e9c0-9a4e-11ed-9d9e-9737749e4b45' dtu: serial: 99978563001 + name: my_DTU_name inverters: - name: 'balkon' serial: 114172220003 - txpower: 'low' # txpower per inverter (min,low,high,max) + txpower: 'low' # txpower per inverter (min,low,high,max) mqtt: - send_raw_enabled: false # allow inject debug data via mqtt - topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}' + send_raw_enabled: false # allow inject debug data via mqtt + topic: 'hoymiles/114172220003' # defaults to '{inverter-name}/{serial}' + strings: # list all available strings + - s_name: 'String 1 left' # String 1 name + s_maxpower: 395 # String 1 max power in inverter + - s_name: 'String 2 right' # String 2 name + s_maxpower: 400 # String 2 max power in inverter + - s_name: 'String 3 up' # String 3 name + s_maxpower: 405 # String 3 max power in inverter + - s_name: 'String 4 down' # String 4 name + s_maxpower: 410 # String 4 max power in inverter + diff --git a/tools/rpi/ahoy_system.service b/tools/rpi/ahoy_system.service new file mode 100644 index 00000000..df8f4b13 --- /dev/null +++ b/tools/rpi/ahoy_system.service @@ -0,0 +1,42 @@ +###################################################################### +# systemd.service configuration for ahoy (lumapu) +# users can modify the lines: +# Description +# ExecStart (example: name of config file) +# WorkingDirectory (absolute path to your private ahoy dir) +# To change other config parameter, please consult systemd documentation +# +# To activate this service, enable and start ahoy.service: +# - Create folder ahoy in /home/ and set owner to the user that the +# service should be executed for (e.g. pi) +# - Copy folder contents to new folder +# - Adjust the user that this service should be executed as, avoid root +# - Execute commands to setup, check and start/stop as wanted +# $ sudo systemctl enable /home/ahoy/tools/rpi/ahoy.service +# $ sudo systemctl status ahoy +# $ sudo systemctl start ahoy +# $ sudo systemctl stop ahoy +# +# 2023.01 +# 2023.03 +###################################################################### + +[Unit] + +Description=ahoy (lumapu) as Service +After=network.target local-fs.target time-sync.target + +[Service] +ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml +RestartSec=10 +Restart=on-failure +Type=simple +User=pi + +# WorkingDirectory must be an absolute path - not relative path +WorkingDirectory=/home/ahoy/tools/rpi +EnvironmentFile=/etc/environment + +[Install] +WantedBy=default.target + diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 38ca4a20..210bed65 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -9,10 +9,30 @@ import struct import time import re from datetime import datetime -import json +import logging import crcmod -from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 from .decoders import * +from os import environ + +try: + # OSI Layer 2 driver for nRF24L01 on Arduino & Raspberry Pi/Linux Devices + # https://github.com/nRF24/RF24.git + from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 + if environ.get('TERM') is not None: + print('Using python Module: RF24') +except ModuleNotFoundError as e: + if environ.get('TERM') is not None: + print(f'{e} - try to use module: RF24') + try: + # Repo for pyRF24 package + # https://github.com/nRF24/pyRF24.git + from pyrf24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 + if environ.get('TERM') is not None: + print(f'{e} - Using python Module: pyrf24') + except ModuleNotFoundError as e: + if environ.get('TERM') is not None: + print(f'{e} - exit') + exit() f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) @@ -51,16 +71,6 @@ def ser_to_esb_addr(inverter_ser): air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01' return air_order[::-1] -def print_addr(inverter_ser): - """ - Debug print addresses - - :param str inverter_ser: inverter serial - """ - print(f"ser# {inverter_ser} ", end='') - print(f" -> HM {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='') - print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}") - class ResponseDecoderFactory: """ Prepare payload decoder @@ -154,6 +164,9 @@ class ResponseDecoder(ResponseDecoderFactory): def __init__(self, response, **params): """Initialize ResponseDecoder""" ResponseDecoderFactory.__init__(self, response, **params) + self.inv_name=params.get('inverter_name', None) + self.dtu_ser=params.get('dtu_ser', None) + self.strings=params.get('strings', None) def decode(self): """ @@ -165,16 +178,33 @@ class ResponseDecoder(ResponseDecoderFactory): model = self.inverter_model command = self.request_command + if HOYMILES_DEBUG_LOGGING: + if command.upper() == '01': + model_desc = "Firmware version / date" + elif command.upper() == '02': + model_desc = "Inverter generic events log" + elif command.upper() == '0B': + model_desc = "mirco-inverters status data" + elif command.upper() == '0C': + model_desc = "mirco-inverters status data" + elif command.upper() == '11': + model_desc = "Inverter generic events log" + elif command.upper() == '12': + model_desc = "Inverter major events log" + logging.info(f'model_decoder: {model}Decode{command.upper()} - {model_desc}') + model_decoders = __import__('hoymiles.decoders') if hasattr(model_decoders, f'{model}Decode{command.upper()}'): device = getattr(model_decoders, f'{model}Decode{command.upper()}') else: - if HOYMILES_DEBUG_LOGGING: - device = getattr(model_decoders, 'DebugDecodeAny') + device = getattr(model_decoders, 'DebugDecodeAny') return device(self.response, time_rx=self.time_rx, - inverter_ser=self.inverter_ser + inverter_ser=self.inverter_ser, + inverter_name=self.inv_name, + dtu_ser=self.dtu_ser, + strings=self.strings ) class InverterPacketFragment: @@ -584,7 +614,7 @@ class InverterTransaction: if HOYMILES_TRANSACTION_LOGGING: c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') + logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') self.radio.transmit(packet, txpower=self.txpower) @@ -592,14 +622,14 @@ class InverterTransaction: try: for response in self.radio.receive(): if HOYMILES_TRANSACTION_LOGGING: - print(response) + logging.debug(response) self.frame_append(response) wait = True except TimeoutError: pass except BufferError as e: - print(f'Buffer error {e}') + logging.warning(f'Buffer error {e}') pass return wait diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 60ef11fc..e34ccc67 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -10,23 +10,43 @@ import struct from enum import IntEnum import re import time +import traceback from datetime import datetime +from datetime import timedelta +from suntimes import SunTimes import argparse import yaml from yaml.loader import SafeLoader -import paho.mqtt.client import hoymiles +import logging -def main_loop(do_init): - """Main loop""" - inverters = [ - inverter for inverter in ahoy_config.get('inverters', []) - if not inverter.get('disabled', False)] +################################################################################ +""" Signal Handler """ +################################################################################ +# from signal import signal, Signals, SIGINT, SIGTERM, SIGKILL, SIGHUP +from signal import * +def signal_handler(sig_num, frame): + signame = Signals(sig_num).name + logging.info(f'Stop by Signal {signame} ({sig_num})') + print (f'Stop by Signal <{signame}> ({sig_num}) at: {time.strftime("%d.%m.%Y %H:%M:%S")}') + + if mqtt_client: + mqtt_client.disco() + + if influx_client: + influx_client.disco() - for inverter in inverters: - if hoymiles.HOYMILES_DEBUG_LOGGING: - print(f'Poll inverter {inverter["serial"]}') - poll_inverter(inverter, do_init) + if volkszaehler_client: + volkszaehler_client.disco() + + sys.exit(0) + +signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C) +signal(SIGTERM, signal_handler) # Signal Handler from terminating processes +signal(SIGHUP, signal_handler) # Hangup detected on controlling terminal or death of controlling process +# signal(SIGKILL, signal_handler) # Signal Handler SIGKILL and SIGSTOP cannot be caught, blocked, or ignored!! +################################################################################ +################################################################################ class InfoCommands(IntEnum): InverterDevInform_Simple = 0 # 0x00 @@ -48,7 +68,96 @@ class InfoCommands(IntEnum): GetSelfCheckState = 30 # 0x1e InitDataState = 0xff -def poll_inverter(inverter, do_init, retries=4): +class SunsetHandler: + def __init__(self, sunset_config): + self.suntimes = None + if sunset_config and sunset_config.get('disabled', True) == False: + latitude = sunset_config.get('latitude') + longitude = sunset_config.get('longitude') + altitude = sunset_config.get('altitude') + self.suntimes = SunTimes(longitude=longitude, latitude=latitude, altitude=altitude) + self.nextSunset = self.suntimes.setutc(datetime.utcnow()) + logging.info (f'Todays sunset is at {self.nextSunset} UTC') + else: + logging.info('Sunset disabled.') + + def checkWaitForSunrise(self): + if not self.suntimes: + return + # if the sunset already happened for today + now = datetime.utcnow() + if self.nextSunset < now: + # wait until the sun rises again. if it's already after midnight, this will be today + nextSunrise = self.suntimes.riseutc(now) + if nextSunrise < now: + tomorrow = now + timedelta(days=1) + nextSunrise = self.suntimes.riseutc(tomorrow) + self.nextSunset = self.suntimes.setutc(nextSunrise) + time_to_sleep = int((nextSunrise - datetime.utcnow()).total_seconds()) + logging.info (f'Next sunrise is at {nextSunrise} UTC, next sunset is at {self.nextSunset} UTC, sleeping for {time_to_sleep} seconds.') + if time_to_sleep > 0: + time.sleep(time_to_sleep) + logging.info (f'Woke up...') + + def sun_status2mqtt(self, dtu_ser, dtu_name): + if not mqtt_client: + return + local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key + if self.suntimes: + mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \ + {'dis_night_comm' : 'True', \ + 'local_sunrise' : local_sunrise, \ + 'local_sunset' : local_sunset, + 'local_zone' : local_zone}) + else: + mqtt_client.sun_info2mqtt({'sun_topic': f'{dtu_name}/{dtu_ser}'}, \ + {'dis_night_comm': 'False'}) + + +def main_loop(ahoy_config): + """Main loop""" + inverters = [ + inverter for inverter in ahoy_config.get('inverters', []) + if not inverter.get('disabled', False)] + + sunset = SunsetHandler(ahoy_config.get('sunset')) + dtu_ser = ahoy_config.get('dtu', {}).get('serial', None) + dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') + sunset.sun_status2mqtt(dtu_ser, dtu_name) + loop_interval = ahoy_config.get('interval', 1) + + try: + do_init = True + while True: + sunset.checkWaitForSunrise() + + t_loop_start = time.time() + + for inverter in inverters: + if not 'name' in inverter: + inverter['name'] = 'hoymiles' + if not 'serial' in inverter: + logging.error("No inverter serial number found in ahoy.yml - exit") + sys.exit(999) + if hoymiles.HOYMILES_DEBUG_LOGGING: + logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') + poll_inverter(inverter, dtu_ser, do_init, 3) + do_init = False + + if loop_interval > 0: + time_to_sleep = loop_interval - (time.time() - t_loop_start) + if time_to_sleep > 0: + time.sleep(time_to_sleep) + + except Exception as e: + logging.fatal('Exception catched: %s' % e) + logging.fatal(traceback.print_exc()) + raise + + +def poll_inverter(inverter, dtu_ser, do_init, retries): """ Send/Receive command_queue, initiate status poll on inverter @@ -57,7 +166,8 @@ def poll_inverter(inverter, do_init, retries=4): :type retries: int """ inverter_ser = inverter.get('serial') - dtu_ser = ahoy_config.get('dtu', {}).get('serial') + inverter_name = inverter.get('name') + inverter_strings = inverter.get('strings') # Queue at least status data request inv_str = str(inverter_ser) @@ -91,90 +201,55 @@ def poll_inverter(inverter, do_init, retries=4): response = com.get_payload() payload_ttl = 0 except Exception as e_all: - print(f'Error while retrieving data: {e_all}') + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + logging.error(f'Error while retrieving data: {e_all}') pass # Handle the response data if any if response: - c_datetime = datetime.now() - if hoymiles.HOYMILES_DEBUG_LOGGING: - print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + c_datetime = datetime.now() + logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) + + # prepare decoder object decoder = hoymiles.ResponseDecoder(response, request=com.request, - inverter_ser=inverter_ser + inverter_ser=inverter_ser, + inverter_name=inverter_name, + dtu_ser=dtu_ser, + strings=inverter_strings ) + + # get decoder object result = decoder.decode() - if isinstance(result, hoymiles.decoders.StatusResponse): - data = result.__dict__() + if hoymiles.HOYMILES_DEBUG_LOGGING: + logging.info(f'Decoded: {result.__dict__()}') - if hoymiles.HOYMILES_DEBUG_LOGGING: - print(f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}', end='') - if data['powerfactor'] is not None: - print(f', pf={data["powerfactor"]}', end='') - phase_id = 0 - for phase in data['phases']: - print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='') - phase_id = phase_id + 1 - string_id = 0 - for string in data['strings']: - print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='') - string_id = string_id + 1 - print() + # check decoder object for output + if isinstance(result, hoymiles.decoders.StatusResponse): + data = result.__dict__() if 'event_count' in data: if event_message_index[inv_str] < data['event_count']: event_message_index[inv_str] = data['event_count'] command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) if mqtt_client: - mqtt_send_status(mqtt_client, inverter_ser, data, - topic=inverter.get('mqtt', {}).get('topic', None)) + mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) + if influx_client: - influx_client.store_status(result) + influx_client.store_status(result) if volkszaehler_client: - volkszaehler_client.store_status(result) + volkszaehler_client.store_status(result) if prometheus_client: prometheus_client.store_status(result) -def mqtt_send_status(broker, inverter_ser, data, topic=None): - """ - Publish StatusResponse object - - :param paho.mqtt.client.Client broker: mqtt-client instance - :param str inverter_ser: inverter serial - :param hoymiles.StatusResponse data: decoded inverter StatusResponse - :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) - :type topic: str - """ - - if not topic: - topic = f'hoymiles/{inverter_ser}' - - # AC Data - phase_id = 0 - for phase in data['phases']: - broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) - broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) - phase_id = phase_id + 1 - - # DC Data - string_id = 0 - for string in data['strings']: - broker.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) - broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) - broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) - broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) - string_id = string_id + 1 - # Global - if data['powerfactor'] is not None: - broker.publish(f'{topic}/pf', data['powerfactor']) - broker.publish(f'{topic}/frequency', data['frequency']) - broker.publish(f'{topic}/temperature', data['temperature']) - if data['energy_total'] is not None: - broker.publish(f'{topic}/total', data['energy_total']/1000) + # check decoder object for output + if isinstance(result, hoymiles.decoders.HardwareInfoResponse): + if mqtt_client: + mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) def mqtt_on_command(client, userdata, message): """ @@ -203,7 +278,7 @@ def mqtt_on_command(client, userdata, message): inverter_ser = next( item[0] for item in mqtt_command_topic_subs if item[1] == message.topic) except StopIteration: - print('Unexpedtedly received mqtt message for {message.topic}') + logging.warning('Unexpedtedly received mqtt message for {message.topic}') if inverter_ser: p_message = message.payload.decode('utf-8').lower() @@ -221,14 +296,37 @@ def mqtt_on_command(client, userdata, message): command_queue[str(inverter_ser)].append( hoymiles.frame_payload(payload[1:])) +def init_logging(ahoy_config): + log_config = ahoy_config.get('logging') + fn = 'hoymiles.log' + lvl = logging.ERROR + if log_config: + fn = log_config.get('filename', fn) + level = log_config.get('level', 'ERROR') + if level == 'DEBUG': + lvl = logging.DEBUG + elif level == 'INFO': + lvl = logging.INFO + elif level == 'WARNING': + lvl = logging.WARNING + elif level == 'ERROR': + lvl = logging.ERROR + elif level == 'FATAL': + lvl = logging.FATAL + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + lvl = logging.DEBUG + logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) + dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu') + logging.info(f'start logging for {dtu_name} with level: {logging.root.level}') + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") parser.add_argument("-c", "--config-file", nargs="?", required=True, help="configuration file") parser.add_argument("--log-transactions", action="store_true", default=False, - help="Enable transaction logging output") + help="Enable transaction logging output (loglevel must be DEBUG)") parser.add_argument("--verbose", action="store_true", default=False, - help="Enable debug output") + help="Enable detailed debug output (loglevel must be DEBUG)") global_config = parser.parse_args() # Load ahoy.yml config file @@ -240,45 +338,35 @@ if __name__ == '__main__': with open('ahoy.yml', 'r') as fh_yaml: cfg = yaml.load(fh_yaml, Loader=SafeLoader) except FileNotFoundError: - print("Could not load config file. Try --help") + logging.error("Could not load config file. Try --help") sys.exit(2) except yaml.YAMLError as e_yaml: - print('Failed to load config frile {global_config.config_file}: {e_yaml}') + logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}') sys.exit(1) - ahoy_config = dict(cfg.get('ahoy', {})) - - # Prepare for multiple transceivers, makes them configurable (currently - # only one supported) - for radio_config in ahoy_config.get('nrf', [{}]): - hmradio = hoymiles.HoymilesNRF(**radio_config) - - mqtt_client = None - - event_message_index = {} - command_queue = {} - mqtt_command_topic_subs = [] - if global_config.log_transactions: hoymiles.HOYMILES_TRANSACTION_LOGGING=True if global_config.verbose: hoymiles.HOYMILES_DEBUG_LOGGING=True - mqtt_config = ahoy_config.get('mqtt', []) - if not mqtt_config.get('disabled', False): - mqtt_client = paho.mqtt.client.Client() - - if mqtt_config.get('useTLS',False): - mqtt_client.tls_set() - mqtt_client.tls_insecure_set(mqtt_config.get('insecureTLS',False)) + # read AHOY configuration file and prepare logging + ahoy_config = dict(cfg.get('ahoy', {})) + init_logging(ahoy_config) - mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None)) - mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883)) - mqtt_client.loop_start() - mqtt_client.on_message = mqtt_on_command + # Prepare for multiple transceivers, makes them configurable + for radio_config in ahoy_config.get('nrf', [{}]): + hmradio = hoymiles.HoymilesNRF(**radio_config) + + # create MQTT - client object + mqtt_client = None + mqtt_config = ahoy_config.get('mqtt', None) + if mqtt_config and not mqtt_config.get('disabled', False): + from .outputs import MqttOutputPlugin + mqtt_client = MqttOutputPlugin(mqtt_config) + # create INFLUX - client object influx_client = None - influx_config = ahoy_config.get('influxdb', {}) + influx_config = ahoy_config.get('influxdb', None) if influx_config and not influx_config.get('disabled', False): from .outputs import InfluxOutputPlugin influx_client = InfluxOutputPlugin( @@ -288,6 +376,7 @@ if __name__ == '__main__': bucket=influx_config.get('bucket', None), measurement=influx_config.get('measurement', 'hoymiles')) + # create prometheus - client object prometheus_client = None prometheus_config = ahoy_config.get('prometheus', {}) if prometheus_config and not prometheus_config.get('disabled', False): @@ -295,23 +384,24 @@ if __name__ == '__main__': prometheus_client = PrometheusOutputPlugin( prometheus_config) + # create VOLKSZAEHLER - client object volkszaehler_client = None volkszaehler_config = ahoy_config.get('volkszaehler', {}) if volkszaehler_config and not volkszaehler_config.get('disabled', False): from .outputs import VolkszaehlerOutputPlugin - volkszaehler_client = VolkszaehlerOutputPlugin( - volkszaehler_config) + volkszaehler_client = VolkszaehlerOutputPlugin(volkszaehler_config) + + event_message_index = {} + command_queue = {} + mqtt_command_topic_subs = [] - g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])] for g_inverter in ahoy_config.get('inverters', []): g_inverter_ser = g_inverter.get('serial') inv_str = str(g_inverter_ser) command_queue[inv_str] = [] event_message_index[inv_str] = 0 - # # Enables and subscribe inverter to mqtt /command-Topic - # if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False): topic_item = ( str(g_inverter_ser), @@ -320,23 +410,5 @@ if __name__ == '__main__': mqtt_client.subscribe(topic_item[1]) mqtt_command_topic_subs.append(topic_item) - loop_interval = ahoy_config.get('interval', 1) - try: - do_init = True - while True: - t_loop_start = time.time() - - main_loop(do_init) - - do_init = False - - print('', end='', flush=True) - - if loop_interval > 0 and (time.time() - t_loop_start) < loop_interval: - time.sleep(loop_interval - (time.time() - t_loop_start)) - - except KeyboardInterrupt: - sys.exit() - except Exception as e: - print ('Exception catched: %s' % e) - raise + # start main-loop + main_loop(ahoy_config) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 19062419..bb32fb07 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -8,6 +8,7 @@ Hoymiles Micro-Inverters decoder library import struct from datetime import datetime, timedelta import crcmod +import logging f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) @@ -44,18 +45,20 @@ def print_table_unpack(s_fmt, payload, cw=6): l_hexlified = [f'{byte:02x}' for byte in payload] - print(f'{"Pos": <{cw}}', end='') - print(''.join([f'{num: >{cw}}' for num in range(0, len(payload))])) - print(f'{"Hex": <{cw}}', end='') - print(''.join([f'{byte: >{cw}}' for byte in l_hexlified])) + dbg = f'{"Pos": <{cw}}' + dbg += ''.join([f'{num: >{cw}}' for num in range(0, len(payload))]) + logging.debug(dbg) + dbg = f'{"Hex": <{cw}}' + dbg += ''.join([f'{byte: >{cw}}' for byte in l_hexlified]) + logging.debug(dbg) l_fmt = struct.calcsize(s_fmt) if len(payload) >= l_fmt: for offset in range(0, l_fmt): - print(f'{s_fmt: <{cw}}', end='') - print(' ' * cw * offset, end='') - print(''.join( - [f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])])) + dbg = f'{s_fmt: <{cw}}' + dbg += ' ' * cw * offset + dbg += ''.join([f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])]) + logging.debug(dbg) class Response: """ All Response Shared methods """ @@ -71,9 +74,11 @@ class Response: self.inverter_ser = params.get('inverter_ser', None) self.inverter_name = params.get('inverter_name', None) self.dtu_ser = params.get('dtu_ser', None) - self.response = args[0] + strings = params.get('strings', None) + self.inv_strings = strings + if isinstance(params.get('time_rx', None), datetime): self.time_rx = params['time_rx'] else: @@ -88,11 +93,13 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] + phase_keys = ['voltage','current','power','reactive_power','frequency'] + string_keys = ['voltage','current','power','energy_total','energy_daily', 'irradiation'] temperature = None frequency = None powerfactor = None event_count = None + unpack_error = False def unpack(self, fmt, base): """ @@ -104,6 +111,10 @@ class StatusResponse(Response): :rtype: tuple """ size = struct.calcsize(fmt) + if (len(self.response) < base+size): + self.unpack_error = True + logging.error(f'base: {base} size: {size} len: {len(self.response)} fmt: {fmt} rep: {self.response}') + return [0] return struct.unpack(fmt, self.response[base:base+size]) @property @@ -120,7 +131,7 @@ class StatusResponse(Response): p_exists = False phase_id = len(phases) phase = {} - for key in self.e_keys: + for key in self.phase_keys: prop = f'ac_{key}_{phase_id}' if hasattr(self, prop): p_exists = True @@ -144,7 +155,8 @@ class StatusResponse(Response): s_exists = False string_id = len(strings) string = {} - for key in self.e_keys: + string['name'] = self.inv_strings[string_id]['s_name'] + for key in self.string_keys: prop = f'dc_{key}_{string_id}' if hasattr(self, prop): s_exists = True @@ -165,16 +177,30 @@ class StatusResponse(Response): data['phases'] = self.phases data['strings'] = self.strings data['temperature'] = self.temperature - data['frequency'] = self.frequency data['powerfactor'] = self.powerfactor - data['event_count'] = self.event_count - data['time'] = self.time_rx - data['energy_total'] = 0.0 + data['yield_total'] = 0.0 + data['yield_today'] = 0.0 for string in data['strings']: - data['energy_total'] += string['energy_total'] + data['yield_total'] += string['energy_total'] + data['yield_today'] += string['energy_daily'] - return data + ac_sum_power = 0.0 + for phase in data['phases']: + ac_sum_power += phase['power'] + dc_sum_power = 0.0 + for string in data['strings']: + dc_sum_power += string['power'] + if dc_sum_power != 0: + data['efficiency'] = round(ac_sum_power * 100 / dc_sum_power, 2) + else: + data['efficiency'] = 0.0 + + data['event_count'] = self.event_count + data['time'] = self.time_rx + + if not self.unpack_error: + return data class UnknownResponse(Response): """ @@ -299,27 +325,39 @@ class EventsResponse(UnknownResponse): crc_valid = self.validate_crc_m() if crc_valid: - #print(' payload has valid modbus crc') + #logging.debug(' payload has valid modbus crc') self.response = self.response[:-2] - status = struct.unpack('>H', self.response[:2])[0] - a_text = self.alarm_codes.get(status, 'N/A') - print (f' Inverter status: {a_text} ({status})') + self.status = struct.unpack('>H', self.response[:2])[0] + self.a_text = self.alarm_codes.get(self.status, 'N/A') + logging.info (f'Inverter status: {self.a_text} ({self.status})') chunk_size = 12 for i_chunk in range(2, len(self.response), chunk_size): chunk = self.response[i_chunk:i_chunk+chunk_size] - print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ') + logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ') + + if (len(chunk[0:6]) < 6): + logging.error(f'length of chunk must be greater or equal 6 bytes: {chunk}') + return opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6]) a_text = self.alarm_codes.get(a_code, 'N/A') + logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}') - print(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}') - + dbg = '' for fmt in ['BBHHHHH']: - print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))) - print(end='', flush=True) + dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)) + logging.debug(dbg) + + def __dict__(self): + """ Base values, availabe in each __dict__ call """ + + data = super().__dict__() + data['inv_stat_num'] = self.status + data['inv_stat_txt'] = self.a_text + return data class HardwareInfoResponse(UnknownResponse): def __init__(self, *args, **params): @@ -329,19 +367,49 @@ class HardwareInfoResponse(UnknownResponse): { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, - { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } + { FLD_FW_Build_Hour_Minute, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 }, + { FLD_unknown, UNIT_NONE, CH0, 10, 2, 1 }, + { FLD_unknown, UNIT_NONE, CH0, 12, 2, 1 }, + { FLD_CRC-M, UNIT_NONE, CH0, 14, 2, 1 } }; self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') """ - fw_version, fw_build_yyyy, fw_build_mmdd, unknown, hw_id = struct.unpack('>HHHHH', self.response[0:10]) + + def __dict__(self): + """ Base values, availabe in each __dict__ call """ + + data = super().__dict__() + + if (len(self.response) != 16): + logging.error(f'HardwareInfoResponse: data length should be 16 bytes - measured {len(self.response)} bytes') + logging.error(f'HardwareInfoResponse: data: {self.response}') + return data + + logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", self.response[0:16])}') + fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10]) fw_version_maj = int((fw_version / 10000)) fw_version_min = int((fw_version % 10000) / 100) fw_version_pat = int((fw_version % 100)) fw_build_mm = int(fw_build_mmdd / 100) fw_build_dd = int(fw_build_mmdd % 100) - print() - print(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}') + fw_build_HH = int(fw_build_hhmm / 100) + fw_build_MM = int(fw_build_hhmm % 100) + logging.info(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\ + f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\ + f'HW revision {hw_id}') + + data['FW_ver_maj'] = fw_version_maj + data['FW_ver_min'] = fw_version_min + data['FW_ver_pat'] = fw_version_pat + data['FW_build_yy'] = fw_build_yyyy + data['FW_build_mm'] = fw_build_mm + data['FW_build_dd'] = fw_build_dd + data['FW_build_HH'] = fw_build_HH + data['FW_build_MM'] = fw_build_MM + data['FW_HW_ID'] = hw_id + return data class DebugDecodeAny(UnknownResponse): """Default decoder""" @@ -351,48 +419,48 @@ class DebugDecodeAny(UnknownResponse): crc8_valid = self.validate_crc8() if crc8_valid: - print(' payload has valid crc8') + logging.debug(' payload has valid crc8') self.response = self.response[:-1] crc_valid = self.validate_crc_m() if crc_valid: - print(' payload has valid modbus crc') + logging.debug(' payload has valid modbus crc') self.response = self.response[:-2] l_payload = len(self.response) - print(f' payload has {l_payload} bytes') + logging.debug(f' payload has {l_payload} bytes') - print() - print('Field view: int') + logging.debug() + logging.debug('Field view: int') print_table_unpack('>B', self.response) - print() - print('Field view: shorts') + logging.debug() + logging.debug('Field view: shorts') print_table_unpack('>H', self.response) - print() - print('Field view: longs') + logging.debug() + logging.debug('Field view: longs') print_table_unpack('>L', self.response) try: if len(self.response) > 2: - print(' type utf-8 : ' + self.response.decode('utf-8')) + logging.debug(' type utf-8 : ' + self.response.decode('utf-8')) except UnicodeDecodeError: - print(' type utf-8 : utf-8 decode error') + logging.debug(' type utf-8 : utf-8 decode error') try: if len(self.response) > 2: - print(' type ascii : ' + self.response.decode('ascii')) + logging.debug(' type ascii : ' + self.response.decode('ascii')) except UnicodeDecodeError: - print(' type ascii : ascii decode error') + logging.debug(' type ascii : ascii decode error') # 1121-Series Intervers, 1 MPPT, 1 Phase class Hm300Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1121-series Firmware version / date """ class Hm300Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1121-series Inverter generic events log """ class Hm300Decode0B(StatusResponse): """ 1121-series mirco-inverters status data """ @@ -417,6 +485,14 @@ class Hm300Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 12)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def ac_voltage_0(self): @@ -431,30 +507,34 @@ class Hm300Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 18)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 16)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 20)[0]/10 + @property def temperature(self): """ Inverter temperature in °C """ - return self.unpack('>H', 26)[0]/10 + return self.unpack('>h', 26)[0]/10 class Hm300Decode0C(Hm300Decode0B): """ 1121-series mirco-inverters status data """ class Hm300Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1121-series Inverter generic events log """ class Hm300Decode12(EventsResponse): - """ Inverter major events log """ + """ 1121-series Inverter major events log """ # 1141-Series Inverters, 2 MPPT, 1 Phase class Hm600Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1141-Series Firmware version / date """ class Hm600Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1141-Series Inverter generic events log """ class Hm600Decode0B(StatusResponse): """ 1141-series mirco-inverters status data """ @@ -479,6 +559,14 @@ class Hm600Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 22)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def dc_voltage_1(self): @@ -500,6 +588,14 @@ class Hm600Decode0B(StatusResponse): def dc_energy_daily_1(self): """ String 2 daily energy in Wh """ return self.unpack('>H', 24)[0] + @property + def dc_irradiation_1(self): + """ String 2 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[1]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property def ac_voltage_0(self): @@ -508,23 +604,27 @@ class Hm600Decode0B(StatusResponse): @property def ac_current_0(self): """ Phase 1 ampere """ - return self.unpack('>H', 34)[0]/10 + return self.unpack('>H', 34)[0]/100 @property def ac_power_0(self): """ Phase 1 watts """ return self.unpack('>H', 30)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 28)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 32)[0]/10 + @property def powerfactor(self): """ Powerfactor """ return self.unpack('>H', 36)[0]/1000 @property def temperature(self): """ Inverter temperature in °C """ - return self.unpack('>H', 38)[0]/10 + return self.unpack('>h', 38)[0]/10 @property def event_count(self): """ Event counter """ @@ -534,18 +634,18 @@ class Hm600Decode0C(Hm600Decode0B): """ 1141-series mirco-inverters status data """ class Hm600Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1141-Series Inverter generic events log """ class Hm600Decode12(EventsResponse): - """ Inverter major events log """ + """ 1141-Series Inverter major events log """ # 1161-Series Inverters, 2 MPPT, 1 Phase class Hm1200Decode01(HardwareInfoResponse): - """ Firmware version / date """ + """ 1161-Series Firmware version / date """ class Hm1200Decode02(EventsResponse): - """ Inverter generic events log """ + """ 1161-Series Inverter generic events log """ class Hm1200Decode0B(StatusResponse): """ 1161-series mirco-inverters status data """ @@ -570,6 +670,14 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 20)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def dc_voltage_1(self): @@ -591,6 +699,14 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_1(self): """ String 2 daily energy in Wh """ return self.unpack('>H', 22)[0] + @property + def dc_irradiation_0(self): + """ String 2 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[1]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property def dc_voltage_2(self): @@ -612,6 +728,14 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_2(self): """ String 3 daily energy in Wh """ return self.unpack('>H', 42)[0] + @property + def dc_irradiation_0(self): + """ String 3 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[2]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3) @property def dc_voltage_3(self): @@ -633,6 +757,14 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_3(self): """ String 4 daily energy in Wh """ return self.unpack('>H', 44)[0] + @property + def dc_irradiation_0(self): + """ String 4 irratiation in percent """ + if self.inv_strings is None: + return None + if self.inv_strings[3]['s_maxpower'] == 0: + return 0.00 + return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3) @property def ac_voltage_0(self): @@ -647,17 +779,21 @@ class Hm1200Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 50)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 48)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 52)[0]/10 + @property def powerfactor(self): """ Powerfactor """ return self.unpack('>H', 56)[0]/1000 @property def temperature(self): """ Inverter temperature in °C """ - return self.unpack('>H', 58)[0]/10 + return self.unpack('>h', 58)[0]/10 @property def event_count(self): """ Event counter """ @@ -667,7 +803,7 @@ class Hm1200Decode0C(Hm1200Decode0B): """ 1161-series mirco-inverters status data """ class Hm1200Decode11(EventsResponse): - """ Inverter generic events log """ + """ 1161-Series Inverter generic events log """ class Hm1200Decode12(EventsResponse): - """ Inverter major events log """ + """ 1161-Series Inverter major events log """ diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 84ab278b..a7c8e80a 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -6,14 +6,10 @@ Hoymiles output plugin library """ import socket +import logging from datetime import datetime, timezone -from hoymiles.decoders import StatusResponse -from os import path - -try: - from influxdb_client import InfluxDBClient -except ModuleNotFoundError: - pass +from hoymiles.decoders import StatusResponse, HardwareInfoResponse +from hoymiles import HOYMILES_TRANSACTION_LOGGING, HOYMILES_DEBUG_LOGGING class OutputPluginFactory: def __init__(self, **params): @@ -116,6 +112,7 @@ class InfluxOutputPlugin(OutputPluginFactory): def __init__(self, url, token, **params): """ Initialize InfluxOutputPlugin + https://influxdb-client.readthedocs.io/en/stable/api.html#influxdbclient The following targets must be present in your InfluxDB. This does not automatically create anything for You. @@ -131,13 +128,26 @@ class InfluxOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + from influxdb_client import InfluxDBClient + except ModuleNotFoundError: + ErrorText1 = f'Module "influxdb_client" for INFLUXDB necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install influxdb_client' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() + self._bucket = params.get('bucket', 'hoymiles/autogen') self._org = params.get('org', '') - self._measurement = params.get('measurement', - f'inverter,host={socket.gethostname()}') + self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}') + + with InfluxDBClient(url, token, bucket=self._bucket) as self.client: + self.api = self.client.write_api() - client = InfluxDBClient(url, token, bucket=self._bucket) - self.api = client.write_api() + def disco(self, **params): + self.client.close() # Shutdown the client + return def store_status(self, response, **params): """ @@ -170,44 +180,52 @@ class InfluxOutputPlugin(OutputPluginFactory): # InfluxDB requires nanoseconds ctime = int(utctime.timestamp() * 1e9) + if HOYMILES_DEBUG_LOGGING: + logging.info(f'InfluxDB: utctime: {utctime}') + # AC Data phase_id = 0 for phase in data['phases']: - data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=voltage value={phase["voltage"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=frequency value={phase["frequency"]:.3f} {ctime}') phase_id = phase_id + 1 # DC Data string_id = 0 for string in data['strings']: - data_stack.append(f'{measurement},string={string_id},type=total value={string["energy_total"]/1000:.4f} {ctime}') - data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=voltage value={string["voltage"]:.3f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=YieldDay value={string["energy_daily"]:.2f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}') string_id = string_id + 1 + # Global if data['event_count'] is not None: data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}') if data['powerfactor'] is not None: - data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}') - data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') - data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}') - if data['energy_total'] is not None: - data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}') - + data_stack.append(f'{measurement},type=PF_AC value={data["powerfactor"]:f} {ctime}') + data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}') + if data['yield_total'] is not None: + data_stack.append(f'{measurement},type=YieldTotal value={data["yield_total"]/1000:.3f} {ctime}') + if data['yield_today'] is not None: + data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}') + data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}') + + if HOYMILES_DEBUG_LOGGING: + #logging.debug(f'INFLUX data to DB: {data_stack}') + pass self.api.write(self._bucket, self._org, data_stack) -try: - import paho.mqtt.client -except ModuleNotFoundError: - pass - class MqttOutputPlugin(OutputPluginFactory): """ Mqtt output plugin """ client = None - def __init__(self, *args, **params): + def __init__(self, config, **params): """ Initialize MqttOutputPlugin @@ -228,14 +246,46 @@ class MqttOutputPlugin(OutputPluginFactory): :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) :type topic: str """ - super().__init__(*args, **params) + super().__init__(**params) + + try: + import paho.mqtt.client + except ModuleNotFoundError: + ErrorText1 = f'Module "paho.mqtt.client" for MQTT-output necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install paho-mqtt' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() mqtt_client = paho.mqtt.client.Client() - mqtt_client.username_pw_set(params.get('user', None), params.get('password', None)) - mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883)) + if config.get('useTLS',False): + mqtt_client.tls_set() + mqtt_client.tls_insecure_set(config.get('insecureTLS',False)) + mqtt_client.username_pw_set(config.get('user', None), config.get('password', None)) + + last_will = config.get('last_will', None) + if last_will: + lw_topic = last_will.get('topic', 'last will hoymiles') + lw_payload = last_will.get('payload', 'last will') + mqtt_client.will_set(str(lw_topic), str(lw_payload)) + + mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883)) mqtt_client.loop_start() self.client = mqtt_client + self.qos = config.get('QoS', 0) # Quality of Service + self.ret = config.get('Retain', True) # Retain Message + + def disco(self, **params): + self.client.loop_stop() # Stop loop + self.client.disconnect() # disconnect + return + + def info2mqtt(self, mqtt_topic, mqtt_data): + for mqtt_key in mqtt_data: + self.client.publish(f'{mqtt_topic["topic"]}/{mqtt_key}', mqtt_data[mqtt_key], self.qos, self.ret) + return def store_status(self, response, **params): """ @@ -248,42 +298,75 @@ class MqttOutputPlugin(OutputPluginFactory): :raises ValueError: when response is not instance of StatusResponse """ - if not isinstance(response, StatusResponse): - raise ValueError('Data needs to be instance of StatusResponse') - data = response.__dict__() - - topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') - - # AC Data - phase_id = 0 - for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) - self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) - phase_id = phase_id + 1 - - # DC Data - string_id = 0 - for string in data['strings']: - self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) - self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) - self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) - string_id = string_id + 1 - # Global - if data['powerfactor'] is not None: - self.client.publish(f'{topic}/pf', data['powerfactor']) - self.client.publish(f'{topic}/frequency', data['frequency']) - self.client.publish(f'{topic}/temperature', data['temperature']) - if data['energy_total'] is not None: - self.client.publish(f'{topic}/total', data['energy_total']/1000) - -try: - import requests - import time -except ModuleNotFoundError: - pass + topic = params.get('topic', None) + if not topic: + topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' + + if HOYMILES_DEBUG_LOGGING: + logging.info(f'MQTT-topic: {topic} data-type: {type(response)}') + + if isinstance(response, StatusResponse): + + # Global Head + if data['time'] is not None: + self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%YT%H:%M:%S"), self.qos, self.ret) + + # AC Data + phase_id = 0 + phase_sum_power = 0 + for phase in data['phases']: + self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret) + phase_id = phase_id + 1 + phase_sum_power += phase['power'] + + # DC Data + string_id = 0 + string_sum_power = 0 + for string in data['strings']: + if 'name' in string: + string_name = string['name'].replace(" ","_") + else: + string_name = string_id + self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret) + string_id = string_id + 1 + string_sum_power += string['power'] + + # Global + if data['event_count'] is not None: + self.client.publish(f'{topic}/total_events', data['event_count'], self.qos, self.ret) + if data['powerfactor'] is not None: + self.client.publish(f'{topic}/PF_AC', data['powerfactor'], self.qos, self.ret) + self.client.publish(f'{topic}/Temp', data['temperature'], self.qos, self.ret) + if data['yield_total'] is not None: + self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000, self.qos, self.ret) + if data['yield_today'] is not None: + self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/Efficiency', data['efficiency'], self.qos, self.ret) + + + elif isinstance(response, HardwareInfoResponse): + self.client.publish(f'{topic}/Firmware/Version',\ + f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret) + + self.client.publish(f'{topic}/Firmware/Build_at',\ + f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}',\ + self.qos, self.ret) + + self.client.publish(f'{topic}/Firmware/HWPartId',\ + f'{data["FW_HW_ID"]}', self.qos, self.ret) + + else: + raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') class VzInverterOutput: def __init__(self, config, session): @@ -291,10 +374,12 @@ class VzInverterOutput: self.serial = config.get('serial') self.baseurl = config.get('url', 'http://localhost/middleware/') self.channels = dict() + for channel in config.get('channels', []): - uid = channel.get('uid') + uid = channel.get('uid', None) ctype = channel.get('type') - if uid and ctype: + # if uid and ctype: + if ctype: self.channels[ctype] = uid def store_status(self, data, session): @@ -310,43 +395,70 @@ class VzInverterOutput: ts = int(round(data['time'].timestamp() * 1000)) + if HOYMILES_DEBUG_LOGGING: + logging.info(f'Volkszaehler-Timestamp: {ts}') + # AC Data phase_id = 0 for phase in data['phases']: - self.try_publish(ts, f'ac_power{phase_id}', phase['power']) self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_current{phase_id}', phase['current']) + self.try_publish(ts, f'ac_power{phase_id}', phase['power']) + self.try_publish(ts, f'ac_reactive_power{phase_id}', phase['reactive_power']) + self.try_publish(ts, f'ac_frequency{phase_id}', phase['frequency']) phase_id = phase_id + 1 # DC Data string_id = 0 for string in data['strings']: - self.try_publish(ts, f'dc_power{string_id}', string['power']) self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) self.try_publish(ts, f'dc_current{string_id}', string['current']) - self.try_publish(ts, f'dc_total{string_id}', string['energy_total']) - self.try_publish(ts, f'dc_daily{string_id}', string['energy_daily']) + self.try_publish(ts, f'dc_power{string_id}', string['power']) + self.try_publish(ts, f'dc_energy_daily{string_id}', string['energy_daily']) + self.try_publish(ts, f'dc_energy_total{string_id}', string['energy_total']) + self.try_publish(ts, f'dc_irradiation{string_id}', string['irradiation']) string_id = string_id + 1 + # Global + if data['event_count'] is not None: + self.try_publish(ts, f'event_count', data['event_count']) if data['powerfactor'] is not None: self.try_publish(ts, f'powerfactor', data['powerfactor']) - self.try_publish(ts, f'frequency', data['frequency']) self.try_publish(ts, f'temperature', data['temperature']) - - if data['energy_total'] is not None: - self.try_publish(ts, f'total', data['energy_total']) + if data['yield_total'] is not None: + self.try_publish(ts, f'yield_total', data['yield_total']) + if data['yield_today'] is not None: + self.try_publish(ts, f'yield_today', data['yield_today']) + self.try_publish(ts, f'efficiency', data['efficiency']) + return def try_publish(self, ts, ctype, value): if not ctype in self.channels: + if HOYMILES_DEBUG_LOGGING: + logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml') return + uid = self.channels[ctype] url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' + if uid == None: + if HOYMILES_DEBUG_LOGGING: + logging.debug(f'ctype \"{ctype}\" has no configured uid-value in ahoy.yml') + return + + if HOYMILES_DEBUG_LOGGING: + logging.debug(f'VZ-url: {url}') + try: r = self.session.get(url) - if r.status_code != 200: - raise ValueError('Could not send request (%s)' % url) - except requests.exceptions.ConnectionError as e: - raise ValueError('Could not send request (%s)' % e) + if r.status_code == 404: + logging.critical('VZ-DB not reachable, please check "middleware"') + if r.status_code == 400: + logging.critical('UUID not configured in VZ-DB') + elif r.status_code != 200: + raise ValueError(f'Transmit result {url}') + except ConnectionError as e: + raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}') + return class VolkszaehlerOutputPlugin(OutputPluginFactory): def __init__(self, config, **params): @@ -355,13 +467,29 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + import requests + import time + except ModuleNotFoundError: + ErrorText1 = f'Module "requests" and "time" for VolkszaehlerOutputPlugin necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install requests' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit(1) + self.session = requests.Session() + self.inverters = dict() for inverterconfig in config.get('inverters', []): serial = inverterconfig.get('serial') output = VzInverterOutput(inverterconfig, self.session) self.inverters[serial] = output + def disco(self, **params): + self.session.close() # closing the connection + return + def store_status(self, response, **params): """ Publish StatusResponse object @@ -370,6 +498,8 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): :raises ValueError: when response is not instance of StatusResponse """ + + # check decoder object for output if not isinstance(response, StatusResponse): raise ValueError('Data needs to be instance of StatusResponse') @@ -383,6 +513,5 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): try: output.store_status(data, self.session) except ValueError as e: - print('Could not send data to volkszaehler instance: %s' % e) - - + logging.warning('Could not send data to volkszaehler instance: %s' % e) + return