From 35e530c0c9f6f06c00f7a3500d01e5f23741eb77 Mon Sep 17 00:00:00 2001 From: DanielR92 Date: Fri, 29 Dec 2023 19:55:10 +0100 Subject: [PATCH 001/115] Some changes in readme --- Getting_Started.md | 188 ++++++++++++++++++++++----------------------- README.md | 12 +-- ahoy_config.md | 65 ++++++++++++++++ 3 files changed, 166 insertions(+), 99 deletions(-) create mode 100644 ahoy_config.md diff --git a/Getting_Started.md b/Getting_Started.md index f6a89dde..81193ed0 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -1,12 +1,13 @@ + ## Overview -On this page, you'll find detailed instructions on how to wire the module of a Wemos D1 mini or ESP32 to the radio module, as well as how to flash it with the latest firmware. This information will enable you to communicate with compatible inverters. +This page contains detailed instructions on building a module and flashing it with the latest firmware. Following these instructions will allow you to communicate with compatible inverters. You find the full [User_Manual here](User_Manual.md) ## Compatiblity -The following inverters are currently supported out of the box: +Currently, the following inverters are supported: Hoymiles Inverters @@ -20,7 +21,6 @@ Hoymiles Inverters ## Table of Contents -- [Table of Contents](#table-of-contents) - [Overview](#overview) - [Compatiblity](#compatiblity) - [Things needed](#things-needed) @@ -38,7 +38,6 @@ Hoymiles Inverters - [ESP32 GPIO settings](#esp32-gpio-settings) - [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware) - [Compiling your own Version](#compiling-your-own-version) - - [Optional Configuration before compilation](#optional-configuration-before-compilation) - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher) - [Connect to your Ahoy DTU](#connect-to-your-ahoy-dtu) - [Your Ahoy DTU is very verbose using the Serial Console](#your-ahoy-dtu-is-very-verbose-using-the-serial-console) @@ -56,47 +55,52 @@ Solenso Inverters: ## Things needed -If you're interested in building your own AhoyDTU, you'll need a few things to get started. While we've provided a list of recommended boards below, keep in mind that the maker community is constantly developing new and innovative options that we may not have covered in this readme.. +To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme. + +Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable. -For optimal performance, we recommend using a Wemos D1 mini or ESP32 along with a NRF24L01+ breakout board as a bare minimum. However, if you have experience working with other ESP boards, any board with at least 4MBytes of ROM may be suitable, depending on your skills. +Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant. + +**Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A! -Just be sure that the NRF24L01+ module you choose includes the "+" in its name, as we rely on the 250kbps features that are only provided by the plus-variant. | **Parts** | **Price** | | --- | --- | -| 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** | +| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 €| +| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*| +| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* | +| 100µF / 10V Capacitor Kondensator | 0,15 €| +| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 €| +| **Total costs** | **10,34 € / 11,48 €** | -If you're interested in using our sister project OpenDTU or you want to future-proof your setup, we recommend investing in an ESP32 board that features two CPU cores. As Radio you can also use a NRF24L01+ module with an external antenna. While this option may cost a bit more, it will provide superior performance and ensure compatibility with upcoming developments. +To future-proof your setup and use our sister project OpenDTU, we recommend investing in an ESP32 board with two CPU cores. Additionally, you can use a NRF24L01+ module with an external antenna as a radio for superior performance and compatibility with upcoming developments. | **Parts** | **Price** | | --- | --- | -| 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** | +| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 €| +| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*| +| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* | +| 100µF / 10V Capacitor Kondensator | 0,15 €| +| Jumper Wire breadboard female-female | 2,49 €| +| **Total costs** | **13,99 € / 15,13 €** | #### There are fake NRF24L01+ Modules out there +Beware of fake NRF24L01+ modules that use rebranded NRF24L01 chips (without the +). +An example of this can be found in Issue #230 (https://github.com/lumapu/ahoy/issues/230). +If you have any additional examples of fake chips, please share them with us and we will add the information here. -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/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! +#### NRF24L01+ improvements +Users have reported improved connections and longer range through walls when using these modules. +The "E01-ML01DP5" module is a 2.4 GHz wireless module that utilizes the nRF24L01+PA+LNA RF module and features an SMA-K antenna connector. +**The product includes an HF cover, but please note that it does not come with an 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... +To achieve the best results, stabilize the Vcc power by using a capacitor and do not exceed the 'LOW' Amplifier Power Level. +Users have reported good connections over 10m through walls and ceilings when using the Amplifier Power Level 'MIN'. +It's important to remember that bigger is not always 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. +If you are using the NRF24 directly on the ESP board, make sure to set the transmission power to the lowest possible level (this can be adjusted later in the web interface). Using a high transmission power can potentially cause problems. +The ESP board's built-in controller has limited reserves in case both WiFi and nRF are transmitting simultaneously. +If you are using additional interfaces, such as a display, the reserves will be further reduced. ## Wiring things up @@ -117,15 +121,12 @@ Additional, there are 3 pins, which can be set individual: *These pins can be changed from the /setup URL.* #### ESP8266 wiring example on WEMOS D1 - -This is an example wiring using a Wemos D1 mini.
+This is an example wiring using a Wemos D1 mini. ##### Schematic - ![Schematic](https://ahoydtu.de/img/fritzing/esp8266_nrf_sch.png) ##### Symbolic view - ![Symbolic](https://ahoydtu.de/img/fritzing/esp8266_nrf.png) #### ESP8266 wiring example on 30pin Lolin NodeMCU v3 @@ -162,42 +163,34 @@ CE D2 (GPIO4) IRQ D0 (GPIO16 - no IRQ!) ``` -IMPORTANT: From development version 108/release 0.6.0 onwards, also MISO, MOSI, and SCLK -are configurable. On new installations, their defaults are correct for most ESP32 boards. -These pins cannot be configured for ESP82xx boards, as this chip cannot move them elsewhere. +**IMPORTANT**: Starting from development version 108/release 0.6.0, MISO, MOSI, and SCLK are also included. + For most ESP32 boards, the default settings are correct on new installations. +However, it is not possible to configure these pins for ESP82xx boards, as they cannot be moved elsewhere. -If you are upgrading an existing install though, you might see that these pins are set to '0' in the web GUI. -Communication with the NRF module wont work. For upgrading an existing installations, set MISO=19, MOSI=23, SCLK=18 in the settings. -This is the correct default for most ESP32 boards. On ESP82xx, simply saving the settings without changes should suffice. +If you are upgrading an existing installation, you may notice that the pins are set to '0' in the web GUI, which will prevent communication with the NRF module. +To resolve this, set MISO=19, MOSI=23, SCLK=18 in the settings. +This is the correct default for most ESP32 boards. For ESP82xx, simply saving the settings without changes should suffice. Save and reboot. ## 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. +After preparing your hardware, you must flash the Ahoy DTU Firmware to your board. +You can either create your own firmware using your configuration or use one of our pre-compiled generic builds. + +Are you ready to flash? Then go to next Step here. ### 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. - -This code comes to you as a **PlatformIO** project and can be compiled using the **PlatformIO** Addon.
-Visual Studio Code, AtomIDE and other IDE's support the PlatformIO Addon.
-If you do not want to compile your own build, you can use one of our ready-to-flash binaries. - -##### Optional Configuration before compilation +### Compiling your own Version (expert) +This information is for those who wish to configure and build their own firmware. -- number of supported inverters (set to 3 by default) `config.h` -- DTU radio id `config.h` (default = 1234567801) -- unformatted list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED` - -Alternativly, instead of modifying `config.h`, `config_override_example.h` can be copied to `config_override.h` and customized. -config_override.h is excluded from version control and stays local. +The code is provided as a PlatformIO project and can be compiled using the PlatformIO Addon. +The PlatformIO Addon is supported by Visual Studio Code, AtomIDE, and other IDEs. +If you do not wish to compile your own build, you can use one of our pre-compiled binaries. #### Using a ready-to-flash binary using nodemcu-pyflasher @@ -217,7 +210,7 @@ This information suits you if you just want to use an easy way. Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities to update your firmware. -! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! +**! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data!** #### Flashing on Linux with `esptool.py` (ESP32) 1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already. @@ -229,51 +222,56 @@ Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities t ## Connect to your Ahoy DTU -When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU. +Once everything is wired and the firmware is flashed, it is time to connect to your Ahoy DTU. #### Your Ahoy DTU is very verbose using the Serial Console - When connected to your computer, you can open a Serial Console to obtain additional information.
- This might be useful in case of any troubles that might occur as well as to simply
- obtain information about the converted values which were read out of the inverter(s). + Once connected to your computer, you can open a serial console to get additional information. + This can be useful for troubleshooting, as well as simply to get + information about the converted values read from the inverter(s). #### Connect to the Ahoy DTU Webinterface using your Browser - After you have sucessfully flashed and powered your Ahoy DTU, you can access it via your Browser.
- If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address
- from your local DHCP Server (in most cases thats your Router).

- In case it could not connect to your configured Network, it will provide its own WiFi Network that you can
- connect to for furter configuration.
- The WiFi SSID *(the WiFi Name)* and Passwort is configured in the config.h and defaults to the SSID "`AHOY-DTU`" with the Passwort "`esp_8266`".
- The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).
- 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.4.1/](http://192.168.4.1/).
- Just open the IP-Address in your browser.
-
- The webinterface has the following abilities: - -- OTA Update (Over The Air Update) -- Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug) -- visual display of the connected inverters / modules -- some statistics about communication (debug) + After you have successfully flashed and powered up your Ahoy DTU, you can access it from your browser.
+ If your Ahoy DTU was able to log on to the configured WiFi network, it will try to obtain an IP address from your local DHCP server (in most cases this is your router). + + If it cannot connect to your configured network, it will provide its own WiFi network that you can + to for further configuration. + + The WiFi SSID *(the WiFi name)* and password are pre-configured and are set to SSID "`AHOY-DTU`" and password "`esp_8266`" by default. + + The Ahoy DTU will keep this network open for a certain amount of time (default is 60sec). + If nothing connects to it and the time expires, it will retry to connect to the configured network, and so on. + + If you are connected to your local network, just find out the IP address used 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.4.1/](http://192.168.4.1/). + Just open the IP-Address in your browser. + + The web interface has the following capabilities: + +- Live data (values updated every 5 seconds) + Click on the title/name/alarm for more actions. +- Webserial (Debug) +- Settings (System Config, Network, Protection, Inverter, NTP Server, Sunrise/Sunset, MQTT, Display Config) +- Update (Over The Air Update) +- System (status about the modules) ##### 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.4.1/setup](http://192.168.4.1/setup) ).
+ 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 | +| /logout| logout the user from webinterface | | yes | | /reboot | reboots the Ahoy DTU | | yes | +| /system| show system inforamtion | | yes | +| /live | displays the live data | | yes | +| /save | | | 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 | | /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h | | /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes | @@ -290,12 +288,14 @@ When everything is wired up and the firmware is flashed, it is time to connect t | `SPI` | 1.0 | LGPL-2.1 | | `Hash` | 1.0 | LGPL-2.1 | | `EEPROM` | 1.0 | LGPL-2.1 | -| `ESP Async WebServer` | 1.2.3 | LGPL-3.0 | -| `ESPAsyncTCP` | 1.2.2 | LGPL-3.0 | -| `Time` | 1.6.1 | LGPL-2.1 | -| `RF24` | 1.4.7 | GPL-2.0 | -| `espMqttClient` | 1.4.4 | MIT | -| `ArduinoJson` | 6.21.3 | MIT | +| `ESPAsyncWebServer` | 1.2.3 | LGPL-3.0 | +| [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) | 1.2.2 | [LGPL-3.0 license](https://github.com/me-no-dev/ESPAsyncTCP#LGPL-3.0-1-ov-file) | +| [Time](https://github.com/PaulStoffregen/Time) | 1.6.1 | ? | +| [RF24](https://github.com/nRF24/RF24) | 1.4.8 | [GPL-2.0 license](https://github.com/nRF24/RF24#GPL-2.0-1-ov-file) | +| [espMqttClient](https://github.com/bertmelis/espMqttClient) | ? | [MIT license](https://github.com/bertmelis/espMqttClient#MIT-1-ov-file) | +| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | 6.21.3 | [MIT license](https://github.com/bblanchon/ArduinoJson#MIT-1-ov-file)| +| [GxEPD2](https://github.com/ZinggJM/GxEPD2) | 1.5.2 | [GPL-3.0 license](https://github.com/ZinggJM/GxEPD2#GPL-3.0-1-ov-file)| +| [U8g2_Arduino](https://registry.platformio.org/libraries/olikraus/U8g2) | [2.35.9](https://registry.platformio.org/libraries/olikraus/U8g2/versions) | [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html) | ## ToDo diff --git a/README.md b/README.md index f774b568..cc85f1a2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This work is licensed under a # 🖐 Ahoy! ![Logo](https://github.com/grindylow/ahoy/blob/main/doc/logo1_small.png?raw=true) -This repository offers hardware and software solutions for communicating with Hoymiles inverters via radio. With our system, you can easily obtain real-time values such as power, current, and daily energy. Additionally, you can set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Whether you're monitoring your solar panel system's performance or fine-tuning its settings, our solutions make it easy to achieve your goals. +This repository provides hardware and software solutions for communicating with Hoymiles inverters via radio. Our system allows you to easily obtain real-time values, such as power, current, and daily energy, as well as set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Our solutions simplify the process of monitoring and fine-tuning your solar panel system to help you achieve your goals. Table of approaches: @@ -32,9 +32,11 @@ Table of approaches: | [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | ## Getting Started -[Guide how to start with a ESP module](Getting_Started.md) +1. [Guide how to start with a ESP module](Getting_Started.md) -[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install) +2. [ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install) + +3. [Ahoy Configuration ](ahoy_config.md) ## Our Website [https://ahoydtu.de](https://ahoydtu.de) @@ -43,11 +45,11 @@ Table of approaches: - [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 (~ 3.800 Users)](https://discord.gg/WzhxEY62mB) +- [Discord Server (~ 7.300 Users)](https://discord.gg/WzhxEY62mB) - [The root of development](https://www.mikrocontroller.net/topic/525778) ### Development -If you run into any issues, please feel free to use the issue tracker here on Github. When describing your issue, please be as detailed and precise as possible, and take a moment to consider whether the issue is related to our software. This will help us to provide more effective solutions to your problem. +If you encounter any problems, use the issue tracker on Github. Provide a detailed description of the issue and consider if it is related to our software. This will help us provide effective solutions. **Contributors are always welcome!** diff --git a/ahoy_config.md b/ahoy_config.md new file mode 100644 index 00000000..5c2321de --- /dev/null +++ b/ahoy_config.md @@ -0,0 +1,65 @@ + + +## Ahoy configuration + + So far we have built our own DTU, written a program on it and put it into operation. +But how do I get my data from the inverter? + +To do this, we need to configure the DTU. + +The following steps are required: +1. Set the pinning to communicate with the radio module. +2. Check if Ahoy has a current time +3. Set inverter data + +### 1.) Set the pinning +Once you are in the web interface, you will find the "System Config" sub-item in the Setup area (left). + +This is where you tell the ESP how you connected the radio module. +Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections. + + +#### OpenDTU Fusion (ESP32-S3) +| NRF24 Pin | ESP Pin| +|---------| --------| +| CS (4) | GPIO37 +| CE (3)| GPIO38 +| IRQ (8) | GPIO47 +| SCLK (5)| GPIO36 +| MOSI (6)| GPIO35 +| MISO (7)| GPIO48 + +| CMT2300A | Pin | +|---------| --------| +| CMT| Enabled | +| SCLK| GPIO6 +| SDIO| GPIO5 +| CSB| GPIO4 +| FCSB| GPIO21 +| GPIO3| GPIO8 + +### 2.) Set current time (normal skip this step) +Ahoy needs a current date and time to talk to the inverter. +It works without, but it is recommended to include a time. This allows you to analyze information from the inverter in more detail. +Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this. +In the section "Settings -> NTP Server" you can also get the time from your own computer. Or set up your own NTP server. + +### 3.) Set inverter data + +#### add new inverter +Now it's time to place the inverter. This is necessary because it is not the inverter that speaks first, but the DTU (Ahoy). + +Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter. + +The S.Nr is a 12-digit number. You can look it up [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. +#### set pv-modules (not necessary) +Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short! +In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter. + +#### set radio parameter (not necessary, only for EU) +In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only). + +#### advanced options (not necessary) +In the "Advanced" section, you can customize more settings. + +# Done - Now check the live site From 8ca0cc5d27db70f0b685b0907067fba466a34668 Mon Sep 17 00:00:00 2001 From: DanielR92 Date: Sat, 30 Dec 2023 10:21:00 +0100 Subject: [PATCH 002/115] move to folder --- Getting_Started.md => manual/Getting_Started.md | 0 User_Manual.md => manual/User_Manual.md | 0 ahoy_config.md => manual/ahoy_config.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Getting_Started.md => manual/Getting_Started.md (100%) rename User_Manual.md => manual/User_Manual.md (100%) rename ahoy_config.md => manual/ahoy_config.md (100%) diff --git a/Getting_Started.md b/manual/Getting_Started.md similarity index 100% rename from Getting_Started.md rename to manual/Getting_Started.md diff --git a/User_Manual.md b/manual/User_Manual.md similarity index 100% rename from User_Manual.md rename to manual/User_Manual.md diff --git a/ahoy_config.md b/manual/ahoy_config.md similarity index 100% rename from ahoy_config.md rename to manual/ahoy_config.md From a993c7a2cf7d513b36d32039012c500622f7b635 Mon Sep 17 00:00:00 2001 From: DanielR92 Date: Sat, 30 Dec 2023 10:40:26 +0100 Subject: [PATCH 003/115] Update ahoy_config.md add some pictures --- manual/ahoy_config.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manual/ahoy_config.md b/manual/ahoy_config.md index 5c2321de..73427b6f 100644 --- a/manual/ahoy_config.md +++ b/manual/ahoy_config.md @@ -54,6 +54,10 @@ Each inverter has its own S.Nr. This also serves as an identity for communicatio The S.Nr is a 12-digit number. You can look it up [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. #### set pv-modules (not necessary) Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short! +![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b52a2d5d-513c-4895-848a-01ce129f93c1) + +![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b508824f-08a7-4b9c-bc41-29dfee02dced) + In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter. #### set radio parameter (not necessary, only for EU) @@ -62,4 +66,5 @@ In the next tab "Radio" you can adjust the power and other parameters if necessa #### advanced options (not necessary) In the "Advanced" section, you can customize more settings. +Save and reboot. # Done - Now check the live site From 41485a9af3a5bb2e87fa163d46dd705e220a048c Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 31 Dec 2023 03:03:14 +0100 Subject: [PATCH 004/115] 0.8.37 * added grid profiles --- src/web/html/grid_info.json | 55 +++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index ee6dea13..5ac2e0b2 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -1,13 +1,24 @@ { "type": [ - {"0x0100": "?"}, + {"0x0100": "CN_NBT32004_2018"}, + {"0x0200": "US_Rule21_240V"}, {"0x0300": "DE_VDE4105_2018"}, {"0x0301": "DE_VDE4105_2011"}, + {"0x0604": "Germany_VDE4105"}, + {"0x0800": "IT_CEI0-21"}, + {"0x0807": "Netherland_EN50438"}, + {"0x0908": "France_VFR2014"}, {"0x0a00": "DE NF_EN_50549-1:2019"}, {"0x0c00": "AT_TOR_Erzeuger_default"}, - {"0x0d04": "France NF_EN_50549-1:2019"}, - {"0x1200": "2.0.4 (EU_EN50438)"}, - {"0x3700": "2.0.0 (CH_NA EEA-NE7–CH2020)"} + {"0x0d00": "FR_VFR2019"}, + {"0x0d04": "NF_EN_50549-1:2019"}, + {"0x1000": "ES_RD1699"}, + {"0x1200": "EU_EN50438"}, + {"0x2600": "BE_C10_26"}, + {"0x2900": "NL_NEN-EN50549-1_2019"}, + {"0x2a00": "PL_PN-EN 50549-1:2019"}, + {"0x3700": "CH_NA EEA-NE7–CH2020"} + {"0xe100": "LN_50Hz"} ], "grp_codes": [ {"0x00": "Voltage H/LVRT"}, @@ -345,39 +356,43 @@ { "name": "Low Frequency 1", "div": 100, + "min": 49.5, + "max": 49.9, + "def": 49.5, "unit": "Hz" }, { - "name": "?", - "div": 10 - }, - { - "name": "?", - "div": 100, - "unit": "Hz" - }, - { - "name": "?", + "name": "LF1 Maximum Trip Time", "div": 10, + "def": 700, "unit": "s" }, { - "name": "?", + "name": "High Frequency 1", "div": 100, + "max": 51, + "min": 50.1, + "def": 50.2, "unit": "Hz" }, { - "name": "?", + "name": "HF1 Maximum Trip time", "div": 10, + "def": 0.1, "unit": "s" }, { - "name": "?", - "div": 100 + "name": "Low Frequency 2", + "div": 100, + "max": 49, + "min": 47.5, + "def": 47.5, + "unit": "Hz" }, { - "name": "?", - "div": 10, + "name": "LF2 Maximum Trip Time", + "div": 100, + "def": 0.1, "unit": "s" } ] From 1b73e493a9608599b7fa40dda6fec1a6b2294cb8 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 31 Dec 2023 13:04:13 +0100 Subject: [PATCH 005/115] 0.8.38 * fix Grid-Profile JSON #1304 --- src/CHANGES.md | 3 +++ src/defines.h | 2 +- src/web/html/grid_info.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 32efd79b..f6e7445e 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.38 - 2023-12-31 +* fix Grid-Profile JSON #1304 + ## 0.8.37 - 2023-12-30 * added grid profiles * format version of grid profile diff --git a/src/defines.h b/src/defines.h index 787e9ccf..05cdc5e0 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 37 +#define VERSION_PATCH 38 //------------------------------------- typedef struct { diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index 5ac2e0b2..f14babe3 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -17,7 +17,7 @@ {"0x2600": "BE_C10_26"}, {"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"}, - {"0x3700": "CH_NA EEA-NE7–CH2020"} + {"0x3700": "CH_NA EEA-NE7–CH2020"}, {"0xe100": "LN_50Hz"} ], "grp_codes": [ From 2571e3c9f9b0abcb9033c183f2c3eee93d3a879a Mon Sep 17 00:00:00 2001 From: rejoe2 Date: Sun, 31 Dec 2023 14:24:23 +0100 Subject: [PATCH 006/115] MI - add grid profile request Note: this doesn't deliver entire grid profile infos... --- src/hm/Communication.h | 47 ++++++++++++++++++++++++++++-------------- src/hm/hmInverter.h | 2 ++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 9f96533d..4aa9ddcc 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -353,6 +353,12 @@ class Communication : public CommQueue<> { miDataDecode(p, q); } else if (p->packet[0] == (0x0f + ALL_FRAMES)) miHwDecode(p, q); + + else if (p->packet[0] == ( 0x10 + ALL_FRAMES)) { + // MI response from get Grid Profile information request + miGPFDecode(p, q); + } + else if ((p->packet[0] == 0x88) || (p->packet[0] == 0x92)) { record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure rec->ts = q->ts; @@ -650,22 +656,31 @@ class Communication : public CommQueue<> { (mCbPayload)(InverterDevInform_Simple, q->iv); q->iv->miMultiParts++; } - //if(q->iv->miMultiParts > 5) - //closeRequest(q->iv, true); - //else - //if(q->iv->miMultiParts < 6) - // mState = States::WAIT; - - /*if (mPayload[iv->id].multi_parts > 5) { - iv->setQueuedCmdFinished(); - mPayload[iv->id].complete = true; - mPayload[iv->id].rxTmo = true; - mPayload[iv->id].requested= false; - iv->radioStatistics.rxSuccess++; - } - if (mHighPrioIv == NULL) - mHighPrioIv = iv; - */ + } + + inline void miGPFDecode(packet_t *p, const queue_s *q) { + record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure + rec->ts = q->ts; + + q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE + q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION + +/* according to xlsx (different start byte -1!) + Polling Grid-connected Protection Parameter File Command - Receipt + byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged + byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file + byte[13] byte[14] Version indicates the version of the grid-connected protection file + byte[15] byte[16] +*/ + /*if(mSerialDebug) { + DPRINT(DBG_INFO,F("ST1 ")); + DBGPRINTLN(String(p->packet[9])); + DPRINT(DBG_INFO,F("CountryStd ")); + DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11])); + DPRINT(DBG_INFO,F("Version ")); + DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); + }*/ + q->iv->miMultiParts = 7; // indicate we are ready } inline void miDataDecode(packet_t *p, const queue_s *q) { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 776b9c6f..757562bb 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -215,6 +215,8 @@ class Inverter { record_t<> *rec = getRecordStruct(InverterDevInform_Simple); if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1 + else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile + cb(0x10, false); // legacy GPF command else cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); } From 29d2bdefab2021847678f89493bc7ca450c4392d Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 1 Jan 2024 15:20:14 +0100 Subject: [PATCH 007/115] PROMETHEUS_EP: Add NRF-radio statistics and power-limit metrics --- doc/prometheus_ep_description.md | 21 ++-- src/web/web.h | 165 ++++++++++++++++--------------- 2 files changed, 98 insertions(+), 88 deletions(-) diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 711299dd..651cd937 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -1,10 +1,10 @@ # Prometheus Endpoint Metrics available for AhoyDTU device, inverters and channels. -Prometheus metrics provided at `/metrics`. +Prometheus metrics provided at `/metrics`. ## Labels -| Label name | Description | +| Label name | Description | |:-------------|:--------------------------------------| | version | current installed version of AhoyDTU | | image | currently not used | @@ -19,11 +19,21 @@ Prometheus metrics provided at `/metrics`. |----------------------------------------------|---------|----------------------------------------------------------|--------------| | `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_freeheap` | Gauge | free heap memory of the AhoyDTU device | devicename | +| `ahoy_solar_wifi_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_inverter_power_limit_read` | Gauge | Power Limit read from inverter | inverter | +| `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter | +| `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_fail` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_fail_answer` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | 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 | @@ -46,9 +56,4 @@ Prometheus metrics provided at `/metrics`. | `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/src/web/web.h b/src/web/web.h index 1e87547b..990f1983 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -623,17 +623,45 @@ class Web { #ifdef ENABLE_PROMETHEUS_EP // Note // Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md - // TODO: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) - enum { - metricsStateStart, - metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, - metricStateRealtimeFieldId, metricStateRealtimeInverterId, + // NOTE: Grouping for fields with channels and totals is currently not working + // TODO: Handle grouping and sorting for independant from channel number + // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) + const char * metricPrefix = "ahoy_solar_"; + typedef enum { + metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterProducing=3, + metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterMaxPower=6, + metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, metricsStateInverterRxFailAnswer=9, + metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, metricsStateInverterRetransmits=12, + metricStateRealtimeFieldId=metricsStateInverterRetransmits+1, // ensure that this state follows the last per_inverter state + metricStateRealtimeInverterId, metricsStateAlarmData, + metricsStateStart, metricsStateEnd - } metricsStep; + } MetricStep_t; + MetricStep_t metricsStep; + typedef struct { + const char *type; + const char *format; + const std::function *iv)> valueFunc; + } InverterMetric_t; + InverterMetric_t inverterMetrics[13] = { + { "info", "info{name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, + { "is_enabled", "is_enabled {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, + { "is_available", "is_available {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, + { "is_producing", "is_producing {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, + { "power_limit_read", "power_limit_read {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, + { "power_limit_ack", "power_limit_ack {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, + { "max_power", "max_power {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, + { "radio_rx_success", "radio_rx_success {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, + { "radio_rx_fail", "radio_rx_fail {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, + { "radio_rx_fail_answer", "radio_rx_fail_answer {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, + { "radio_frame_cnt", "radio_frame_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, + { "radio_tx_cnt", "radio_tx_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, + { "radio_retransmits", "radio_retransmits {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} } + }; int metricsInverterId; uint8_t metricsFieldId; - bool metricDeclared; + bool metricDeclared, metricTotalDeclard; void showMetrics(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); @@ -654,79 +682,58 @@ class Web { // Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. // So several "Info:" blocks are used to keep the transmission going 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", + case metricsStateStart: // System Info : fit to one packet + snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricPrefix, 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()); + snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricPrefix,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()); + snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricPrefix); + snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricPrefix, 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()); + snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricPrefix, mConfig->sys.deviceName, WiFi.RSSI()); metrics += String(type) + String(topic); - // NRF Statistics - // @TODO 2023-10-01: the statistic data is now available per inverter - /*stat = mApp->getNrfStatistics(); - 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"), stat->txCnt); - metrics += radioStatistic(F("retrans_cnt"), stat->retransmits);*/ - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); // Next is Inverter information - metricsInverterId = 0; - metricsStep = metricsStateInverter1; + metricsStep = metricsStateInverterInfo; break; - case metricsStateInverter1: // Information about all inverters configured : fit to one packet - metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); + // Information about all inverters configured : each metric for all inverters must fit to one network packet + case metricsStateInverterInfo: + case metricsStateInverterEnabled: + case metricsStateInverterAvailable: + case metricsStateInverterProducing: + case metricsStateInverterPowerLimitRead: + case metricsStateInverterPowerLimitAck: + case metricsStateInverterMaxPower: + case metricsStateInverterRxSuccess: + case metricsStateInverterRxFail: + case metricsStateInverterRxFailAnswer: + case metricsStateInverterFrameCnt: + case metricsStateInverterTxCnt: + case metricsStateInverterRetransmits: + metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].type) + " gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].valueFunc); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter2; - break; - - case metricsStateInverter2: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;}); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter3; - break; - - case metricsStateInverter3: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable();}); - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter4; - break; - - case metricsStateInverter4: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing();}); - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Realtime Field loop + // ugly hack to increment the enum + metricsStep = static_cast( static_cast(metricsStep) + 1); + // Prepare Realtime Field loop, which may be startet next metricsFieldId = FLD_UDC; - metricsStep = metricStateRealtimeFieldId; break; + case metricStateRealtimeFieldId: // Iterate over all defined fields if (metricsFieldId < FLD_LAST_ALARM_CODE) { metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; metricDeclared = false; + metricTotalDeclard = false; metricsInverterId = 0; metricsStep = metricStateRealtimeInverterId; @@ -741,7 +748,6 @@ class Web { metrics = ""; if (metricsInverterId < mSys->getNumInverters()) { // process all channels of this inverter - iv = mSys->getInverterByPos(metricsInverterId); if (NULL != iv) { rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -755,22 +761,27 @@ class Web { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); // Declare metric only once if (channel != 0 && !metricDeclared) { - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); metrics += type; metricDeclared = true; } // report value if (0 == channel) { + // Report a _total value if also channel values were reported. Otherwise report without _total char total[7]; total[0] = 0; if (metricDeclared) { - // A declaration and value for channels has been delivered. So declare and deliver a _total metric + // A declaration and value for channels have been delivered. So declare and deliver a _total metric strncpy(total,"_total",sizeof(total)); } - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); - metrics += type; - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); + if (!metricTotalDeclard) { + snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); + metrics += type; + metricTotalDeclard = true; + } + snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); } else { + // Report (non zero) channel value // Use a fallback channel name (ch0, ch1, ...)if non is given by user char chName[MAX_NAME_LENGTH]; if (iv->config->chName[channel-1][0] != 0) { @@ -778,7 +789,7 @@ class Web { } else { snprintf(chName,sizeof(chName),"ch%1d",channel); } - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); } snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); metrics += topic; @@ -808,7 +819,7 @@ class Web { case metricsStateAlarmData: // Alarm Info loop : fit to one packet // Perform grouping on metrics according to Prometheus exposition format specification - snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); + snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricPrefix,fields[FLD_LAST_ALARM_CODE]); metrics = type; for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { @@ -820,7 +831,7 @@ class Web { alarmChannelId = 0; if (alarmChannelId < rec->length) { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); metrics += topic; metrics += val; @@ -831,11 +842,13 @@ class Web { metricsStep = metricsStateEnd; break; - case metricsStateEnd: default: // end of transmission + DBGPRINT("E: Prometheus: Bad metricsStep="); + DBGPRINTLN(String(metricsStep)); + case metricsStateEnd: len = 0; break; - } + } // switch return len; }); request->send(response); @@ -843,27 +856,19 @@ class Web { // Traverse all inverters and collect the metric via valueFunc - String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv, IApp *mApp)> valueFunc) { + String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv)> valueFunc) { Inverter<> *iv; String metric = ""; for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { iv = mSys->getInverterByPos(metricsInverterId); if (NULL != iv) { - snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp)); + snprintf(buffer,len,format,iv->config->name, valueFunc(iv)); metric += String(buffer); } } return metric; } - 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"}; From 2cda39c9f9511eb078a14aad8c096d59687e42d4 Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 1 Jan 2024 23:02:14 +0100 Subject: [PATCH 008/115] 0.8.39 * fix MqTT dis_night_comm in the morning #1309 * seperated offset for sunrise and sunset #1308 --- src/CHANGES.md | 4 ++++ src/app.cpp | 21 ++++++++++++--------- src/config/settings.h | 19 +++++++++++++------ src/defines.h | 4 ++-- src/publisher/pubMqtt.h | 10 +++++----- src/web/RestApi.h | 14 ++++++++------ src/web/html/index.html | 38 +++++++++++++++++++------------------- src/web/html/setup.html | 16 +++++++++++----- src/web/web.h | 8 +++++--- 9 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index f6e7445e..66056890 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.39 - 2024-01-01 +* fix MqTT dis_night_comm in the morning #1309 +* seperated offset for sunrise and sunset #1308 + ## 0.8.38 - 2023-12-31 * fix Grid-Profile JSON #1304 diff --git a/src/app.cpp b/src/app.cpp index 0bba14da..8e70778a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -226,15 +226,18 @@ 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 + if (mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) // 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 + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSecEvening + 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) + if (mMqttEnabled) { tickSun(); + nxtTrig = mSunrise - mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly + onceAt(std::bind(&app::tickSun, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm' + } } //----------------------------------------------------------------------------- @@ -251,14 +254,14 @@ void app::tickIVCommunication(void) { iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on if (!iv->commEnabled) { // inverter communication only during the day - if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start - nxtTrig = mSunrise - mConfig->sun.offsetSec; + if (mTimestamp < (mSunrise - mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start + nxtTrig = mSunrise - mConfig->sun.offsetSecMorning; } 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 + if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // 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 iv->commEnabled = true; - nxtTrig = mSunset + mConfig->sun.offsetSec; + nxtTrig = mSunset + mConfig->sun.offsetSecEvening; } } if (nxtTrig != 0) @@ -279,7 +282,7 @@ void app::tickIVCommunication(void) { //----------------------------------------------------------------------------- void app::tickSun(void) { // only used and enabled by MQTT (see setup()) - if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec)) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening)) once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry } diff --git a/src/config/settings.h b/src/config/settings.h index fe2ad1b0..c493eb1a 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -30,7 +30,7 @@ * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ -#define CONFIG_VERSION 7 +#define CONFIG_VERSION 8 #define PROT_MASK_INDEX 0x0001 @@ -106,7 +106,8 @@ typedef struct { typedef struct { float lat; float lon; - uint16_t offsetSec; + uint16_t offsetSecMorning; + uint16_t offsetSecEvening; } cfgSun_t; typedef struct { @@ -420,7 +421,8 @@ class settings { mCfg.sun.lat = 0.0; mCfg.sun.lon = 0.0; - mCfg.sun.offsetSec = 0; + mCfg.sun.offsetSecMorning = 0; + mCfg.sun.offsetSecEvening = 0; mCfg.serial.showIv = false; mCfg.serial.debug = false; @@ -496,6 +498,9 @@ class settings { if(mCfg.configVersion < 7) { mCfg.led.luminance = 255; } + if(mCfg.configVersion < 8) { + mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning; + } } } @@ -625,11 +630,13 @@ class settings { if(set) { obj[F("lat")] = mCfg.sun.lat; obj[F("lon")] = mCfg.sun.lon; - obj[F("offs")] = mCfg.sun.offsetSec; + obj[F("offs")] = mCfg.sun.offsetSecMorning; + obj[F("offsEve")] = mCfg.sun.offsetSecEvening; } else { getVal(obj, F("lat"), &mCfg.sun.lat); getVal(obj, F("lon"), &mCfg.sun.lon); - getVal(obj, F("offs"), &mCfg.sun.offsetSec); + getVal(obj, F("offs"), &mCfg.sun.offsetSecMorning); + getVal(obj, F("offsEve"), &mCfg.sun.offsetSecEvening); } } diff --git a/src/defines.h b/src/defines.h index 05cdc5e0..ad321921 100644 --- a/src/defines.h +++ b/src/defines.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 38 +#define VERSION_PATCH 39 //------------------------------------- typedef struct { diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index e062439c..9834f29a 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -134,14 +134,14 @@ class PubMqtt { #endif } - bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) { + bool tickerSun(uint32_t sunrise, uint32_t sunset, uint16_t offsM, uint16_t offsE) { 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_COMM_START], String(sunrise - offsM).c_str(), true); + publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true); Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { @@ -155,7 +155,7 @@ class PubMqtt { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); - publish(mSubTopic, (((*mUtcTimestamp > (sunset + offs)) || (*mUtcTimestamp < (sunrise - offs))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise - offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); return true; } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index a74f4f14..c920a8e3 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -600,7 +600,8 @@ class RestApi { 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("offs")] = mConfig->sun.offsetSec; + obj[F("offsSr")] = mConfig->sun.offsetSecMorning; + obj[F("offsSs")] = mConfig->sun.offsetSecEvening; } void getPinout(JsonObject obj) { @@ -685,10 +686,11 @@ class RestApi { void getIndex(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, 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("ts_now")] = mApp->getTimestamp(); + obj[F("ts_sunrise")] = mApp->getSunrise(); + obj[F("ts_sunset")] = mApp->getSunset(); + obj[F("ts_offsSr")] = mConfig->sun.offsetSecMorning; + obj[F("ts_offsSs")] = mConfig->sun.offsetSecEvening; JsonArray inv = obj.createNestedArray(F("inverter")); Inverter<> *iv; diff --git a/src/web/html/index.html b/src/web/html/index.html index baa70742..3ac72e89 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -45,12 +45,12 @@ function apiCb(obj) { var e = document.getElementById("apiResult"); - if(obj["success"]) { + if(obj.success) { e.innerHTML = " command executed"; getAjax("/api/index", parse); } else - e.innerHTML = " Error: " + obj["error"]; + e.innerHTML = " Error: " + obj.error; } function setTime() { @@ -68,9 +68,9 @@ } function parseSys(obj) { - ts = obj["ts_now"]; - var date = new Date(obj["ts_now"] * 1000); - var up = obj["generic"]["ts_uptime"]; + ts = obj.ts_now; + var date = new Date(obj.ts_now * 1000); + var up = obj.generic["ts_uptime"]; var days = parseInt(up / 86400) % 365; var hrs = parseInt(up / 3600) % 24; var min = parseInt(up / 60) % 60; @@ -83,8 +83,8 @@ + ("0"+min).substr(-2) + ":" + ("0"+sec).substr(-2); var dSpan = document.getElementById("date"); - if(0 != obj["ts_now"]) { - if(obj["ts_now"] < 1680000000) + if(0 != obj.ts_now) { + if(obj.ts_now < 1680000000) setTime(); else dSpan.innerHTML = toIsoDateStr(date); @@ -98,18 +98,18 @@ e.addEventListener("click", setTime); } - if(obj["disNightComm"]) { - if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) - && ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { - commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + if(obj.disNightComm) { + if(((obj.ts_sunrise - obj.ts_offsSr) < obj.ts_now) + && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) { + commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { commInfo = "Night time, inverter polling disabled, "; - if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { - commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + if(obj.ts_now > (obj.ts_sunrise - obj.ts_offsSr)) { + commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + commInfo += "will start polling at " + (new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')); } } } @@ -190,11 +190,11 @@ function parse(obj) { if(null != obj) { if(exeOnce) - parseNav(obj["generic"]); - parseGeneric(obj["generic"]); + parseNav(obj.generic); + parseGeneric(obj.generic); parseSys(obj); - parseIv(obj["inverter"], obj.ts_now); - parseWarn(obj["warnings"]); + parseIv(obj.inverter, obj.ts_now); + parseWarn(obj.warnings); if(exeOnce) { window.setInterval("tick()", 1000); exeOnce = false; @@ -210,7 +210,7 @@ } function parseRelease(obj) { - release = obj["name"].substring(6); + release = obj.name.substring(6); getAjax("/api/index", parse); } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index f172f925..aef43dcf 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -227,8 +227,12 @@
-
Offset (pre sunrise, post sunset)
-
+
Offset (sunrise)
+
+
+
+
Offset (sunset)
+
@@ -889,9 +893,11 @@ function parseSun(obj) { document.getElementsByName("sunLat")[0].value = obj["lat"]; document.getElementsByName("sunLon")[0].value = obj["lon"]; - const sel = document.getElementsByName("sunOffs")[0]; - for(var i = 0; i <= 60; i++) { - sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); + for(p of [["sunOffsSr", "offsSr"], ["sunOffsSs", "offsSs"]]) { + const sel = document.getElementsByName(p[0])[0]; + for(var i = 0; i <= 60; i++) { + sel.appendChild(opt(i, i + " minutes", (i == (obj[p[1]] / 60)))); + } } } diff --git a/src/web/web.h b/src/web/web.h index 1e87547b..d10fa62d 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -541,11 +541,13 @@ class Web { if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { mConfig->sun.lat = 0.0; mConfig->sun.lon = 0.0; - mConfig->sun.offsetSec = 0; + mConfig->sun.offsetSecMorning = 0; + mConfig->sun.offsetSecEvening = 0; } else { mConfig->sun.lat = request->arg("sunLat").toFloat(); mConfig->sun.lon = request->arg("sunLon").toFloat(); - mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; + mConfig->sun.offsetSecMorning = request->arg("sunOffsSr").toInt() * 60; + mConfig->sun.offsetSecEvening = request->arg("sunOffsSs").toInt() * 60; } // mqtt From ea29e49c93cdb1fa017886b441ce0a23d9a73a4e Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 00:21:06 +0100 Subject: [PATCH 009/115] 0.8.39 * fix MqTT dis_night_comm in the morning #1309 #1286 * seperated offset for sunrise and sunset #1308 * **BREAKING CHANGE**: powerlimit (active power control) now has one decimal place (MqTT / API) #1199 --- src/CHANGES.md | 3 ++- src/hm/Communication.h | 4 ++-- src/hm/hmDefines.h | 4 ++-- src/hm/hmInverter.h | 8 ++++---- src/hm/hmRadio.h | 10 +++++----- src/hms/hmsRadio.h | 10 +++++----- src/web/html/visualization.html | 4 ++-- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 66056890..67e94621 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,8 +1,9 @@ # Development Changes ## 0.8.39 - 2024-01-01 -* fix MqTT dis_night_comm in the morning #1309 +* fix MqTT dis_night_comm in the morning #1309 #1286 * seperated offset for sunrise and sunset #1308 +* **BREAKING CHANGE**: powerlimit (active power control) now has one decimal place (MqTT / API) #1199 ## 0.8.38 - 2023-12-31 * fix Grid-Profile JSON #1304 diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 9f96533d..79f5d34a 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -392,7 +392,7 @@ class Communication : public CommQueue<> { DBGPRINT(F("has ")); if(!accepted) DBGPRINT(F("not ")); DBGPRINT(F("accepted power limit set point ")); - DBGPRINT(String(q->iv->powerLimit[0])); + DBGPRINT(String(q->iv->powerLimit[0]/10)); DBGPRINT(F(" with PowerLimitControl ")); DBGPRINTLN(String(q->iv->powerLimit[1])); q->iv->actPowerLimit = 0xffff; // unknown, readback current value diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 55259289..a2a2d6b4 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __HM_DEFINES_H__ diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 776b9c6f..fa8e1b87 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __HM_INVERTER_H__ @@ -110,7 +110,7 @@ class Inverter { 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 + uint16_t powerLimit[2]; // limit power output (multiplied by 10) float actPowerLimit; // actual power limit bool powerLimitAck; // acknowledged power limit (default: false) uint8_t devControlCmd; // carries the requested cmd @@ -152,7 +152,7 @@ class Inverter { Inverter() { ivGen = IV_HM; - powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited + powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited powerLimit[1] = AbsolutNonPersistent; // default power limit setting powerLimitAck = false; actPowerLimit = 0xffff; // init feedback from inverter to -1 diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 0754f83c..6539dd21 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -177,10 +177,10 @@ class HmRadio : public Radio { 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 + mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction) + mTxBuf[cnt++] = (data[0] ) & 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 uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index d2779012..6b502816 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -50,10 +50,10 @@ class CmtRadio : public Radio { 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 + mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction) + mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit + mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling } sendPacket(iv, cnt, isRetransmit); diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 9567504c..d8896e14 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -399,7 +399,7 @@ var html = ml("div", {}, [ ml("div", {class: "row mb-3"}, [ ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"), - ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")), + ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number", step: "0.1", min: 1}, "")), ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct")) ]), ml("div", {class: "row mb-3"}, [ @@ -450,7 +450,7 @@ var obj = new Object(); obj.id = id; obj.cmd = cmd; - obj.val = val; + obj.val = Math.round(val*10); getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); } From 54b9e2f3ea6532df551edbf92c68e78cd40fb2c5 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 00:29:36 +0100 Subject: [PATCH 010/115] 0.8.39 * merge Prometheus metrics fix #1310 * merge MI grid profile request #1306 --- src/hm/Communication.h | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 7802bf32..53106524 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -351,10 +351,9 @@ class Communication : public CommQueue<> { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 //mPayload[iv->id].txId = p->packet[0]; miDataDecode(p, q); - } else if (p->packet[0] == (0x0f + ALL_FRAMES)) + } else if (p->packet[0] == (0x0f + ALL_FRAMES)) { miHwDecode(p, q); - - else if (p->packet[0] == ( 0x10 + ALL_FRAMES)) { + } else if (p->packet[0] == ( 0x10 + ALL_FRAMES)) { // MI response from get Grid Profile information request miGPFDecode(p, q); } @@ -665,13 +664,13 @@ class Communication : public CommQueue<> { q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION -/* according to xlsx (different start byte -1!) - Polling Grid-connected Protection Parameter File Command - Receipt - byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged - byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file - byte[13] byte[14] Version indicates the version of the grid-connected protection file - byte[15] byte[16] -*/ + /* according to xlsx (different start byte -1!) + Polling Grid-connected Protection Parameter File Command - Receipt + byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged + byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file + byte[13] byte[14] Version indicates the version of the grid-connected protection file + byte[15] byte[16] + */ /*if(mSerialDebug) { DPRINT(DBG_INFO,F("ST1 ")); DBGPRINTLN(String(p->packet[9])); From 27ad75b7d39fcc38d21f28885783fa4efed1c49d Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 00:45:42 +0100 Subject: [PATCH 011/115] 0.8.39 * merge update documentation / readme #1305 --- doc/screenshots/inverterSettings.png | Bin 0 -> 22006 bytes doc/screenshots/settings.png | Bin 0 -> 95179 bytes manual/Getting_Started.md | 3 +-- manual/User_Manual.md | 19 ++++++++++--------- manual/ahoy_config.md | 27 ++++++++++++++------------- src/CHANGES.md | 3 +++ 6 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 doc/screenshots/inverterSettings.png create mode 100644 doc/screenshots/settings.png diff --git a/doc/screenshots/inverterSettings.png b/doc/screenshots/inverterSettings.png new file mode 100644 index 0000000000000000000000000000000000000000..ef9484c166d00ef7df2bcee0562e093902dedac9 GIT binary patch literal 22006 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

|Rb3|~#t-{|@of>C% z1#%}_=zm4Q4SQFN`>qig__WJ1!2k9}?N{v+xz{BO7gd|1mQQ30|RRy;(SUpT_cCQa|?HVnRt4m!t%< z)g}zx;D{E1Y}S{Q7Ei!R#cvDN0XOWWRZts|=pbo6#tPKvmEQIH8i$S1!?vc;=Ow3` zONK)*TEEu$NWI&bjkK0~bmX)!gBq>fV9nHRrW2i@IahzU6@RWcBr@?g3B1;daAaYO z$iZI3`1oy;w?V_{v3$_zK+PdLO^!6TypMcwJiDL!q)vH9q%HkSLmysr@rd5MNjd~% z-LXQ`nJ)eP4vOu0r_FTQ<9_%3oK$!@{8n=O_?WuFWcy6~JLdz&ub!OU8g0?8 z1W1;Hkk0I@xZ`D~WPC$}`tAnpEqF1y2RW6=Z;yN$+E~KJ2}b=EV{wt;m6*L%g9PIN zeF!90H@~)u@}SFFO<7|QonP<|6m?3t?ve=VU&Vi=0_{j%!X4 zTjW^v66@daj@scaD>3z1&I$sxTWEn-*t&>9L>4nPRBX1bN?}ZP;2hN<$GSbmZsp2_ z!PUX|nh~m4RAlKyf|S)$!CeP0C9fL0MSZ4rQm|hHYOxg4A+1E>q!jt%vKkil@oHtf z%_CIP&}a+k9Aj0e;fhaC^=&eM&@Hj0Qnp$;C=zprlqeS6mw9UK@nz^zQ3fnu3rXLA z$Bz~ixeWD2lgZtZYOS{QAIdD}BP%gaQq984zb(pajcrvP$x=C%zQ;_vJv>#Q%XK`n zs`5lBHbjFgycJrDNu@#zy9(nw5t+>AF%Ulfr@zAA=bUrvW*jdPD6wkBS>u|;#3k8t zU{50r0%&%ZGs3Yg8%M4{JszZb7I;pbygB#P4WH($KzPgai*Z~CLuSS_%hXv_#t1Yy zVMUy=>Irw#iQKOE$|Vc=nVL^=lCmhKU6!k&KSRR*Xp>Ge940iK(Q?aMp&2*W?m?86 zcs>AlGg~{y5HPwCv1|#AdH|h=jH_N&M1MIO(W0gWHDq;{_gtL1RH^a7h16+o#5FnL zz`OwWGz{f`cy=Hve_cb@Rd$a`TU4g+7uVGT#Qll&ua}%&>_Yi&l~>B1#fkMpV#CQO zqTD`0=;;Ap%Js9QLI;Mva8s+D2pgd%w2+j%8thx=T(GMS$qjsl{?Uk05 zepDMh)r!jPO=uNb)tjGr!VE6fdnok>X^Y^}*W2OIsAs)^iY6-)207fCux-^aHBs0t zi#s=wbmgm&wAvACdRVfZ;L^uy0uO$?xD~~EkVVDxtnI06E4*zVm9Xg$TBN4`ebF6hK8`oQaYdg_=#B`#P@R&P%#g9hBx}a6 zm~cvuf7Ym9;H}6B0bnET-2j5Iuw21QZD*S& zQX=_*ZtjdZwk{~{_r&gguSFC&_3K!u_M1p~0iGQ>6u zYQwv>zAnCP4*E59?E5Qy5j(*-1De&j!4EK#o#}{|e$%Rk;fK$BtPPg=HZ$XL@uG}? zP2X(N=aGF70V=J8)H7aI5)>q8qx=|c{=4!OTH(njs&hc@sy9hK94DpwL!Pd@UG=C# nl#|+Q9t5f6-sUU5AaSacDnS7}SFV%zQ-($QE;s)Kt^qrq literal 0 HcmV?d00001 diff --git a/doc/screenshots/settings.png b/doc/screenshots/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..179a5d077e428858f2fb1bd6f542acf8ea461ce4 GIT binary patch literal 95179 zcmb@uWmKHO)-?zOcWIz;YuqKcd*f~i4ha(69fG@Sa3{DE+${<2E+J^J5L~8t-y3r0 zoB1_s{;*j6R9Dq=&Z(-i_u19qDoQeF$b`sHP*7-KSxGf0s8?VpC>R4oc*rNlt4i}w zP~=cxNihv~gQHah9e~U>{OJ|%F?vi)z6rjU_E86IZ9gGS^lB;nWmv~m75f}lv`|Xv zHVRK;llkVu%%}$Oar91KIF-}hR+VRnm-DURBe0o;A@r)%-iz+z} zqQwR9j{$wffdLE8Oo~e(6dau>G_s063}Cnjh9ji#?ULh7?Gh^Zrgqk29FY5;$K6LT zsNi?ByKw6|{uKMs`|gx4U&|H!2?tEzup6Lq((|XFFL~oXU-HuFjX2~ZLoO%MFW`_u z!Kt`*wEr3Exm%+}NS7(A!WFCKkbk^M|4sw)>>GJ^oEf~C0p%xHU_u>I!oc(N{xg;P zL?{XDoY)nr?zkkcrHI!>FJpayOpP9&$Dp&EUqY?Z+HsBTWsbv0&>@AHD!Z@O6(H09 zzT%Ge@+h~v3)@KcR{O0WuMAurJ7}Q4f8%U>Xky=)&r`mg_g_1Ib0l5?fk29px}~7T z8`^*m$l%S*&C|46B`JPih8j`TgH|Fz`*PIcIfLsCYD6Lh9KECsU&NI)kQ41-4_ z%(dU_{j&E{SM$XZqR>u=vQN2>uAT|l4gO⋙8kh@~5>(X;d4I@O7L1M{U>l`|s2x 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 should be set to `1` = `ON` and `0` = `OFF` } ``` +**beginning from verson `0.8.39` the wattage and percentage has one decimal place!** -### Power Limit relative persistent [%] +### Power Limit (active power control) relative persistent [%] ```json { @@ -205,10 +206,10 @@ The `` should be set to `1` = `ON` and `0` = `OFF` "val": } ``` -The `VALUE` represents a percent number in a range of `[2 .. 100]` +The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]` -### Power Limit absolute persistent [Watts] +### Power Limit (active power control) absolute persistent [Watts] ```json { @@ -217,10 +218,10 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]` "val": } ``` -The `VALUE` represents watts in a range of `[0 .. 65535]` +The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` -### Power Limit relative non persistent [%] +### Power Limit (active power control) relative non persistent [%] ```json { @@ -229,10 +230,10 @@ The `VALUE` represents watts in a range of `[0 .. 65535]` "val": } ``` -The `VALUE` represents a percent number in a range of `[2 .. 100]` +The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]` -### Power Limit absolute non persistent [Watts] +### Power Limit (active power control) absolute non persistent [Watts] ```json { @@ -241,7 +242,7 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]` "val": } ``` -The `VALUE` represents watts in a range of `[0 .. 65535]` +The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` @@ -328,7 +329,7 @@ Send Power Limit: - 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. ### Update your AHOY-DTU Firmware To update your AHOY-DTU, you have to download the latest firmware package. -Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download. +Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://fw.ahoydtu.de/dev) available for download. As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file. From the unzipped files, select the right .bin file for your hardware and needs. - If you use an ESP8266, select the file ending with esp8266.bin diff --git a/manual/ahoy_config.md b/manual/ahoy_config.md index 73427b6f..1971848f 100644 --- a/manual/ahoy_config.md +++ b/manual/ahoy_config.md @@ -1,19 +1,18 @@ - +# Ahoy configuration -## Ahoy configuration +## Prerequists +You have build your own hardware (or purchased one). The firmware is already loaded on the ESP and the WebUI is accessible from your browser. - So far we have built our own DTU, written a program on it and put it into operation. +## Start But how do I get my data from the inverter? -To do this, we need to configure the DTU. - The following steps are required: 1. Set the pinning to communicate with the radio module. 2. Check if Ahoy has a current time -3. Set inverter data +3. Configure the inverter data (e.g. serialnumber) ### 1.) Set the pinning -Once you are in the web interface, you will find the "System Config" sub-item in the Setup area (left). +Once you are in the web interface, you will find the "System Config" sub-item in the Setup area. This is where you tell the ESP how you connected the radio module. Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections. @@ -38,7 +37,7 @@ Note the schematics you saw earlier. - If you haven't noticed them yet, here's a | FCSB| GPIO21 | GPIO3| GPIO8 -### 2.) Set current time (normal skip this step) +### 2.) Set current time (standard: skip this step) Ahoy needs a current date and time to talk to the inverter. It works without, but it is recommended to include a time. This allows you to analyze information from the inverter in more detail. Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this. @@ -51,20 +50,22 @@ Now it's time to place the inverter. This is necessary because it is not the inv Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter. -The S.Nr is a 12-digit number. You can look it up [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. +The S.Nr is a 12-digit number. Check [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. + #### set pv-modules (not necessary) Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short! -![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b52a2d5d-513c-4895-848a-01ce129f93c1) +![grafik](https://github.com/lumapu/ahoy/doc/screenshots/settings.png) -![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b508824f-08a7-4b9c-bc41-29dfee02dced) +![grafik](https://github.com/lumapu/ahoy/doc/screenshots/inverterSettings.png) In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter. #### set radio parameter (not necessary, only for EU) In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only). -#### advanced options (not necessary) +#### advanced options (not necessary to be changed) In the "Advanced" section, you can customize more settings. Save and reboot. -# Done - Now check the live site + +## ✅ Done - Now check the live site diff --git a/src/CHANGES.md b/src/CHANGES.md index 67e94621..a29901ac 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -4,6 +4,9 @@ * fix MqTT dis_night_comm in the morning #1309 #1286 * seperated offset for sunrise and sunset #1308 * **BREAKING CHANGE**: powerlimit (active power control) now has one decimal place (MqTT / API) #1199 +* merge Prometheus metrics fix #1310 +* merge MI grid profile request #1306 +* merge update documentation / readme #1305 ## 0.8.38 - 2023-12-31 * fix Grid-Profile JSON #1304 From 40097aba1807241e910eabb4ee4fca0552a909b8 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 01:03:40 +0100 Subject: [PATCH 012/115] 0.8.39 * add `getLossRate` to radio statistics and to MqTT #1199 --- src/CHANGES.md | 1 + src/defines.h | 4 ++++ src/hm/Communication.h | 2 +- src/hm/hmInverter.h | 35 +++++++++++++++-------------------- src/hm/hmRadio.h | 2 +- src/hms/hmsRadio.h | 2 +- src/publisher/pubMqttIvData.h | 10 +++++++--- 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index a29901ac..c83130c8 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -7,6 +7,7 @@ * merge Prometheus metrics fix #1310 * merge MI grid profile request #1306 * merge update documentation / readme #1305 +* add `getLossRate` to radio statistics and to MqTT #1199 ## 0.8.38 - 2023-12-31 * fix Grid-Profile JSON #1304 diff --git a/src/defines.h b/src/defines.h index ad321921..d5aa04c2 100644 --- a/src/defines.h +++ b/src/defines.h @@ -103,6 +103,10 @@ typedef struct { uint32_t frmCnt; uint32_t txCnt; uint32_t retransmits; + uint16_t ivRxCnt; // last iv rx frames (from GetLossRate) + uint16_t ivTxCnt; // last iv tx frames (from GetLossRate) + uint16_t dtuRxCnt; // current DTU rx frames (since last GetLossRate) + uint16_t dtuTxCnt; // current DTU tx frames (since last GetLossRate) } statistics_t; #endif /*__DEFINES_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 53106524..a448884e 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -151,7 +151,7 @@ class Communication : public CommQueue<> { if(validateIvSerial(&p->packet[1], q->iv)) { q->iv->radioStatistics.frmCnt++; - q->iv->mDtuRxCnt++; + q->iv->radioStatistics.dtuRxCnt++; if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if(parseFrame(p)) diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 91b8480e..37a469a7 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -141,12 +141,6 @@ class Inverter { uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime bool commEnabled; // 'pause night communication' sets this field to false - uint16_t mIvRxCnt; // last iv rx frames (from GetLossRate) - uint16_t mIvTxCnt; // last iv tx frames (from GetLossRate) - uint16_t mDtuRxCnt; // cur dtu rx frames (since last GetLossRate) - uint16_t mDtuTxCnt; // cur dtu tx frames (since last getLoassRate) - uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debu - static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup @@ -171,10 +165,6 @@ class Inverter { mIsSingleframeReq = false; radio = NULL; commEnabled = true; - mIvRxCnt = 0; - mIvTxCnt = 0; - mDtuRxCnt = 0; - mDtuTxCnt = 0; memset(&radioStatistics, 0, sizeof(statistics_t)); memset(heuristics.txRfQuality, -6, 5); @@ -605,21 +595,25 @@ class Inverter { uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t txCnt = (pyld[2] << 8) + pyld[3]; - if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past + if (radioStatistics.ivRxCnt || radioStatistics.ivTxCnt) { // there was successful GetLossRate in the past DPRINT_IVID(DBG_INFO, id); - DBGPRINTLN("Inv loss: " + - String (mDtuTxCnt - (rxCnt - mIvRxCnt)) + " of " + - String (mDtuTxCnt) + ", DTU loss: " + - String (txCnt - mIvTxCnt - mDtuRxCnt) + " of " + - String (txCnt - mIvTxCnt)); + DBGPRINT(F("Inv loss: ")); + DBGPRINT(String (radioStatistics.dtuTxCnt - (rxCnt - radioStatistics.ivRxCnt))); + DBGPRINT(F(" of ")); + DBGPRINT(String (radioStatistics.dtuTxCnt)); + DBGPRINT(F(", DTU loss: ")); + DBGPRINT(String (txCnt - radioStatistics.ivTxCnt - radioStatistics.dtuRxCnt)); + DBGPRINT(F(" of ")); + DBGPRINTLN(String (txCnt - radioStatistics.ivTxCnt)); } - mIvRxCnt = rxCnt; - mIvTxCnt = txCnt; - mDtuRxCnt = 0; // start new interval - mDtuTxCnt = 0; // start new interval + radioStatistics.ivRxCnt = rxCnt; + radioStatistics.ivTxCnt = txCnt; + radioStatistics.dtuRxCnt = 0; // start new interval + radioStatistics.dtuTxCnt = 0; // start new interval return true; } + return false; } @@ -811,6 +805,7 @@ class Inverter { bool mDevControlRequest; // true if change needed uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; + uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug }; template diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 6539dd21..c7b9581c 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -339,7 +339,7 @@ class HmRadio : public Radio { mMillis = millis(); mLastIv = iv; - iv->mDtuTxCnt++; + iv->radioStatistics.dtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index 6b502816..3b3893ac 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -112,7 +112,7 @@ class CmtRadio : public Radio { if(CMT_ERR_RX_IN_FIFO == status) mIrqRcvd = true; } - iv->mDtuTxCnt++; + iv->radioStatistics.dtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 9a364ec9..3a38a5ed 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -195,12 +195,16 @@ class PubMqttIvData { inline void sendRadioStat(uint8_t start) { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name); - snprintf(mVal, 100, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d}", + snprintf(mVal, 140, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d,\"lossIvRx\":%d,\"lossIvTx\":%d,\"lossDtuRx\":%d,\"lossDtuTx\":%d}", mIv->radioStatistics.txCnt, mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFailNoAnser, - mIv->radioStatistics.retransmits); + mIv->radioStatistics.retransmits, + mIv->radioStatistics.ivRxCnt, + mIv->radioStatistics.ivTxCnt, + mIv->radioStatistics.dtuRxCnt, + mIv->radioStatistics.dtuTxCnt); mPublish(mSubTopic, mVal, false, QOS_0); } @@ -263,7 +267,7 @@ class PubMqttIvData { bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; - char mVal[100]; + char mVal[140]; bool mZeroValues; // makes sure that yield day is sent even if no inverter is online std::queue *mSendList; From 3dd4997094ef4698bc60ff31d0dce6c97c39f0b4 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 14:43:33 +0100 Subject: [PATCH 013/115] 0.8.40 * fix display of sunrise and sunset in `/system` #1308 * fix MqTT set power limit #1313 --- src/CHANGES.md | 6 +++++- src/defines.h | 2 +- src/hm/Communication.h | 2 +- src/hm/hmInverter.h | 2 +- src/publisher/pubMqtt.h | 12 +++++++----- src/web/html/system.html | 4 ++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index c83130c8..bbede74c 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,9 +1,13 @@ # Development Changes +## 0.8.40 - 2024-01-02 +* fix display of sunrise and sunset in `/system` #1308 +* fix MqTT set power limit #1313 + ## 0.8.39 - 2024-01-01 * fix MqTT dis_night_comm in the morning #1309 #1286 * seperated offset for sunrise and sunset #1308 -* **BREAKING CHANGE**: powerlimit (active power control) now has one decimal place (MqTT / API) #1199 +* powerlimit (active power control) now has one decimal place (MqTT / API) #1199 * merge Prometheus metrics fix #1310 * merge MI grid profile request #1306 * merge update documentation / readme #1305 diff --git a/src/defines.h b/src/defines.h index d5aa04c2..26ddf237 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 39 +#define VERSION_PATCH 40 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index a448884e..d44e860f 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -397,7 +397,7 @@ class Communication : public CommQueue<> { DBGPRINT(F("has ")); if(!accepted) DBGPRINT(F("not ")); DBGPRINT(F("accepted power limit set point ")); - DBGPRINT(String(q->iv->powerLimit[0]/10)); + DBGPRINT(String((float)q->iv->powerLimit[0]/10.0)); DBGPRINT(F(" with PowerLimitControl ")); DBGPRINTLN(String(q->iv->powerLimit[1])); q->iv->actPowerLimit = 0xffff; // unknown, readback current value diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 37a469a7..25f575e5 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -805,7 +805,7 @@ class Inverter { bool mDevControlRequest; // true if change needed uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; - uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug + uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug }; template diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 9834f29a..76ec452f 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -310,15 +310,18 @@ class PubMqtt { char *pyld = new char[len + 1]; strncpy(pyld, (const char*)payload, len); pyld[len] = '\0'; - root[F("val")] = atoi(pyld); + if(NULL == strstr(topic, "limit")) + root[F("val")] = atoi(pyld); + else + root[F("val")] = (int)(atof(pyld) * 10.0f); + if(pyld[len-1] == 'W') limitAbs = true; delete[] pyld; } const char *p = topic + strlen(mCfgMqtt->topic); - uint8_t pos = 0; - uint8_t elm = 0; + uint8_t pos = 0, elm = 0; char tmp[30]; while(1) { @@ -333,8 +336,7 @@ class PubMqtt { root[F("cmd")] = F("limit_nonpersistent_absolute"); else root[F("cmd")] = F("limit_nonpersistent_relative"); - } - else + } else root[F("cmd")] = String(tmp); break; case 3: root[F("id")] = atoi(tmp); break; diff --git a/src/web/html/system.html b/src/web/html/system.html index eb25d5fa..0415864b 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -108,8 +108,8 @@ ml("tbody", {}, [ tr("Sunrise", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), tr("Sunset", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), - tr("Communication start", new Date((obj.ts_sunrise - obj.ts_offset) * 1000).toLocaleString('de-DE')), - tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offset) * 1000).toLocaleString('de-DE')), + tr("Communication start", new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')), + tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), tr("Night behaviour", badge(obj.disNightComm, ((obj.disNightComm) ? "not" : "") + " communicating", "warning")) ]) ) From 938c42925f8022bfc13441b6f9b4cf98c2251356 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 2 Jan 2024 14:58:19 +0100 Subject: [PATCH 014/115] PROMETHEUS_EP: add `LossRate` radio statistics to metric bugfix: use specific metric types for each inverter metric --- doc/prometheus_ep_description.md | 6 ++- src/web/web.h | 93 +++++++++++++++++++------------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 651cd937..6b4ce56c 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -25,7 +25,7 @@ Prometheus metrics provided at `/metrics`. | `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_inverter_power_limit_read` | Gauge | Power Limit read from inverter | inverter | +| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter. Defaults to 65535 | inverter | | `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter | | `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter | | `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter | @@ -34,6 +34,10 @@ Prometheus metrics provided at `/metrics`. | `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | 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 | diff --git a/src/web/web.h b/src/web/web.h index 3ed0353d..b76dbf70 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -628,13 +628,16 @@ class Web { // NOTE: Grouping for fields with channels and totals is currently not working // TODO: Handle grouping and sorting for independant from channel number // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) - const char * metricPrefix = "ahoy_solar_"; + const char * metricConstPrefix = "ahoy_solar_"; + const char * metricConstInverterFormat = " {inverter=\"%s\"} %d\n"; typedef enum { - metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterProducing=3, - metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterMaxPower=6, - metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, metricsStateInverterRxFailAnswer=9, - metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, metricsStateInverterRetransmits=12, - metricStateRealtimeFieldId=metricsStateInverterRetransmits+1, // ensure that this state follows the last per_inverter state + metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, + metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, + metricsStateInverterMaxPower=6, metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, + metricsStateInverterRxFailAnswer=9, metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, + metricsStateInverterRetransmits=12, metricsStateInverterIvRxCnt=13, metricsStateInverterIvTxCnt=14, + metricsStateInverterDtuRxCnt=15, metricsStateInverterDtuTxCnt=16, + metricStateRealtimeFieldId=metricsStateInverterDtuTxCnt+1, // ensure that this state follows the last per_inverter state metricStateRealtimeInverterId, metricsStateAlarmData, metricsStateStart, @@ -642,24 +645,29 @@ class Web { } MetricStep_t; MetricStep_t metricsStep; typedef struct { + const char *topic; const char *type; const char *format; const std::function *iv)> valueFunc; } InverterMetric_t; - InverterMetric_t inverterMetrics[13] = { - { "info", "info{name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, - { "is_enabled", "is_enabled {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, - { "is_available", "is_available {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, - { "is_producing", "is_producing {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, - { "power_limit_read", "power_limit_read {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, - { "power_limit_ack", "power_limit_ack {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, - { "max_power", "max_power {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, - { "radio_rx_success", "radio_rx_success {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, - { "radio_rx_fail", "radio_rx_fail {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, - { "radio_rx_fail_answer", "radio_rx_fail_answer {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, - { "radio_frame_cnt", "radio_frame_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, - { "radio_tx_cnt", "radio_tx_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, - { "radio_retransmits", "radio_retransmits {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} } + InverterMetric_t inverterMetrics[17] = { + { "info", "gauge", " {name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, + { "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, + { "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, + { "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, + { "power_limit_read", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, + { "power_limit_ack", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, + { "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, + { "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, + { "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, + { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, + { "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, + { "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, + { "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }, + { "radio_iv_rx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivRxCnt;} }, + { "radio_iv_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivTxCnt;} }, + { "radio_dtu_rx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuRxCnt;} }, + { "radio_dtu_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuTxCnt;} } }; int metricsInverterId; uint8_t metricsFieldId; @@ -685,21 +693,21 @@ class Web { // So several "Info:" blocks are used to keep the transmission going switch (metricsStep) { case metricsStateStart: // System Info : fit to one packet - snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricPrefix, + snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricConstPrefix, mApp->getVersion(), mConfig->sys.deviceName); metrics = String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricPrefix,mConfig->sys.deviceName,ESP.getFreeHeap()); + snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricConstPrefix,mConfig->sys.deviceName,ESP.getFreeHeap()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricPrefix); - snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricPrefix, mConfig->sys.deviceName, mApp->getUptime()); + snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricConstPrefix, mConfig->sys.deviceName, mApp->getUptime()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricPrefix, mConfig->sys.deviceName, WiFi.RSSI()); + snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI()); metrics += String(type) + String(topic); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); @@ -721,8 +729,15 @@ class Web { case metricsStateInverterFrameCnt: case metricsStateInverterTxCnt: case metricsStateInverterRetransmits: - metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].type) + " gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].valueFunc); + case metricsStateInverterIvRxCnt: + case metricsStateInverterIvTxCnt: + case metricsStateInverterDtuRxCnt: + case metricsStateInverterDtuTxCnt: + metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].topic) + " " + String(inverterMetrics[metricsStep].type) + "\n"; + metrics += inverterMetric(topic, sizeof(topic), + (String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic + + inverterMetrics[metricsStep].format).c_str(), + inverterMetrics[metricsStep].valueFunc); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); // ugly hack to increment the enum metricsStep = static_cast( static_cast(metricsStep) + 1); @@ -763,7 +778,7 @@ class Web { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); // Declare metric only once if (channel != 0 && !metricDeclared) { - snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); metrics += type; metricDeclared = true; } @@ -777,11 +792,11 @@ class Web { strncpy(total,"_total",sizeof(total)); } if (!metricTotalDeclard) { - snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); metrics += type; metricTotalDeclard = true; } - snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); } else { // Report (non zero) channel value // Use a fallback channel name (ch0, ch1, ...)if non is given by user @@ -791,7 +806,7 @@ class Web { } else { snprintf(chName,sizeof(chName),"ch%1d",channel); } - snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); } snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); metrics += topic; @@ -800,12 +815,14 @@ class Web { } } if (metrics.length() < 1) { - metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metrics = "# Info: Field #"+String(metricsFieldId)+" (" + fields[metricsFieldId] + + ") not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; metricsFieldId++; // Process next field Id metricsStep = metricStateRealtimeFieldId; } } else { - metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metrics = "# Info: No data for field #"+String(metricsFieldId)+ " (" + fields[metricsFieldId] + + ") of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; metricsFieldId++; // Process next field Id metricsStep = metricStateRealtimeFieldId; } @@ -821,7 +838,7 @@ class Web { case metricsStateAlarmData: // Alarm Info loop : fit to one packet // Perform grouping on metrics according to Prometheus exposition format specification - snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricPrefix,fields[FLD_LAST_ALARM_CODE]); + snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricConstPrefix,fields[FLD_LAST_ALARM_CODE]); metrics = type; for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { @@ -833,7 +850,7 @@ class Web { alarmChannelId = 0; if (alarmChannelId < rec->length) { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); metrics += topic; metrics += val; From f2e1e536f52a7f21efddde1f505a53c0aff29601 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 2 Jan 2024 15:12:24 +0100 Subject: [PATCH 015/115] 0.8.41 * fix display timeout (OLED) to 60s * change offs to signed value --- src/CHANGES.md | 4 ++++ src/app.cpp | 6 +++--- src/config/settings.h | 8 ++++---- src/defines.h | 2 +- src/plugins/Display/Display_Mono.h | 2 +- src/publisher/pubMqtt.h | 6 +++--- src/web/html/setup.html | 2 +- src/web/html/system.html | 2 +- 8 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index bbede74c..43e58613 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.41 - 2024-01-02 +* fix display timeout (OLED) to 60s +* change offs to signed value + ## 0.8.40 - 2024-01-02 * fix display of sunrise and sunset in `/system` #1308 * fix MqTT set power limit #1313 diff --git a/src/app.cpp b/src/app.cpp index 8e70778a..cd13050b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -235,7 +235,7 @@ void app::tickCalcSunrise(void) { onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); if (mMqttEnabled) { tickSun(); - nxtTrig = mSunrise - mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly + nxtTrig = mSunrise + mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly onceAt(std::bind(&app::tickSun, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm' } } @@ -254,8 +254,8 @@ void app::tickIVCommunication(void) { iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on if (!iv->commEnabled) { // inverter communication only during the day - if (mTimestamp < (mSunrise - mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start - nxtTrig = mSunrise - mConfig->sun.offsetSecMorning; + if (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start + nxtTrig = mSunrise + mConfig->sun.offsetSecMorning; } else { if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise nxtTrig = 0; diff --git a/src/config/settings.h b/src/config/settings.h index c493eb1a..5cecd46a 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -106,8 +106,8 @@ typedef struct { typedef struct { float lat; float lon; - uint16_t offsetSecMorning; - uint16_t offsetSecEvening; + int16_t offsetSecMorning; + int16_t offsetSecEvening; } cfgSun_t; typedef struct { @@ -635,8 +635,8 @@ class settings { } else { getVal(obj, F("lat"), &mCfg.sun.lat); getVal(obj, F("lon"), &mCfg.sun.lon); - getVal(obj, F("offs"), &mCfg.sun.offsetSecMorning); - getVal(obj, F("offsEve"), &mCfg.sun.offsetSecEvening); + getVal(obj, F("offs"), &mCfg.sun.offsetSecMorning); + getVal(obj, F("offsEve"), &mCfg.sun.offsetSecEvening); } } diff --git a/src/defines.h b/src/defines.h index 26ddf237..9ee93c17 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 40 +#define VERSION_PATCH 41 //------------------------------------- typedef struct { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 3e998b6d..322e991e 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -80,7 +80,7 @@ class DisplayMono { uint8_t mExtra; int8_t mPixelshift=0; - TimeMonitor mDisplayTime = TimeMonitor(1000 * 15, true); + TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); bool mDisplayActive = true; // always start with display on char mFmtText[DISP_FMT_TEXT_LEN]; diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 76ec452f..a1efa5fb 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -134,13 +134,13 @@ class PubMqtt { #endif } - bool tickerSun(uint32_t sunrise, uint32_t sunset, uint16_t offsM, uint16_t offsE) { + bool tickerSun(uint32_t sunrise, uint32_t sunset, int16_t offsM, int16_t offsE) { 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 - offsM).c_str(), true); + publish(subtopics[MQTT_COMM_START], String(sunrise + offsM).c_str(), true); publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true); Inverter<> *iv; @@ -155,7 +155,7 @@ class PubMqtt { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); - publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise - offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise + offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); return true; } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index aef43dcf..aa52ac33 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -895,7 +895,7 @@ document.getElementsByName("sunLon")[0].value = obj["lon"]; for(p of [["sunOffsSr", "offsSr"], ["sunOffsSs", "offsSs"]]) { const sel = document.getElementsByName(p[0])[0]; - for(var i = 0; i <= 60; i++) { + for(var i = -60; i <= 60; i++) { sel.appendChild(opt(i, i + " minutes", (i == (obj[p[1]] / 60)))); } } diff --git a/src/web/html/system.html b/src/web/html/system.html index 0415864b..035f30ff 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -108,7 +108,7 @@ ml("tbody", {}, [ tr("Sunrise", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), tr("Sunset", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), - tr("Communication start", new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')), + tr("Communication start", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), tr("Night behaviour", badge(obj.disNightComm, ((obj.disNightComm) ? "not" : "") + " communicating", "warning")) ]) From 4ae4c167666bba2f390ffd8876a00a172b860fec Mon Sep 17 00:00:00 2001 From: Gerald <34278535+GHolli@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:21:04 +0100 Subject: [PATCH 016/115] Add space between values and units. --- src/web/html/visualization.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index d255d1c5..b027ef95 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -106,7 +106,7 @@ if(65535 != obj.power_limit_read) { pwrLimit = obj.power_limit_read + " %"; if(0 != obj.max_pwr) - pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W"; + pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + " W"; } return ml("div", {class: "row mt-2"}, ml("div", {class: "col"}, [ @@ -192,9 +192,9 @@ if(obj.rssi > -127) { if(obj.generation < 2) - ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)"; + ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64 dBm)"; else - ageInfo += " (RSSI: " + obj.rssi + "dBm)"; + ageInfo += " (RSSI: " + obj.rssi + " dBm)"; } return ml("div", {class: "mb-5"}, [ @@ -383,9 +383,9 @@ var html = ml("table", {class: "table"}, [ ml("tbody", {}, [ tr2(["TX count", obj.tx_cnt, ""]), - tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]), - tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]), - tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]), + tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + " %"]), tr2(["RX fragments", obj.frame_cnt, ""]), tr2(["TX retransmits", obj.retransmits, ""]) ]) From cd9cc976e76b77cd47b4acbd79b01687d72678d9 Mon Sep 17 00:00:00 2001 From: lumapu Date: Wed, 3 Jan 2024 00:14:10 +0100 Subject: [PATCH 017/115] 0.8.42 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 --- src/CHANGES.md | 3 +++ src/app.cpp | 32 ++++++++++++++++++-------------- src/config/config.h | 3 +++ src/config/settings.h | 18 ++++++++++-------- src/defines.h | 2 +- src/web/RestApi.h | 5 +++-- src/web/html/setup.html | 2 +- src/web/web.h | 23 ++++++++++++----------- 8 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 43e58613..1539fd5c 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.42 - 2024-01-02 +* add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 + ## 0.8.41 - 2024-01-02 * fix display timeout (OLED) to 60s * change offs to signed value diff --git a/src/app.cpp b/src/app.cpp index cd13050b..6bce1709 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -456,14 +456,11 @@ void app::mqttSubRxCb(JsonObject obj) { //----------------------------------------------------------------------------- void app::setupLed(void) { uint8_t led_off = (mConfig->led.high_active) ? 0 : 255; - - if (mConfig->led.led0 != DEF_PIN_OFF) { - pinMode(mConfig->led.led0, OUTPUT); - analogWrite(mConfig->led.led0, led_off); - } - if (mConfig->led.led1 != DEF_PIN_OFF) { - pinMode(mConfig->led.led1, OUTPUT); - analogWrite(mConfig->led.led1, led_off); + for(uint8_t i = 0; i < 3; i ++) { + if (mConfig->led.led[i] != DEF_PIN_OFF) { + pinMode(mConfig->led.led[i], OUTPUT); + analogWrite(mConfig->led.led[i], led_off); + } } } @@ -472,27 +469,34 @@ void app::updateLed(void) { uint8_t led_off = (mConfig->led.high_active) ? 0 : 255; uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance); - if (mConfig->led.led0 != DEF_PIN_OFF) { + if (mConfig->led.led[0] != DEF_PIN_OFF) { Inverter<> *iv; for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL != iv) { if (iv->isProducing()) { // turn on when at least one inverter is producing - analogWrite(mConfig->led.led0, led_on); + analogWrite(mConfig->led.led[0], led_on); break; } else if(iv->config->enabled) - analogWrite(mConfig->led.led0, led_off); + analogWrite(mConfig->led.led[0], led_off); } } } - if (mConfig->led.led1 != DEF_PIN_OFF) { + if (mConfig->led.led[1] != DEF_PIN_OFF) { if (getMqttIsConnected()) { - analogWrite(mConfig->led.led1, led_on); + analogWrite(mConfig->led.led[1], led_on); } else { - analogWrite(mConfig->led.led1, led_off); + analogWrite(mConfig->led.led[1], led_off); } } + + if (mConfig->led.led[2] != DEF_PIN_OFF) { + if((mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) || (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning))) + analogWrite(mConfig->led.led[2], led_on); + else + analogWrite(mConfig->led.led[2], led_off); + } } diff --git a/src/config/config.h b/src/config/config.h index 2cb9bcd3..fb05a1cc 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -142,6 +142,9 @@ #ifndef DEF_LED1 #define DEF_LED1 DEF_PIN_OFF #endif +#ifndef DEF_LED2 + #define DEF_LED2 DEF_PIN_OFF +#endif #ifdef LED_ACTIVE_HIGH #define LED_HIGH_ACTIVE true #else diff --git a/src/config/settings.h b/src/config/settings.h index 5cecd46a..476f9fbf 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -118,8 +118,7 @@ typedef struct { } cfgSerial_t; typedef struct { - uint8_t led0; // first LED pin - uint8_t led1; // second LED pin + uint8_t led[3]; // LED pins bool high_active; // determines if LEDs are high or low active uint8_t luminance; // luminance of LED } cfgLed_t; @@ -453,8 +452,9 @@ class settings { mCfg.inst.iv[i].add2Total = true; } - mCfg.led.led0 = DEF_LED0; - mCfg.led.led1 = DEF_LED1; + mCfg.led.led[0] = DEF_LED0; + mCfg.led.led[1] = DEF_LED1; + mCfg.led.led[2] = DEF_LED2; mCfg.led.high_active = LED_HIGH_ACTIVE; mCfg.led.luminance = 255; @@ -677,13 +677,15 @@ class settings { void jsonLed(JsonObject obj, bool set = false) { if(set) { - obj[F("0")] = mCfg.led.led0; - obj[F("1")] = mCfg.led.led1; + obj[F("0")] = mCfg.led.led[0]; + obj[F("1")] = mCfg.led.led[1]; + obj[F("2")] = mCfg.led.led[2]; obj[F("act_high")] = mCfg.led.high_active; obj[F("lum")] = mCfg.led.luminance; } else { - getVal(obj, F("0"), &mCfg.led.led0); - getVal(obj, F("1"), &mCfg.led.led1); + getVal(obj, F("0"), &mCfg.led.led[0]); + getVal(obj, F("1"), &mCfg.led.led[1]); + getVal(obj, F("2"), &mCfg.led.led[2]); getVal(obj, F("act_high"), &mCfg.led.high_active); getVal(obj, F("lum"), &mCfg.led.luminance); } diff --git a/src/defines.h b/src/defines.h index 9ee93c17..56bdf5e6 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 41 +#define VERSION_PATCH 42 //------------------------------------- typedef struct { diff --git a/src/web/RestApi.h b/src/web/RestApi.h index c920a8e3..7541e095 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -611,8 +611,9 @@ class RestApi { 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; + obj[F("led0")] = mConfig->led.led[0]; + obj[F("led1")] = mConfig->led.led[1]; + obj[F("led2")] = mConfig->led.led[2]; obj[F("led_high_active")] = mConfig->led.high_active; obj[F("led_lum")] = mConfig->led.luminance; } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index aa52ac33..a103cf15 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -909,7 +909,7 @@ if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ - pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected']]; + pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected'], ['led2', 'pinLed2', 'Night time']]; for(p of pins) { e.append( ml("div", {class: "row mb-3"}, [ diff --git a/src/web/web.h b/src/web/web.h index 3ed0353d..3df40839 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -39,7 +39,7 @@ #define WEB_SERIAL_BUF_SIZE 2048 -const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; +const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; template class Web { @@ -506,7 +506,7 @@ class Web { // pinout uint8_t pin; - for (uint8_t i = 0; i < 15; i++) { + for (uint8_t i = 0; i < 16; i++) { pin = request->arg(String(pinArgNames[i])).toInt(); switch(i) { case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break; @@ -515,15 +515,16 @@ class Web { case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_NRF_SCLK_PIN); break; case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_NRF_MOSI_PIN); break; case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_NRF_MISO_PIN); break; - case 6: mConfig->led.led0 = pin; break; - case 7: mConfig->led.led1 = pin; break; - case 8: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense - case 9: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense - case 10: mConfig->cmt.pinSclk = pin; break; - case 11: mConfig->cmt.pinSdio = pin; break; - case 12: mConfig->cmt.pinCsb = pin; break; - case 13: mConfig->cmt.pinFcsb = pin; break; - case 14: mConfig->cmt.pinIrq = pin; break; + case 6: mConfig->led.led[0] = pin; break; + case 7: mConfig->led.led[1] = pin; break; + case 8: mConfig->led.led[2] = pin; break; + case 9: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense + case 10: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense + case 11: mConfig->cmt.pinSclk = pin; break; + case 12: mConfig->cmt.pinSdio = pin; break; + case 13: mConfig->cmt.pinCsb = pin; break; + case 14: mConfig->cmt.pinFcsb = pin; break; + case 15: mConfig->cmt.pinIrq = pin; break; } } From 9ad119b39ea1ca6843a9496fcfe456fa4c50dc50 Mon Sep 17 00:00:00 2001 From: lumapu Date: Wed, 3 Jan 2024 00:20:22 +0100 Subject: [PATCH 018/115] 0.8.42 * merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 * meger PR: Prometheus add `getLossRate` and bugfixing #1315 --- src/CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CHANGES.md b/src/CHANGES.md index 1539fd5c..20a6f745 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,8 @@ ## 0.8.42 - 2024-01-02 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 +* merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 +* meger PR: Prometheus add `getLossRate` and bugfixing #1315 ## 0.8.41 - 2024-01-02 * fix display timeout (OLED) to 60s From dd0bec6fc0d785757778f4fbede8698fe5c84f52 Mon Sep 17 00:00:00 2001 From: lumapu Date: Wed, 3 Jan 2024 00:21:49 +0100 Subject: [PATCH 019/115] 0.8.42 --- src/CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 20a6f745..ba3c661b 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -3,7 +3,7 @@ ## 0.8.42 - 2024-01-02 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 * merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 -* meger PR: Prometheus add `getLossRate` and bugfixing #1315 +* merge PR: Prometheus add `getLossRate` and bugfixing #1315 ## 0.8.41 - 2024-01-02 * fix display timeout (OLED) to 60s From 0d10d19b30ce8d48d6215bda352821fb8c3b544a Mon Sep 17 00:00:00 2001 From: lumapu Date: Wed, 3 Jan 2024 01:29:46 +0100 Subject: [PATCH 020/115] 0.8.42 * add loss rate to `/visualization` in the statistics window * corrected `getLossRate` infos for MqTT and prometheus * added information about working IRQ for NRF24 and CMT2300A to `/system` --- src/CHANGES.md | 3 +++ src/defines.h | 8 ++++---- src/hm/Communication.h | 2 +- src/hm/hmInverter.h | 29 ++++++++++++++++++++--------- src/hm/hmRadio.h | 2 +- src/hm/radio.h | 6 ++++++ src/hms/hmsRadio.h | 2 +- src/publisher/pubMqttIvData.h | 10 +++++----- src/web/RestApi.h | 6 ++++++ src/web/html/system.html | 10 ++++++++++ src/web/html/visualization.html | 4 +++- src/web/web.h | 8 ++++---- 12 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index ba3c661b..a2be1750 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -4,6 +4,9 @@ * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 * merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 * merge PR: Prometheus add `getLossRate` and bugfixing #1315 +* add loss rate to `/visualization` in the statistics window +* corrected `getLossRate` infos for MqTT and prometheus +* added information about working IRQ for NRF24 and CMT2300A to `/system` ## 0.8.41 - 2024-01-02 * fix display timeout (OLED) to 60s diff --git a/src/defines.h b/src/defines.h index 56bdf5e6..4b3482a2 100644 --- a/src/defines.h +++ b/src/defines.h @@ -103,10 +103,10 @@ typedef struct { uint32_t frmCnt; uint32_t txCnt; uint32_t retransmits; - uint16_t ivRxCnt; // last iv rx frames (from GetLossRate) - uint16_t ivTxCnt; // last iv tx frames (from GetLossRate) - uint16_t dtuRxCnt; // current DTU rx frames (since last GetLossRate) - uint16_t dtuTxCnt; // current DTU tx frames (since last GetLossRate) + uint16_t ivLoss; // lost frames (from GetLossRate) + uint16_t ivSent; // sent frames (from GetLossRate) + uint16_t dtuLoss; // current DTU lost frames (since last GetLossRate) + uint16_t dtuSent; // current DTU sent frames (since last GetLossRate) } statistics_t; #endif /*__DEFINES_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h index d44e860f..e31984b6 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -151,7 +151,7 @@ class Communication : public CommQueue<> { if(validateIvSerial(&p->packet[1], q->iv)) { q->iv->radioStatistics.frmCnt++; - q->iv->radioStatistics.dtuRxCnt++; + q->iv->mDtuRxCnt++; if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if(parseFrame(p)) diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 25f575e5..57de135f 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -595,22 +595,27 @@ class Inverter { uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t txCnt = (pyld[2] << 8) + pyld[3]; - if (radioStatistics.ivRxCnt || radioStatistics.ivTxCnt) { // there was successful GetLossRate in the past + if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past + radioStatistics.ivLoss = mDtuTxCnt - (rxCnt - mIvRxCnt); + radioStatistics.ivSent = mDtuTxCnt; + radioStatistics.dtuLoss = txCnt - mIvTxCnt - mDtuRxCnt; + radioStatistics.dtuSent = txCnt - mIvTxCnt; + DPRINT_IVID(DBG_INFO, id); DBGPRINT(F("Inv loss: ")); - DBGPRINT(String (radioStatistics.dtuTxCnt - (rxCnt - radioStatistics.ivRxCnt))); + DBGPRINT(String(radioStatistics.ivLoss)); DBGPRINT(F(" of ")); - DBGPRINT(String (radioStatistics.dtuTxCnt)); + DBGPRINT(String(radioStatistics.ivSent)); DBGPRINT(F(", DTU loss: ")); - DBGPRINT(String (txCnt - radioStatistics.ivTxCnt - radioStatistics.dtuRxCnt)); + DBGPRINT(String(radioStatistics.dtuLoss)); DBGPRINT(F(" of ")); - DBGPRINTLN(String (txCnt - radioStatistics.ivTxCnt)); + DBGPRINTLN(String(radioStatistics.dtuSent)); } - radioStatistics.ivRxCnt = rxCnt; - radioStatistics.ivTxCnt = txCnt; - radioStatistics.dtuRxCnt = 0; // start new interval - radioStatistics.dtuTxCnt = 0; // start new interval + mIvRxCnt = rxCnt; + mIvTxCnt = txCnt; + mDtuRxCnt = 0; // start new interval + mDtuTxCnt = 0; // start new interval return true; } @@ -806,6 +811,12 @@ class Inverter { uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug + uint16_t mIvRxCnt = 0; + uint16_t mIvTxCnt = 0; + + public: + uint16_t mDtuRxCnt = 0; + uint16_t mDtuTxCnt = 0; }; template diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index c7b9581c..6539dd21 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -339,7 +339,7 @@ class HmRadio : public Radio { mMillis = millis(); mLastIv = iv; - iv->radioStatistics.dtuTxCnt++; + iv->mDtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/hm/radio.h b/src/hm/radio.h index 2fe4f640..1ef32e05 100644 --- a/src/hm/radio.h +++ b/src/hm/radio.h @@ -14,6 +14,8 @@ #include "../utils/dbg.h" #include "../utils/crc.h" +enum { IRQ_UNKNOWN = 0, IRQ_OK, IRQ_ERROR }; + // forward declaration of class template class Inverter; @@ -30,6 +32,7 @@ class Radio { void handleIntr(void) { mIrqRcvd = true; + mIrqOk = IRQ_OK; } void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { @@ -65,6 +68,7 @@ class Radio { public: std::queue mBufCtrl; + uint8_t mIrqOk = IRQ_UNKNOWN; protected: virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0; @@ -77,6 +81,8 @@ class Radio { CP_U32_LittleEndian(&mTxBuf[5], mDtuSn); mTxBuf[9] = pid; memset(&mTxBuf[10], 0x00, (MAX_RF_PAYLOAD_SIZE-10)); + if(IRQ_UNKNOWN == mIrqOk) + mIrqOk = IRQ_ERROR; } void updateCrcs(uint8_t *len, bool appendCrc16=true) { diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index 3b3893ac..6b502816 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -112,7 +112,7 @@ class CmtRadio : public Radio { if(CMT_ERR_RX_IN_FIFO == status) mIrqRcvd = true; } - iv->radioStatistics.dtuTxCnt++; + iv->mDtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 3a38a5ed..c645c70b 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -201,10 +201,10 @@ class PubMqttIvData { mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFailNoAnser, mIv->radioStatistics.retransmits, - mIv->radioStatistics.ivRxCnt, - mIv->radioStatistics.ivTxCnt, - mIv->radioStatistics.dtuRxCnt, - mIv->radioStatistics.dtuTxCnt); + mIv->radioStatistics.ivLoss, + mIv->radioStatistics.ivSent, + mIv->radioStatistics.dtuLoss, + mIv->radioStatistics.dtuSent); mPublish(mSubTopic, mVal, false, QOS_0); } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 7541e095..03275eed 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -405,6 +405,10 @@ class RestApi { obj[F("frame_cnt")] = iv->radioStatistics.frmCnt; obj[F("tx_cnt")] = iv->radioStatistics.txCnt; obj[F("retransmits")] = iv->radioStatistics.retransmits; + obj[F("ivLoss")] = iv->radioStatistics.ivLoss; + obj[F("ivSent")] = iv->radioStatistics.ivSent; + obj[F("dtuLoss")] = iv->radioStatistics.dtuLoss; + obj[F("dtuSent")] = iv->radioStatistics.dtuSent; } void getIvPowerLimitAck(JsonObject obj, uint8_t id) { @@ -633,6 +637,7 @@ class RestApi { if(mConfig->cmt.enabled) { obj[F("isconnected")] = mRadioCmt->isChipConnected(); obj[F("sn")] = String(mRadioCmt->getDTUSn(), HEX); + obj[F("irqOk")] = mRadioCmt->mIrqOk; } } #endif @@ -643,6 +648,7 @@ class RestApi { obj[F("isconnected")] = mRadioNrf->isChipConnected(); obj[F("dataRate")] = mRadioNrf->getDataRate(); obj[F("sn")] = String(mRadioNrf->getDTUSn(), HEX); + obj[F("irqOk")] = mRadioNrf->mIrqOk; } } diff --git a/src/web/html/system.html b/src/web/html/system.html index 035f30ff..f400db41 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -44,12 +44,21 @@ return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text))) } + function irqBadge(state) { + switch(state) { + case 0: return badge(false, "unknown", "warning"); break; + case 1: return badge(true, "true"); break; + default: return badge(false, "false"); break; + } + } + function parseRadio(obj) { const dr = ["1 M", "2 M", "250 k"] if(obj.radioNrf.en) { lines = [ tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")), + tr("Interrupt Pin working", irqBadge(obj.radioNrf.irqOk)), tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps"), tr("DTU Radio ID", obj.radioNrf.sn) ]; @@ -67,6 +76,7 @@ if(obj.radioCmt.en) { cmt = [ tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")), + tr("Interrupt Pin working", irqBadge(obj.radioCmt.irqOk)), tr("DTU Radio ID", obj.radioCmt.sn) ]; } else diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 88bbdc9b..ec6596bb 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -388,7 +388,9 @@ tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + " %"]), tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + " %"]), tr2(["RX fragments", obj.frame_cnt, ""]), - tr2(["TX retransmits", obj.retransmits, ""]) + tr2(["TX retransmits", obj.retransmits, ""]), + tr2(["Inverter loss rate", "lost " + obj.ivLoss + " of " + obj.ivSent, String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + " %"]), + tr2(["DTU loss rate", "lost " + obj.dtuLoss + " of " + obj.dtuSent, String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + " %"]) ]) ]) modal("Radio statistics for inverter " + obj.name, ml("div", {}, html)) diff --git a/src/web/web.h b/src/web/web.h index 2bcc031a..ea25e5ff 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -665,10 +665,10 @@ class Web { { "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, { "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, { "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }, - { "radio_iv_rx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivRxCnt;} }, - { "radio_iv_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivTxCnt;} }, - { "radio_dtu_rx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuRxCnt;} }, - { "radio_dtu_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuTxCnt;} } + { "radio_iv_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivLoss;} }, + { "radio_iv_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivSent;} }, + { "radio_dtu_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuLoss;} }, + { "radio_dtu_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuSent;} } }; int metricsInverterId; uint8_t metricsFieldId; From 31ecb9620fbd188c7bad50c9828d029e7e7b8f7a Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 5 Jan 2024 01:24:38 +0100 Subject: [PATCH 021/115] 0.8.43 * fix display of sunrise in `/system` #1308 * fix overflow of `getLossRate` calculation #1318 * improved MqTT by marking sent data and improved `last_success` resends #1319 * added timestamp for `max ac power` as tooltip #1324 #1123 #1199 * repaired Power-limit acknowledge #1322 --- src/CHANGES.md | 7 +++ src/app.cpp | 1 + src/app.h | 4 -- src/appInterface.h | 1 - src/defines.h | 2 +- src/hm/Communication.h | 7 +++ src/hm/hmInverter.h | 75 +++++++++++++++++++++------------ src/publisher/pubMqttIvData.h | 32 +++++--------- src/web/RestApi.h | 1 + src/web/html/index.html | 6 +-- src/web/html/setup.html | 2 +- src/web/html/style.css | 35 +++++---------- src/web/html/visualization.html | 11 +++-- 13 files changed, 97 insertions(+), 87 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index a2be1750..ca1f5332 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,12 @@ # Development Changes +## 0.8.43 - 2024-01-04 +* fix display of sunrise in `/system` #1308 +* fix overflow of `getLossRate` calculation #1318 +* improved MqTT by marking sent data and improved `last_success` resends #1319 +* added timestamp for `max ac power` as tooltip #1324 #1123 #1199 +* repaired Power-limit acknowledge #1322 + ## 0.8.42 - 2024-01-02 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 * merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 diff --git a/src/app.cpp b/src/app.cpp index 6bce1709..6a93f96c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -52,6 +52,7 @@ void app::setup() { mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); mSys.setup(&mTimestamp, &mConfig->inst); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { initInverter(i); diff --git a/src/app.h b/src/app.h index a24cccb3..8d6fafbe 100644 --- a/src/app.h +++ b/src/app.h @@ -180,10 +180,6 @@ class app : public IApp, public ah::Scheduler { once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); } - void setMqttPowerLimitAck(Inverter<> *iv) { - mMqtt.setPowerLimitAck(iv); - } - bool getMqttIsConnected() { return mMqtt.isConnected(); } diff --git a/src/appInterface.h b/src/appInterface.h index 34dc5ddc..9d633765 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -52,7 +52,6 @@ class IApp { virtual bool getRebootRequestState() = 0; virtual bool getSettingsValid() = 0; virtual void setMqttDiscoveryFlag() = 0; - virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; virtual bool getMqttIsConnected() = 0; virtual bool getNrfEnabled() = 0; diff --git a/src/defines.h b/src/defines.h index 4b3482a2..21564fdb 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 42 +#define VERSION_PATCH 43 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index e31984b6..99c8f382 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -19,6 +19,7 @@ #define MAX_BUFFER 250 typedef std::function *)> payloadListenerType; +typedef std::function *)> powerLimitAckListenerType; typedef std::function *)> alarmListenerType; class Communication : public CommQueue<> { @@ -40,6 +41,10 @@ class Communication : public CommQueue<> { mCbPayload = cb; } + void addPowerLimitAckListener(powerLimitAckListenerType cb) { + mCbPwrAck = cb; + } + void addAlarmListener(alarmListenerType cb) { mCbAlarm = cb; } @@ -401,6 +406,7 @@ class Communication : public CommQueue<> { DBGPRINT(F(" with PowerLimitControl ")); DBGPRINTLN(String(q->iv->powerLimit[1])); q->iv->actPowerLimit = 0xffff; // unknown, readback current value + (mCbPwrAck)(q->iv); return accepted; } @@ -921,6 +927,7 @@ class Communication : public CommQueue<> { uint8_t mMaxFrameId; uint8_t mPayload[MAX_BUFFER]; payloadListenerType mCbPayload = NULL; + powerLimitAckListenerType mCbPwrAck = NULL; alarmListenerType mCbAlarm = NULL; Heuristic mHeu; uint32_t mLastEmptyQueueMillis = 0; diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 57de135f..1d38c23f 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -64,13 +64,28 @@ struct calcFunc_t { func_t* func; // function pointer }; +enum class MqttSentStatus : uint8_t { + NEW_DATA, + LAST_SUCCESS_SENT, + DATA_SENT +}; + +enum class InverterStatus : uint8_t { + OFF, + STARTING, + PRODUCING, + WAS_PRODUCING, + WAS_ON +}; + template struct record_t { - byteAssign_t* assign; // assignment of bytes in payload - uint8_t length; // length of the assignment list - T *record; // data pointer - uint32_t ts; // timestamp of last received payload - uint8_t pyldLen; // expected payload length for plausibility check + byteAssign_t* assign; // assignment of bytes in payload + uint8_t length; // length of the assignment list + T *record; // data pointer + uint32_t ts; // timestamp of last received payload + uint8_t pyldLen; // expected payload length for plausibility check + MqttSentStatus mqttSentStatus; // indicates the current MqTT sent status }; struct alarm_t { @@ -94,14 +109,6 @@ const calcFunc_t calcFunctions[] = { { CALC_MPDC_CH, &calcMaxPowerDc } }; -enum class InverterStatus : uint8_t { - OFF, - STARTING, - PRODUCING, - WAS_PRODUCING, - WAS_ON -}; - template class Inverter { public: @@ -124,26 +131,28 @@ class Inverter { bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) InverterStatus status; // indicates the current inverter status std::array lastAlarm; // holds last 10 alarms - uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer) + int8_t rssi; // RSSI uint16_t alarmCnt; // counts the total number of occurred alarms uint16_t alarmLastId; // lastId which was received - int8_t rssi; // RSSI + uint8_t mCmd; // holds the command to send + bool mGotFragment; // shows if inverter has sent at least one fragment uint8_t miMultiParts; // helper info for MI multiframe msgs uint8_t outstandingFrames; // helper info to count difference between expected and received frames - bool mGotFragment; // shows if inverter has sent at least one fragment uint8_t curFrmCnt; // count received frames in current loop bool mGotLastMsg; // shows if inverter has already finished transmission cycle - uint8_t mCmd; // holds the command to send bool mIsSingleframeReq; // indicates this is a missing single frame request Radio *radio; // pointer to associated radio class statistics_t radioStatistics; // information about transmitted, failed, ... packets HeuristicInv heuristics; // heuristic information / logic uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime bool commEnabled; // 'pause night communication' sets this field to false + uint32_t tsMaxAcPower; // holds the timestamp when the MaxAC power was seen static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup + public: + Inverter() { ivGen = IV_HM; powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited @@ -155,7 +164,6 @@ class Inverter { alarmMesIndex = 0; isConnected = false; status = InverterStatus::OFF; - alarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; rssi = -127; @@ -165,6 +173,7 @@ class Inverter { mIsSingleframeReq = false; radio = NULL; commEnabled = true; + tsMaxAcPower = 0; memset(&radioStatistics, 0, sizeof(statistics_t)); memset(heuristics.txRfQuality, -6, 5); @@ -310,11 +319,11 @@ class Inverter { rec->record[pos] = (REC_TYP)(val); } } + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; } if(rec == &recordMeas) { DPRINTLN(DBG_VERBOSE, "add real time"); - // get last alarm message index and save it in the inverter object if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (alarmMesIndex < rec->record[pos]) { @@ -498,6 +507,7 @@ class Inverter { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment")); rec->ts = 0; rec->length = 0; + rec->mqttSentStatus = MqttSentStatus::DATA_SENT; // nothing new to transmit switch (cmd) { case RealTimeRunData_Debug: if (INV_TYPE_1CH == type) { @@ -582,7 +592,7 @@ class Inverter { void resetAlarms() { lastAlarm.fill({0, 0, 0}); - alarmNxtWrPos = 0; + mAlarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; @@ -596,10 +606,18 @@ class Inverter { uint16_t txCnt = (pyld[2] << 8) + pyld[3]; if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past - radioStatistics.ivLoss = mDtuTxCnt - (rxCnt - mIvRxCnt); radioStatistics.ivSent = mDtuTxCnt; - radioStatistics.dtuLoss = txCnt - mIvTxCnt - mDtuRxCnt; - radioStatistics.dtuSent = txCnt - mIvTxCnt; + if (rxCnt < mIvRxCnt) // overflow + radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt + ((uint16_t)65535 - mIvRxCnt) + 1); + else + radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt - mIvRxCnt); + + if (txCnt < mIvTxCnt) // overflow + radioStatistics.dtuSent = txCnt + ((uint16_t)65535 - mIvTxCnt) + 1; + else + radioStatistics.dtuSent = txCnt - mIvTxCnt; + + radioStatistics.dtuLoss = radioStatistics.dtuSent - mDtuRxCnt; DPRINT_IVID(DBG_INFO, id); DBGPRINT(F("Inv loss: ")); @@ -790,9 +808,9 @@ class Inverter { private: inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { - lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end); - if(++alarmNxtWrPos >= 10) // rolling buffer - alarmNxtWrPos = 0; + lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end); + if(++mAlarmNxtWrPos >= 10) // rolling buffer + mAlarmNxtWrPos = 0; } void toRadioId(void) { @@ -813,6 +831,7 @@ class Inverter { uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug uint16_t mIvRxCnt = 0; uint16_t mIvTxCnt = 0; + uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer) public: uint16_t mDtuRxCnt = 0; @@ -948,8 +967,10 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { dcMaxPower = iv->getValue(i, rec); } } - if(dcPower > dcMaxPower) + if(dcPower > dcMaxPower) { + iv->tsMaxAcPower = *iv->timestamp; return dcPower; + } } return dcMaxPower; } diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index c645c70b..61344ede 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -28,7 +28,6 @@ class PubMqttIvData { mState = IDLE; mZeroValues = false; - memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t)); mRTRDataHasBeenSent = false; mTable[IDLE] = &PubMqttIvData::stateIdle; @@ -102,7 +101,7 @@ class PubMqttIvData { mPos = 0; if(found) { record_t<> *rec = mIv->getRecordStruct(mCmd); - if((RealTimeRunData_Debug == mCmd) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages.... + if(MqttSentStatus::NEW_DATA == rec->mqttSentStatus) { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); mPublish(mSubTopic, mVal, true, QOS_0); @@ -112,13 +111,14 @@ class PubMqttIvData { snprintf(mVal, 40, "%d", mIv->rssi); mPublish(mSubTopic, mVal, false, QOS_0); } + rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT; } mIv->isProducing(); // recalculate status mState = SEND_DATA; - } else if(mSendTotals && mTotalFound) + } else if(mSendTotals && mTotalFound) { mState = SEND_TOTALS; - else { + } else { mSendList->pop(); mZeroValues = false; mState = START; @@ -132,12 +132,8 @@ class PubMqttIvData { DPRINT(DBG_WARN, "unknown record to publish!"); return; } - uint32_t lastTs = mIv->getLastTs(rec); - bool pubData = (lastTs > 0); - if (mCmd == RealTimeRunData_Debug) - pubData &= (lastTs != mIvLastRTRpub[mIv->id]); - if (pubData) { + if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { if(mPos < rec->length) { bool retained = false; if (mCmd == RealTimeRunData_Debug) { @@ -172,21 +168,16 @@ class PubMqttIvData { } else mAllTotalFound = false; } - } else - mIvLastRTRpub[mIv->id] = lastTs; - - uint8_t qos = QOS_0; - if(FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) - qos = QOS_2; - - if((mIvSend == mIv) || (NULL == mIvSend)) { // send only updated values, or all if the inverter is NULL - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); - snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); - mPublish(mSubTopic, mVal, retained, qos); } + + uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); + snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); + mPublish(mSubTopic, mVal, retained, qos); mPos++; } else { sendRadioStat(rec->length); + rec->mqttSentStatus = MqttSentStatus::DATA_SENT; mState = FIND_NXT_IV; } } else @@ -263,7 +254,6 @@ class PubMqttIvData { Inverter<> *mIv, *mIvSend; uint8_t mPos; - uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 03275eed..2b341441 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -485,6 +485,7 @@ class RestApi { obj[F("status")] = (uint8_t)iv->getStatus(); obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("rssi")] = iv->rssi; + obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower; JsonArray ch = obj.createNestedArray("ch"); diff --git a/src/web/html/index.html b/src/web/html/index.html index 3ac72e89..cfdaeee6 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -99,17 +99,17 @@ } if(obj.disNightComm) { - if(((obj.ts_sunrise - obj.ts_offsSr) < obj.ts_now) + if(((obj.ts_sunrise + obj.ts_offsSr) < obj.ts_now) && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) { commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { commInfo = "Night time, inverter polling disabled, "; - if(obj.ts_now > (obj.ts_sunrise - obj.ts_offsSr)) { + if(obj.ts_now > (obj.ts_sunrise + obj.ts_offsSr)) { commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo += "will start polling at " + (new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')); + commInfo += "will start polling at " + (new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')); } } } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index a103cf15..782c23be 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -765,7 +765,7 @@ ml("div", {class: "col-2"}, cbDisNightCom) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default)"), + ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default, MqTT only)"), ml("div", {class: "col-2"}, cbAddTotal) ]) ]), diff --git a/src/web/html/style.css b/src/web/html/style.css index b31ae7c9..e7de68f4 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -650,39 +650,24 @@ div.hr { } -.css-tooltip{ +.tooltip{ position: relative; } -.css-tooltip:hover:after{ - content:attr(data-tooltip); - background:#000; - padding:5px; - border-radius:3px; +.tooltip:hover:after { + content: attr(data); + background: var(--nav-active); + 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; + color: var(--fg2); + min-width: 100px; + 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); + font-size: 1rem; } #modal { diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index ec6596bb..3ae8ff0b 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -45,13 +45,14 @@ ]); } - function numMid(val, unit, des) { + function numMid(val, unit, des, opt={class: "fs-6"}) { return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ ml("div", {class: "row"}, ml("div", {class: "col"}, [ - ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)), + ml("span", opt, String(Math.round(val * 100) / 100)), ml("span", {class: "fs-8 mx-1"}, unit) - ])), + ]) + ), ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-9"}, des) @@ -108,6 +109,8 @@ if(0 != obj.max_pwr) pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + " W"; } + + var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); return ml("div", {class: "row mt-2"}, ml("div", {class: "col"}, [ ml("div", {class: "p-2 " + clh}, @@ -133,7 +136,7 @@ ]), ml("div", {class: "hr"}), ml("div", {class: "row mt-2"},[ - numMid(obj.ch[0][11], "W", "Max AC Power"), + numMid(obj.ch[0][11], "W", "Max AC Power", {class: "fs-6 tooltip", data: maxAcPwr}), numMid(obj.ch[0][8], "W", "DC Power"), numMid(obj.ch[0][0], "V", "AC Voltage"), numMid(obj.ch[0][1], "A", "AC Current"), From 21d77f58cac1077229e88187a1223118a691b065 Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 5 Jan 2024 01:33:26 +0100 Subject: [PATCH 022/115] 0.8.43 * fix `max_power` in `/visualization` was set to `0` after sunset --- src/CHANGES.md | 1 + src/web/html/visualization.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index ca1f5332..c416fd80 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -6,6 +6,7 @@ * improved MqTT by marking sent data and improved `last_success` resends #1319 * added timestamp for `max ac power` as tooltip #1324 #1123 #1199 * repaired Power-limit acknowledge #1322 +* fix `max_power` in `/visualization` was set to `0` after sunset ## 0.8.42 - 2024-01-02 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 3ae8ff0b..27f5da35 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -92,10 +92,10 @@ function ivHead(obj) { if(0 != obj.status) { // only add totals if inverter is online total[0] += obj.ch[0][2]; // P_AC - total[3] += obj.ch[0][11]; // MAX P_AC total[4] += obj.ch[0][8]; // P_DC total[5] += obj.ch[0][10]; // Q_AC } + total[3] += obj.ch[0][11]; // MAX P_AC total[1] += obj.ch[0][7]; // YieldDay total[2] += obj.ch[0][6]; // YieldTotal From b4ba35e6ab4f452ebf451789b4cb4c02968b084a Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 5 Jan 2024 14:08:38 +0100 Subject: [PATCH 023/115] 0.8.44 * fix MqTT transmission of data #1326 * live data is read much earlier / faster and more often --- src/CHANGES.md | 4 ++++ src/defines.h | 2 +- src/hm/Communication.h | 1 + src/hm/hmInverter.h | 48 ++++++++++++++++++++++++------------------ 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index c416fd80..9dc10a72 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.44 - 2024-01-05 +* fix MqTT transmission of data #1326 +* live data is read much earlier / faster and more often + ## 0.8.43 - 2024-01-04 * fix display of sunrise in `/system` #1308 * fix overflow of `getLossRate` calculation #1318 diff --git a/src/defines.h b/src/defines.h index 21564fdb..f0c9731a 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 43 +#define VERSION_PATCH 44 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 99c8f382..219cb92e 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -498,6 +498,7 @@ class Communication : public CommQueue<> { for (uint8_t i = 0; i < rec->length; i++) { q->iv->addValue(i, mPayload, rec); } + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; q->iv->rssi = rssi; q->iv->doCalculations(); diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 1d38c23f..a5834d9a 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -186,28 +186,33 @@ class Inverter { if(mDevControlRequest) { cb(devControlCmd, true); mDevControlRequest = false; - } else if (IV_MI != ivGen) { + } else if (IV_MI != ivGen) { // HM / HMS / HMT mGetLossInterval++; - if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) - cb(AlarmData, false); // get last alarms - else if(0 == getFwVersion()) - cb(InverterDevInform_All, false); // get firmware version - else if(0 == getHwVersion()) - cb(InverterDevInform_Simple, false); // get hardware version - else if(actPowerLimit == 0xffff) - cb(SystemConfigPara, false); // power limit info - else if(InitDataState != devControlCmd) { - cb(devControlCmd, false); // custom command which was received by API - devControlCmd = InitDataState; - mGetLossInterval = 1; - } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile - cb(GridOnProFilePara, false); - } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate - mGetLossInterval = 1; - cb(GetLossRate, false); - } else + if(mNextLive) cb(RealTimeRunData_Debug, false); // get live data - } else { + else { + mNextLive = true; + if(actPowerLimit == 0xffff) + cb(SystemConfigPara, false); // power limit info + else if(InitDataState != devControlCmd) { + cb(devControlCmd, false); // custom command which was received by API + devControlCmd = InitDataState; + mGetLossInterval = 1; + } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) + cb(AlarmData, false); // get last alarms + else if(0 == getFwVersion()) + cb(InverterDevInform_All, false); // get firmware version + else if(0 == getHwVersion()) + cb(InverterDevInform_Simple, false); // get hardware version + else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile + cb(GridOnProFilePara, false); + } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate + mGetLossInterval = 1; + cb(GetLossRate, false); + } else + cb(RealTimeRunData_Debug, false); // get live data + } + } else { // MI if(0 == getFwVersion()) cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number else { @@ -319,10 +324,10 @@ class Inverter { rec->record[pos] = (REC_TYP)(val); } } - rec->mqttSentStatus = MqttSentStatus::NEW_DATA; } if(rec == &recordMeas) { + mNextLive = false; // live data received DPRINTLN(DBG_VERBOSE, "add real time"); // get last alarm message index and save it in the inverter object if (getPosByChFld(0, FLD_EVT, rec) == pos) { @@ -832,6 +837,7 @@ class Inverter { uint16_t mIvRxCnt = 0; uint16_t mIvTxCnt = 0; uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer) + bool mNextLive = true; // first read live data after booting up then version etc. public: uint16_t mDtuRxCnt = 0; From 617cf0a92a3e303000179e6946b018da4f3d6d09 Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 5 Jan 2024 17:19:53 +0100 Subject: [PATCH 024/115] 0.8.45 * fix MqTT total values #1326 --- src/CHANGES.md | 5 ++- src/defines.h | 2 +- src/publisher/pubMqttIvData.h | 75 ++++++++++++++++++----------------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 9dc10a72..ab0963d7 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,8 +1,11 @@ # Development Changes +## 0.8.45 - 2024-01-05 +* fix MqTT total values #1326 + ## 0.8.44 - 2024-01-05 * fix MqTT transmission of data #1326 -* live data is read much earlier / faster and more often +* live data is read much earlier / faster and more often #1272 ## 0.8.43 - 2024-01-04 * fix display of sunrise in `/system` #1308 diff --git a/src/defines.h b/src/defines.h index f0c9731a..4d3d7fdd 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 44 +#define VERSION_PATCH 45 //------------------------------------- typedef struct { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 61344ede..a7deadfb 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -133,55 +133,56 @@ class PubMqttIvData { return; } - if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { - if(mPos < rec->length) { - bool retained = false; - if (mCmd == RealTimeRunData_Debug) { - if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId)) - retained = true; - - // calculate total values for RealTimeRunData_Debug - if (CH0 == rec->assign[mPos].ch) { - if(mIv->getStatus() > InverterStatus::STARTING) { - if(mIv->config->add2Total) { - mTotalFound = true; - switch (rec->assign[mPos].fieldId) { - case FLD_PAC: - mTotal[0] += mIv->getValue(mPos, rec); - break; - case FLD_YT: - mTotal[1] += mIv->getValue(mPos, rec); - break; - case FLD_YD: { - float val = mIv->getValue(mPos, rec); - if(0 == val) // inverter restarted during day - mSendTotalYd = false; - else - mTotal[2] += val; - break; - } - case FLD_PDC: - mTotal[3] += mIv->getValue(mPos, rec); - break; + if(mPos < rec->length) { + bool retained = false; + if (mCmd == RealTimeRunData_Debug) { + if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId)) + retained = true; + + // calculate total values for RealTimeRunData_Debug + if (CH0 == rec->assign[mPos].ch) { + if(mIv->getStatus() > InverterStatus::STARTING) { + if(mIv->config->add2Total) { + mTotalFound = true; + switch (rec->assign[mPos].fieldId) { + case FLD_PAC: + mTotal[0] += mIv->getValue(mPos, rec); + break; + case FLD_YT: + mTotal[1] += mIv->getValue(mPos, rec); + break; + case FLD_YD: { + float val = mIv->getValue(mPos, rec); + if(0 == val) // inverter restarted during day + mSendTotalYd = false; + else + mTotal[2] += val; + break; } + case FLD_PDC: + mTotal[3] += mIv->getValue(mPos, rec); + break; } - } else - mAllTotalFound = false; - } + } + } else + mAllTotalFound = false; } + } + if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); mPublish(mSubTopic, mVal, retained, qos); - mPos++; - } else { + } + mPos++; + } else { + if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { sendRadioStat(rec->length); rec->mqttSentStatus = MqttSentStatus::DATA_SENT; - mState = FIND_NXT_IV; } - } else mState = FIND_NXT_IV; + } } inline void sendRadioStat(uint8_t start) { From 5ca26895a1147417411eb59b9fd1ea9482ef3c12 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sat, 6 Jan 2024 02:58:19 +0100 Subject: [PATCH 025/115] 0.8.45 * start implementing a wizard for initial (WiFi) configuration #1199 --- src/CHANGES.md | 1 + src/app.h | 12 ++++++ src/appInterface.h | 3 ++ src/web/RestApi.h | 12 +++++- src/web/html/style.css | 17 ++++++++ src/web/html/wizard.html | 87 ++++++++++++++++++++++++++++++++++++++++ src/web/web.h | 11 ++++- src/wifi/ahoywifi.cpp | 15 ++++--- src/wifi/ahoywifi.h | 9 ++++- 9 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 src/web/html/wizard.html diff --git a/src/CHANGES.md b/src/CHANGES.md index ab0963d7..34f9aacc 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,7 @@ ## 0.8.45 - 2024-01-05 * fix MqTT total values #1326 +* start implementing a wizard for initial (WiFi) configuration #1199 ## 0.8.44 - 2024-01-05 * fix MqTT transmission of data #1326 diff --git a/src/app.h b/src/app.h index 8d6fafbe..41ff008b 100644 --- a/src/app.h +++ b/src/app.h @@ -150,6 +150,18 @@ class app : public IApp, public ah::Scheduler { return mWifi.getAvailNetworks(obj); } + void setupStation(void) { + mWifi.setupStation(); + } + + void setStopApAllowedMode(bool allowed) { + mWifi.setStopApAllowedMode(allowed); + } + + String getStationIp(void) { + return mWifi.getStationIp(); + } + #endif /* !defined(ETHERNET) */ void setRebootFlag() { diff --git a/src/appInterface.h b/src/appInterface.h index 9d633765..76c5e935 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -37,6 +37,9 @@ class IApp { #if !defined(ETHERNET) virtual void scanAvailNetworks() = 0; virtual bool getAvailNetworks(JsonObject obj) = 0; + virtual void setupStation(void) = 0; + virtual void setStopApAllowedMode(bool allowed) = 0; + virtual String getStationIp(void) = 0; #endif /* defined(ETHERNET) */ virtual uint32_t getUptime() = 0; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 2b341441..cea845a2 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -100,6 +100,7 @@ class RestApi { else if(path == "setup") getSetup(request, root); #if !defined(ETHERNET) else if(path == "setup/networks") getNetworks(root); + else if(path == "setup/getip") getWifiIp(root); #endif /* !defined(ETHERNET) */ else if(path == "live") getLive(request,root); else { @@ -754,6 +755,9 @@ class RestApi { void getNetworks(JsonObject obj) { mApp->getAvailNetworks(obj); } + void getWifiIp(JsonObject obj) { + obj[F("ip")] = mApp->getStationIp(); + } #endif /* !defined(ETHERNET) */ void getLive(AsyncWebServerRequest *request, JsonObject obj) { @@ -834,7 +838,13 @@ class RestApi { mTimezoneOffset = jsonIn[F("val")]; else if(F("discovery_cfg") == jsonIn[F("cmd")]) mApp->setMqttDiscoveryFlag(); // for homeassistant - else if(F("save_iv") == jsonIn[F("cmd")]) { + else if(F("save_wifi") == jsonIn[F("cmd")]) { + snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as()); + snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as()); + mApp->saveSettings(false); // without reboot + mApp->setStopApAllowedMode(false); + mApp->setupStation(); + } else if(F("save_iv") == jsonIn[F("cmd")]) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false); iv->config->enabled = jsonIn[F("en")]; iv->config->serial.u64 = jsonIn[F("ser")]; diff --git a/src/web/html/style.css b/src/web/html/style.css index e7de68f4..c9f9fbf7 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -814,3 +814,20 @@ ul { background-color: var(--input-bg); color: var(--fg); } + +.d-flex { + display: flex !important; +} + +.jc { + justify-content: center !important; +} + +.aic { + align-items: center !important; +} + +.container { + height: 100%; + overflow: auto; +} diff --git a/src/web/html/wizard.html b/src/web/html/wizard.html new file mode 100644 index 00000000..1ebfb65f --- /dev/null +++ b/src/web/html/wizard.html @@ -0,0 +1,87 @@ + + + + Setup Wizard + {#HTML_HEADER} + + +

+ + + diff --git a/src/web/web.h b/src/web/web.h index ea25e5ff..eae106a9 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -36,6 +36,7 @@ #include "html/h/update_html.h" #include "html/h/visualization_html.h" #include "html/h/about_html.h" +#include "html/h/wizard_html.h" #define WEB_SERIAL_BUF_SIZE 2048 @@ -77,6 +78,7 @@ class Web { mWeb.on("/factorytrue", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1)); mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); + mWeb.on("/wizard", HTTP_GET, std::bind(&Web::onWizard, 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)); @@ -422,7 +424,7 @@ class Web { void showNotFound(AsyncWebServerRequest *request) { checkProtection(request); - request->redirect("/setup"); + request->redirect("/wizard"); } void onReboot(AsyncWebServerRequest *request) { @@ -444,6 +446,13 @@ class Web { getPage(request, PROT_MASK_SETUP, setup_html, setup_html_len); } + void onWizard(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len); + response->addHeader(F("Content-Encoding"), "gzip"); + response->addHeader(F("content-type"), "text/html; charset=UTF-8"); + request->send(response); + } + void showSave(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("showSave")); diff --git a/src/wifi/ahoywifi.cpp b/src/wifi/ahoywifi.cpp index 02f8d252..4ab7ee92 100644 --- a/src/wifi/ahoywifi.cpp +++ b/src/wifi/ahoywifi.cpp @@ -40,6 +40,7 @@ void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) { mCnt = 0; mScanActive = false; mScanCnt = 0; + mStopApAllowed = true; #if defined(ESP8266) wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1)); @@ -94,7 +95,7 @@ void ahoywifi::tickWifiLoop() { #endif return; case IN_AP_MODE: - if (WiFi.softAPgetStationNum() == 0) { + if ((WiFi.softAPgetStationNum() == 0) || (!mStopApAllowed)) { mCnt = 0; mDns.stop(); WiFi.mode(WIFI_AP_STA); @@ -105,7 +106,7 @@ void ahoywifi::tickWifiLoop() { } break; case DISCONNECTED: - if (WiFi.softAPgetStationNum() > 0) { + if ((WiFi.softAPgetStationNum() > 0) && (mStopApAllowed)) { mStaConn = IN_AP_MODE; // first time switch to AP Mode if (mScanActive) { @@ -182,10 +183,12 @@ void ahoywifi::tickWifiLoop() { break; case GOT_IP: welcome(WiFi.localIP().toString(), F(" (Station)")); - WiFi.softAPdisconnect(); - WiFi.mode(WIFI_STA); - DBGPRINTLN(F("[WiFi] AP disabled")); - delay(100); + if(mStopApAllowed) { + WiFi.softAPdisconnect(); + WiFi.mode(WIFI_STA); + DBGPRINTLN(F("[WiFi] AP disabled")); + delay(100); + } mAppWifiCb(true); mGotDisconnect = false; mStaConn = IN_STA_MODE; diff --git a/src/wifi/ahoywifi.h b/src/wifi/ahoywifi.h index e44d6858..36837600 100644 --- a/src/wifi/ahoywifi.h +++ b/src/wifi/ahoywifi.h @@ -28,6 +28,13 @@ class ahoywifi { bool getNtpTime(void); void scanAvailNetworks(void); bool getAvailNetworks(JsonObject obj); + void setStopApAllowedMode(bool allowed) { + mStopApAllowed = allowed; + } + String getStationIp(void) { + return WiFi.localIP().toString(); + } + void setupStation(void); private: typedef enum WiFiStatus { @@ -43,7 +50,6 @@ class ahoywifi { void setupWifi(bool startAP); void setupAp(void); - void setupStation(void); void sendNTPpacket(IPAddress& address); void sortRSSI(int *sort, int n); bool getBSSIDs(void); @@ -78,6 +84,7 @@ class ahoywifi { bool mScanActive; bool mGotDisconnect; std::list mBSSIDList; + bool mStopApAllowed; }; #endif /*__AHOYWIFI_H__*/ From 8156cd9d30685ee661134f707857b294f17767df Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 6 Jan 2024 09:23:29 +0100 Subject: [PATCH 026/115] PROMETHEUS_EP: Update documentation acc. to v0.8.42 --- doc/prometheus_ep_description.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 6b4ce56c..4b266da4 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -34,10 +34,10 @@ Prometheus metrics provided at `/metrics`. | `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter | -| `ahoy_solar_inverter_radio_iv_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | -| `ahoy_solar_inverter_radio_iv_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | -| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | -| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_loss_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_sent_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_loss_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_sent_cnt` | Counter | NRF24 statistic of inverter | 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 | From 1aa14ebf0871d89b3f5527ac870d702ea0181d5c Mon Sep 17 00:00:00 2001 From: lumapu Date: Sat, 6 Jan 2024 10:07:41 +0100 Subject: [PATCH 027/115] 0.8.45 fix compile --- src/web/RestApi.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/web/RestApi.h b/src/web/RestApi.h index cea845a2..de4fe8e3 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -838,13 +838,16 @@ class RestApi { mTimezoneOffset = jsonIn[F("val")]; else if(F("discovery_cfg") == jsonIn[F("cmd")]) mApp->setMqttDiscoveryFlag(); // for homeassistant + #if !defined(ETHERNET) else if(F("save_wifi") == jsonIn[F("cmd")]) { snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as()); snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as()); mApp->saveSettings(false); // without reboot mApp->setStopApAllowedMode(false); mApp->setupStation(); - } else if(F("save_iv") == jsonIn[F("cmd")]) { + } + #endif /* !defined(ETHERNET */ + else if(F("save_iv") == jsonIn[F("cmd")]) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false); iv->config->enabled = jsonIn[F("en")]; iv->config->serial.u64 = jsonIn[F("ser")]; From dd225bdf79878baacef641aadee35ce1e84071b0 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sat, 6 Jan 2024 14:05:49 +0100 Subject: [PATCH 028/115] 0.8.46 * improved communication --- src/CHANGES.md | 3 ++ src/defines.h | 2 +- src/hm/Communication.h | 2 +- src/hm/hmInverter.h | 48 ++++++++++++++---------------- src/plugins/Display/Display_data.h | 26 ++++++++-------- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 34f9aacc..9313dbee 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.46 - 2024-01-06 +* improved communication + ## 0.8.45 - 2024-01-05 * fix MqTT total values #1326 * start implementing a wizard for initial (WiFi) configuration #1199 diff --git a/src/defines.h b/src/defines.h index 4d3d7fdd..fcd3bbf7 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 45 +#define VERSION_PATCH 46 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 219cb92e..f69d19c7 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -118,7 +118,7 @@ class Communication : public CommQueue<> { mIsRetransmit = false; setAttempt(); if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) - incrAttempt(q->cmd == AlarmData? 5 : 3); + incrAttempt(15); mState = States::WAIT; break; diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index a5834d9a..4e714cfd 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -191,19 +191,18 @@ class Inverter { if(mNextLive) cb(RealTimeRunData_Debug, false); // get live data else { - mNextLive = true; if(actPowerLimit == 0xffff) cb(SystemConfigPara, false); // power limit info else if(InitDataState != devControlCmd) { cb(devControlCmd, false); // custom command which was received by API devControlCmd = InitDataState; mGetLossInterval = 1; - } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) - cb(AlarmData, false); // get last alarms - else if(0 == getFwVersion()) + } else if(0 == getFwVersion()) cb(InverterDevInform_All, false); // get firmware version else if(0 == getHwVersion()) cb(InverterDevInform_Simple, false); // get hardware version + else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) + cb(AlarmData, false); // get last alarms else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile cb(GridOnProFilePara, false); } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate @@ -339,28 +338,27 @@ class Inverter { } } } - else if (rec->assign == InfoAssignment) { - DPRINTLN(DBG_DEBUG, "add info"); - // eg. fw version ... - isConnected = true; - } - else if (rec->assign == SimpleInfoAssignment) { - DPRINTLN(DBG_DEBUG, "add simple info"); - // eg. hw version ... - } - else if (rec->assign == SystemConfigParaAssignment) { - DPRINTLN(DBG_DEBUG, "add config"); - if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ - actPowerLimit = rec->record[pos]; - DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); - DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); - } - } - else if (rec->assign == AlarmDataAssignment) { - DPRINTLN(DBG_DEBUG, "add alarm"); + else { + mNextLive = true; + if (rec->assign == InfoAssignment) { + DPRINTLN(DBG_DEBUG, "add info"); + // eg. fw version ... + isConnected = true; + } else if (rec->assign == SimpleInfoAssignment) { + DPRINTLN(DBG_DEBUG, "add simple info"); + // eg. hw version ... + } else if (rec->assign == SystemConfigParaAssignment) { + DPRINTLN(DBG_DEBUG, "add config"); + if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ + actPowerLimit = rec->record[pos]; + DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); + DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); + } + } else if (rec->assign == AlarmDataAssignment) { + DPRINTLN(DBG_DEBUG, "add alarm"); + } else + DPRINTLN(DBG_WARN, F("add with unknown assignment")); } - else - DPRINTLN(DBG_WARN, F("add with unknown assignment")); } else DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h index a400377d..a7a7ecee 100644 --- a/src/plugins/Display/Display_data.h +++ b/src/plugins/Display/Display_data.h @@ -4,19 +4,19 @@ #define __DISPLAY_DATA__ struct DisplayData { - const char *version=nullptr; - float totalPower=0.0f; // indicate current power (W) - float totalYieldDay=0.0f; // indicate day yield (Wh) - float totalYieldTotal=0.0f; // indicate total yield (kWh) - uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized - uint8_t nrProducing=0; // indicate number of producing inverters - uint8_t nrSleeping=0; // indicate number of sleeping inverters - bool WifiSymbol = false; // indicate if WiFi is connected - bool RadioSymbol = false; // indicate if radio module is connecting and working - bool MQTTSymbol = false; // indicate if MQTT is connected - int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi - int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio - IPAddress ipAddress; // indicate ip adress of ahoy + const char *version=nullptr; + float totalPower=0.0f; // indicate current power (W) + float totalYieldDay=0.0f; // indicate day yield (Wh) + float totalYieldTotal=0.0f; // indicate total yield (kWh) + uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized + uint8_t nrProducing=0; // indicate number of producing inverters + uint8_t nrSleeping=0; // indicate number of sleeping inverters + bool WifiSymbol = false; // indicate if WiFi is connected + bool RadioSymbol = false; // indicate if radio module is connecting and working + bool MQTTSymbol = false; // indicate if MQTT is connected + int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi + int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio + IPAddress ipAddress; // indicate ip adress of ahoy }; #endif /*__DISPLAY_DATA__*/ From 7a34b7e6167d8d62984371d71ce61cdf280c66a8 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sat, 6 Jan 2024 17:45:53 +0100 Subject: [PATCH 029/115] 0.8.47 * reduce GxEPD2 lib to compile faster * upgraded GxEPD2 lib to `1.5.3` * updated espressif32 platform to `6.5.0` * updated U8g2 to `2.35.9` * started to convert deprecated functions of new ArduinoJson `7.0.0` --- patches/GxEPD2_SW_SPI.patch | 12 ++++---- scripts/reduceGxEPD2.py | 41 ++++++++++++++++++++++++++++ src/CHANGES.md | 7 +++++ src/config/settings.h | 20 +++++++------- src/defines.h | 2 +- src/platformio.ini | 32 +++++++++++----------- src/plugins/Display/Display_ePaper.h | 2 -- 7 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 scripts/reduceGxEPD2.py diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch index 9697eec8..dc3fa9ca 100644 --- a/patches/GxEPD2_SW_SPI.patch +++ b/patches/GxEPD2_SW_SPI.patch @@ -1,5 +1,5 @@ diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp -index 1588444..592869b 100644 +index 8df8bef..91d7f49 100644 --- a/src/GxEPD2_EPD.cpp +++ b/src/GxEPD2_EPD.cpp @@ -19,9 +19,9 @@ @@ -71,7 +71,7 @@ index 1588444..592869b 100644 void GxEPD2_EPD::_reset() { if (_rst >= 0) -@@ -174,115 +169,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) +@@ -174,115 +171,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) void GxEPD2_EPD::_writeCommand(uint8_t c) { @@ -304,7 +304,7 @@ index 1588444..592869b 100644 + _endTransaction(); } diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h -index ef2318f..50aa961 100644 +index 34c1145..c480b7d 100644 --- a/src/GxEPD2_EPD.h +++ b/src/GxEPD2_EPD.h @@ -8,6 +8,10 @@ @@ -334,7 +334,7 @@ index ef2318f..50aa961 100644 protected: void _reset(); void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); -@@ -111,9 +115,14 @@ class GxEPD2_EPD +@@ -111,17 +115,22 @@ class GxEPD2_EPD void _startTransfer(); void _transfer(uint8_t value); void _endTransfer(); @@ -351,7 +351,9 @@ index ef2318f..50aa961 100644 bool _diag_enabled, _pulldown_rst_mode; - SPIClass* _pSPIx; SPISettings _spi_settings; -@@ -123,5 +124,5 @@ class GxEPD2_EPD + bool _initial_write, _initial_refresh; + bool _power_is_on, _using_partial_mode, _hibernating; + bool _init_display_done; uint16_t _reset_duration; - void (*_busy_callback)(const void*); + void (*_busy_callback)(const void*); diff --git a/scripts/reduceGxEPD2.py b/scripts/reduceGxEPD2.py new file mode 100644 index 00000000..f05e5c3b --- /dev/null +++ b/scripts/reduceGxEPD2.py @@ -0,0 +1,41 @@ +import os +import subprocess +import glob +Import("env") + +def rmDirWithFiles(path): + if os.path.isdir(path): + for f in glob.glob(path + "/*"): + os.remove(f) + os.rmdir(path) + +def clean(libName): + # save current wd + start = os.getcwd() + + if os.path.exists('.pio/libdeps/' + env['PIOENV'] + '/' + libName) == False: + print("path '" + '.pio/libdeps/' + env['PIOENV'] + '/' + libName + "' does not exist") + return + + os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName) + os.chdir('src/') + types = ('epd/*.h', 'epd/*.cpp') # the tuple of file types + files = [] + for t in types: + files.extend(glob.glob(t)) + + for f in files: + if f.count('GxEPD2_150_BN') == 0: + os.remove(f) + + rmDirWithFiles("epd3c") + rmDirWithFiles("epd4c") + rmDirWithFiles("epd7c") + rmDirWithFiles("gdeq") + rmDirWithFiles("gdey") + rmDirWithFiles("it8951") + + os.chdir(start) + + +clean("GxEPD2") diff --git a/src/CHANGES.md b/src/CHANGES.md index 9313dbee..e91179f7 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,12 @@ # Development Changes +## 0.8.47 - 2024-01-06 +* reduce GxEPD2 lib to compile faster +* upgraded GxEPD2 lib to `1.5.3` +* updated espressif32 platform to `6.5.0` +* updated U8g2 to `2.35.9` +* started to convert deprecated functions of new ArduinoJson `7.0.0` + ## 0.8.46 - 2024-01-06 * improved communication diff --git a/src/config/settings.h b/src/config/settings.h index 476f9fbf..8ce71167 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -308,18 +308,18 @@ class settings { DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); JsonObject root = json.to(); json[F("version")] = CONFIG_VERSION; - jsonNetwork(root.createNestedObject(F("wifi")), true); - jsonNrf(root.createNestedObject(F("nrf")), true); + jsonNetwork(root[F("wifi")].to(), true); + jsonNrf(root[F("nrf")].to(), true); #if defined(ESP32) - jsonCmt(root.createNestedObject(F("cmt")), true); + jsonCmt(root[F("cmt")].to(), true); #endif - 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); + jsonNtp(root[F("ntp")].to(), true); + jsonSun(root[F("sun")].to(), true); + jsonSerial(root[F("serial")].to(), true); + jsonMqtt(root[F("mqtt")].to(), true); + jsonLed(root[F("led")].to(), true); + jsonPlugin(root[F("plugin")].to(), true); + jsonInst(root[F("inst")].to(), true); DPRINT(DBG_INFO, F("memory usage: ")); DBGPRINTLN(String(json.memoryUsage())); diff --git a/src/defines.h b/src/defines.h index fcd3bbf7..87b4101d 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 46 +#define VERSION_PATCH 47 //------------------------------------- typedef struct { diff --git a/src/platformio.ini b/src/platformio.ini index 2b4b25cf..9b204f2e 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -22,6 +22,7 @@ extra_scripts = pre:../scripts/auto_firmware_version.py pre:../scripts/convertHtml.py pre:../scripts/applyPatches.py + pre:../scripts/reduceGxEPD2.py lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer @@ -30,8 +31,8 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.5.0 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.7 - https://github.com/zinggjm/GxEPD2 @ ^1.5.2 + olikraus/U8g2 @ ^2.35.9 + https://github.com/zinggjm/GxEPD2 @ ^1.5.3 build_flags = -std=c++17 -std=gnu++17 @@ -71,7 +72,7 @@ monitor_filters = esp8266_exception_decoder [env:esp32-wroom32] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD @@ -79,7 +80,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-prometheus] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD @@ -98,8 +99,8 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.5.0 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.7 - zinggjm/GxEPD2 @ ^1.5.2 + olikraus/U8g2 @ ^2.35.9 + https://github.com/zinggjm/GxEPD2 @ ^1.5.3 build_flags = ${env.build_flags} -D ETHERNET -DRELEASE @@ -110,7 +111,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-s2-mini] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = lolin_s2_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD @@ -124,7 +125,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-c3-mini] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = lolin_c3_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD @@ -139,7 +140,7 @@ monitor_filters = [env:opendtufusion] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin build_flags = ${env.build_flags} @@ -162,7 +163,7 @@ monitor_filters = esp32_exception_decoder, colorize [env:opendtufusion-ethernet] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 lib_deps = khoih-prog/AsyncWebServer_ESP32_W5500 @@ -172,13 +173,12 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.5.0 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.7 - zinggjm/GxEPD2 @ ^1.5.2 + olikraus/U8g2 @ ^2.35.9 + https://github.com/zinggjm/GxEPD2 @ ^1.5.3 upload_protocol = esp-builtin build_flags = ${env.build_flags} -DETHERNET -DSPI_HAL - -DUSE_HSPI_FOR_EPD -DDEF_ETH_CS_PIN=42 -DDEF_ETH_SCK_PIN=39 -DDEF_ETH_MISO_PIN=41 @@ -205,7 +205,7 @@ monitor_filters = esp32_exception_decoder, colorize [env:opendtufusion-dev] -platform = espressif32@6.4.0 +platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer @@ -214,8 +214,8 @@ lib_deps = https://github.com/bertmelis/espMqttClient#v1.5.0 bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.7 - https://github.com/zinggjm/GxEPD2 @ ^1.5.2 + olikraus/U8g2 @ ^2.35.9 + https://github.com/zinggjm/GxEPD2 @ ^1.5.3 upload_protocol = esp-builtin build_flags = ${env.build_flags} -DDEF_NRF_CS_PIN=37 diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index c9a0fbf5..0e077601 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -9,11 +9,9 @@ // enable GxEPD2_GFX base class #define ENABLE_GxEPD2_GFX 1 -#include #include #include -#include // FreeFonts from Adafruit_GFX #include #include From aa6a022214a5a83e33c3f0d7b2dc1bee8301ba33 Mon Sep 17 00:00:00 2001 From: Patrick Amrhein Date: Sat, 6 Jan 2024 19:45:18 +0100 Subject: [PATCH 030/115] Add defines for Retry ATTEMPS --- src/hm/CommQueue.h | 18 ++++++++++++++---- src/hm/Communication.h | 12 ++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h index bb815a7e..eb770bb0 100644 --- a/src/hm/CommQueue.h +++ b/src/hm/CommQueue.h @@ -11,6 +11,10 @@ #include "hmInverter.h" #include "../utils/dbg.h" +#define DEFAULT_ATTEMPS 10 +#define MORE_ATTEMPS_ALARMDATA 15 +#define MORE_ATTEMPS_GRIDONPROFILEPARA 15 + template class CommQueue { public: @@ -44,11 +48,12 @@ class CommQueue { Inverter<> *iv; uint8_t cmd; uint8_t attempts; + uint8_t attemptsMax; uint32_t ts; bool isDevControl; queue_s() {} queue_s(Inverter<> *i, uint8_t c, bool dev) : - iv(i), cmd(c), attempts(5), ts(0), isDevControl(dev) {} + iv(i), cmd(c), attempts(DEFAULT_ATTEMPS), attemptsMax(DEFAULT_ATTEMPS), ts(0), isDevControl(dev) {} }; protected: @@ -59,8 +64,10 @@ class CommQueue { void add(const queue_s *q, bool rstAttempts = false) { mQueue[mWrPtr] = *q; - if(rstAttempts) - mQueue[mWrPtr].attempts = 5; + if(rstAttempts) { + mQueue[mWrPtr].attempts = DEFAULT_ATTEMPS; + mQueue[mWrPtr].attemptsMax = DEFAULT_ATTEMPS; + } inc(&mWrPtr); } @@ -79,7 +86,8 @@ class CommQueue { void cmdDone(bool keep = false) { if(keep) { - mQueue[mRdPtr].attempts = 5; + mQueue[mRdPtr].attempts = DEFAULT_ATTEMPS; + mQueue[mRdPtr].attemptsMax = DEFAULT_ATTEMPS; add(mQueue[mRdPtr]); // add to the end again } inc(&mRdPtr); @@ -96,6 +104,8 @@ class CommQueue { void incrAttempt(uint8_t attempts = 1) { mQueue[mRdPtr].attempts += attempts; + if (mQueue[mRdPtr].attempts > mQueue[mRdPtr].attemptsMax) + mQueue[mRdPtr].attemptsMax = mQueue[mRdPtr].attempts; } void inc(uint8_t *ptr) { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index f69d19c7..3c7c82a5 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -118,8 +118,8 @@ class Communication : public CommQueue<> { mIsRetransmit = false; setAttempt(); if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) - incrAttempt(15); - + incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA); +/// statt 5:3 mState = States::WAIT; break; @@ -194,7 +194,7 @@ class Communication : public CommQueue<> { return; } } else { - mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt); + mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); if(((q->cmd == 0x39) && (q->iv->type == INV_TYPE_4CH)) || ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH)) || ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) { @@ -529,7 +529,7 @@ class Communication : public CommQueue<> { private: void closeRequest(const queue_s *q, bool crcPass) { - mHeu.evalTxChQuality(q->iv, crcPass, (4 - q->attempts), q->iv->curFrmCnt); + mHeu.evalTxChQuality(q->iv, crcPass, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); if(crcPass) q->iv->radioStatistics.rxSuccess++; else if(q->iv->mGotFragment) @@ -730,7 +730,7 @@ class Communication : public CommQueue<> { miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]); if (p->packet[0] < (0x39 + ALL_FRAMES) ) { - mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), 1); + mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), 1); miNextRequest((p->packet[0] - ALL_FRAMES + 1), q); } else { q->iv->miMultiParts = 7; // indicate we are ready @@ -739,7 +739,7 @@ class Communication : public CommQueue<> { } else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) { //addImportant(q->iv, MI_REQ_CH2); miNextRequest(MI_REQ_CH2, q); - mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt); + mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); //use also miMultiParts here for better statistics? //mHeu.setGotFragment(q->iv); } else { // first data msg for 1ch, 2nd for 2ch From b765916a2af3f0229eeae1da8d93277566c6dfb4 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 02:36:19 +0100 Subject: [PATCH 031/115] 0.8.47 * started to have german translations of all variants (environments) #925 #1199 --- .github/workflows/compile_development.yml | 23 +- scripts/convertHtml.py | 39 +- scripts/getVersion.py | 54 + src/CHANGES.md | 1 + src/config/config.h | 4 +- src/platformio.ini | 144 ++- src/web/html/includes/header.html | 2 +- src/web/html/includes/nav.html | 10 +- src/web/html/index.html | 53 +- src/web/html/save.html | 8 +- src/web/html/setup.html | 288 +++-- src/web/html/system.html | 36 +- src/web/html/update.html | 8 +- src/web/html/visualization.html | 136 +-- src/web/html/wizard.html | 28 +- src/web/lang.json | 1334 +++++++++++++++++++++ 16 files changed, 1869 insertions(+), 299 deletions(-) create mode 100644 src/web/lang.json diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 6eba4879..4b688fca 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -43,7 +43,28 @@ jobs: pip install --upgrade platformio - name: Run PlatformIO - run: pio run -d src --environment esp8266 --environment esp8266-prometheus --environment esp8285 --environment esp32-wroom32 --environment esp32-wroom32-prometheus --environment esp32-wroom32-ethernet --environment esp32-s2-mini --environment esp32-c3-mini --environment opendtufusion --environment opendtufusion-ethernet + run: > + pio run -d src + --environment esp8266 + --environment esp8266-prometheus + --environment esp8285 + --environment esp32-wroom32 + --environment esp32-wroom32-prometheus + --environment esp32-wroom32-ethernet + --environment esp32-s2-mini + --environment esp32-c3-mini + --environment opendtufusion + --environment opendtufusion-ethernet + --environment esp8266-de + --environment esp8266-prometheus-de + --environment esp8285-de + --environment esp32-wroom32-de + --environment esp32-wroom32-prometheus-de + --environment esp32-wroom32-ethernet-de + --environment esp32-s2-mini-de + --environment esp32-c3-mini-de + --environment opendtufusion-de + --environment opendtufusion-ethernet-de - name: Copy boot_app0.bin run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py index 6eaa92a3..4e01a875 100644 --- a/scripts/convertHtml.py +++ b/scripts/convertHtml.py @@ -3,6 +3,7 @@ import os import gzip import glob import shutil +import json from datetime import date from pathlib import Path import subprocess @@ -33,7 +34,7 @@ def readVersion(path): ver += line[p+13:].rstrip() + "." return ver[:-1] -def htmlParts(file, header, nav, footer, version): +def htmlParts(file, header, nav, footer, version, lang): p = ""; f = open(file, "r") lines = f.readlines() @@ -64,6 +65,8 @@ def htmlParts(file, header, nav, footer, version): # remove if - endif ESP32 p = checkIf(p) + p = translate(file, p, lang) + p = translate("general", p, lang) # menu / header / footer f = open("tmp/" + file, "w") f.write(p); @@ -94,7 +97,30 @@ def checkIf(data): return data -def convert2Header(inFile, version): +def findLang(file): + with open('../lang.json') as j: + lang = json.load(j) + + for l in lang["files"]: + if l["name"] == file: + return l + + return None + +def translate(file, data, lang="de"): + json = findLang(file) + + if None != json: + matches = re.findall(r'\{\#([A-Z0-9_]+)\}', data) + for x in matches: + for e in json["list"]: + if x == e["token"]: + #print("replace " + "{#" + x + "}" + " with " + e[lang]) + data = data.replace("{#" + x + "}", e[lang]) + return data + + +def convert2Header(inFile, version, lang): fileType = inFile.split(".")[1] define = inFile.split(".")[0].upper() define2 = inFile.split(".")[1].upper() @@ -114,7 +140,7 @@ def convert2Header(inFile, version): f.close() else: if fileType == "html": - data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version) + data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version, lang) else: f = open(inFile, "r") data = f.read() @@ -169,6 +195,11 @@ Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with a shutil.copyfile("style.css", "tmp/style.css") version = readVersion("../../defines.h") +# get language from environment +lang = "en" +if env['PIOENV'][-3:] == "-de": + lang = "de" + # go throw the array for val in files_grabbed: - convert2Header(val, version) + convert2Header(val, version, lang) diff --git a/scripts/getVersion.py b/scripts/getVersion.py index cdb39ae8..ce34d26e 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -59,6 +59,7 @@ def readVersion(path, infile): os.mkdir(path + "firmware/ESP32-S3-ETH/") sha = os.getenv("SHA",default="sha") +## ENGLISH VERSIONS versionout = version[:-1] + "_" + sha + "_esp8266.bin" src = path + ".pio/build/esp8266/firmware.bin" dst = path + "firmware/ESP8266/" + versionout @@ -110,6 +111,59 @@ def readVersion(path, infile): dst = path + "firmware/ESP32-S3-ETH/" + versionout os.rename(src, dst) +## GERMAN VERSIONS + versionout = version[:-1] + "_" + sha + "_esp8266-de.bin" + src = path + ".pio/build/esp8266-de/firmware.bin" + dst = path + "firmware/ESP8266/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp8266_prometheus-de.bin" + src = path + ".pio/build/esp8266-prometheus-de/firmware.bin" + dst = path + "firmware/ESP8266/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp8285-de.bin" + src = path + ".pio/build/esp8285-de/firmware.bin" + dst = path + "firmware/ESP8285/" + versionout + os.rename(src, dst) + gzip_bin(dst, dst + ".gz") + + versionout = version[:-1] + "_" + sha + "_esp32-de.bin" + src = path + ".pio/build/esp32-wroom32-de/firmware.bin" + dst = path + "firmware/ESP32/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32_prometheus-de.bin" + src = path + ".pio/build/esp32-wroom32-prometheus-de/firmware.bin" + dst = path + "firmware/ESP32/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32_ethernet-de.bin" + src = path + ".pio/build/esp32-wroom32-ethernet-de/firmware.bin" + dst = path + "firmware/ESP32/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32s2-mini-de.bin" + src = path + ".pio/build/esp32-s2-mini-de/firmware.bin" + dst = path + "firmware/ESP32-S2/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32c3-mini-de.bin" + src = path + ".pio/build/esp32-c3-mini-de/firmware.bin" + dst = path + "firmware/ESP32-C3/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32s3-de.bin" + src = path + ".pio/build/opendtufusion-de/firmware.bin" + dst = path + "firmware/ESP32-S3/" + versionout + os.rename(src, dst) + + versionout = version[:-1] + "_" + sha + "_esp32s3_ethernet-de.bin" + src = path + ".pio/build/opendtufusion-ethernet-de/firmware.bin" + dst = path + "firmware/ESP32-S3-ETH/" + versionout + os.rename(src, dst) + +## BOOTLOADER AND PARTITIONS # other ESP32 bin files src = path + ".pio/build/esp32-wroom32/" dst = path + "firmware/ESP32/" diff --git a/src/CHANGES.md b/src/CHANGES.md index e91179f7..c403f8b0 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -6,6 +6,7 @@ * updated espressif32 platform to `6.5.0` * updated U8g2 to `2.35.9` * started to convert deprecated functions of new ArduinoJson `7.0.0` +* started to have german translations of all variants (environments) #925 #1199 ## 0.8.46 - 2024-01-06 * improved communication diff --git a/src/config/config.h b/src/config/config.h index fb05a1cc..40cb5b76 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __CONFIG_H__ diff --git a/src/platformio.ini b/src/platformio.ini index 9b204f2e..e9eb282b 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -50,6 +50,16 @@ build_flags = ${env.build_flags} monitor_filters = esp8266_exception_decoder +[env:esp8266-de] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = ${env.build_flags} + -DEMC_MIN_FREE_MEMORY=4096 + -DLANG_DE + ;-Wl,-Map,output.map +monitor_filters = + esp8266_exception_decoder [env:esp8266-prometheus] platform = espressif8266 @@ -61,6 +71,17 @@ build_flags = ${env.build_flags} monitor_filters = esp8266_exception_decoder +[env:esp8266-prometheus-de] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = ${env.build_flags} + -DENABLE_PROMETHEUS_EP + -DEMC_MIN_FREE_MEMORY=4096 + -DLANG_DE +monitor_filters = + esp8266_exception_decoder + [env:esp8285] platform = espressif8266 board = esp8285 @@ -71,6 +92,17 @@ build_flags = ${env.build_flags} monitor_filters = esp8266_exception_decoder +[env:esp8285-de] +platform = espressif8266 +board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld +board_build.f_cpu = 80000000L +build_flags = ${env.build_flags} + -DEMC_MIN_FREE_MEMORY=4096 + -DLANG_DE +monitor_filters = + esp8266_exception_decoder + [env:esp32-wroom32] platform = espressif32@6.5.0 board = lolin_d32 @@ -79,6 +111,15 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder +[env:esp32-wroom32-de] +platform = espressif32@6.5.0 +board = lolin_d32 +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD + -DLANG_DE +monitor_filters = + esp32_exception_decoder + [env:esp32-wroom32-prometheus] platform = espressif32@6.5.0 board = lolin_d32 @@ -88,6 +129,16 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder +[env:esp32-wroom32-prometheus-de] +platform = espressif32@6.5.0 +board = lolin_d32 +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD + -DENABLE_PROMETHEUS_EP + -DLANG_DE +monitor_filters = + esp32_exception_decoder + [env:esp32-wroom32-ethernet] platform = espressif32 board = esp32dev @@ -110,6 +161,29 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder +[env:esp32-wroom32-ethernet-de] +platform = espressif32 +board = esp32dev +lib_deps = + khoih-prog/AsyncWebServer_ESP32_W5500 + khoih-prog/AsyncUDP_ESP32_W5500 + nrf24/RF24 @ ^1.4.8 + paulstoffregen/Time @ ^1.6.1 + https://github.com/bertmelis/espMqttClient#v1.5.0 + bblanchon/ArduinoJson @ ^6.21.3 + https://github.com/JChristensen/Timezone @ ^1.2.4 + olikraus/U8g2 @ ^2.35.9 + https://github.com/zinggjm/GxEPD2 @ ^1.5.3 +build_flags = ${env.build_flags} + -D ETHERNET + -DRELEASE + -DUSE_HSPI_FOR_EPD + -DLANG_DE + -DLOG_LOCAL_LEVEL=ESP_LOG_INFO + -DDEBUG_LEVEL=DBG_INFO +monitor_filters = + esp32_exception_decoder + [env:esp32-s2-mini] platform = espressif32@6.5.0 board = lolin_s2_mini @@ -124,6 +198,21 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder +[env:esp32-s2-mini-de] +platform = espressif32@6.5.0 +board = lolin_s2_mini +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD + -DDEF_NRF_CS_PIN=12 + -DDEF_NRF_CE_PIN=3 + -DDEF_NRF_IRQ_PIN=5 + -DDEF_NRF_MISO_PIN=9 + -DDEF_NRF_MOSI_PIN=11 + -DDEF_NRF_SCLK_PIN=7 + -DLANG_DE +monitor_filters = + esp32_exception_decoder + [env:esp32-c3-mini] platform = espressif32@6.5.0 board = lolin_c3_mini @@ -138,6 +227,20 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder +[env:esp32-c3-mini-de] +platform = espressif32@6.5.0 +board = lolin_c3_mini +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD + -DDEF_NRF_CS_PIN=5 + -DDEF_NRF_CE_PIN=0 + -DDEF_NRF_IRQ_PIN=1 + -DDEF_NRF_MISO_PIN=3 + -DDEF_NRF_MOSI_PIN=4 + -DDEF_NRF_SCLK_PIN=2 + -DLANG_DE +monitor_filters = + esp32_exception_decoder [env:opendtufusion] platform = espressif32@6.5.0 @@ -162,6 +265,30 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder, colorize +[env:opendtufusion-de] +platform = espressif32@6.5.0 +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +build_flags = ${env.build_flags} + -DDEF_NRF_CS_PIN=37 + -DDEF_NRF_CE_PIN=38 + -DDEF_NRF_IRQ_PIN=47 + -DDEF_NRF_MISO_PIN=48 + -DDEF_NRF_MOSI_PIN=35 + -DDEF_NRF_SCLK_PIN=36 + -DDEF_CMT_CSB=4 + -DDEF_CMT_FCSB=21 + -DDEF_CMT_IRQ=8 + -DDEF_CMT_SDIO=5 + -DDEF_CMT_SCLK=6 + -DDEF_LED0=18 + -DDEF_LED1=17 + -DLED_ACTIVE_HIGH + -DARDUINO_USB_MODE=1 + -DLANG_DE +monitor_filters = + esp32_exception_decoder, colorize + [env:opendtufusion-ethernet] platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 @@ -204,11 +331,12 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder, colorize -[env:opendtufusion-dev] +[env:opendtufusion-ethernet-de] platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer + khoih-prog/AsyncWebServer_ESP32_W5500 + khoih-prog/AsyncUDP_ESP32_W5500 https://github.com/nrf24/RF24 @ ^1.4.8 paulstoffregen/Time @ ^1.6.1 https://github.com/bertmelis/espMqttClient#v1.5.0 @@ -218,6 +346,14 @@ lib_deps = https://github.com/zinggjm/GxEPD2 @ ^1.5.3 upload_protocol = esp-builtin build_flags = ${env.build_flags} + -DETHERNET + -DSPI_HAL + -DDEF_ETH_CS_PIN=42 + -DDEF_ETH_SCK_PIN=39 + -DDEF_ETH_MISO_PIN=41 + -DDEF_ETH_MOSI_PIN=40 + -DDEF_ETH_IRQ_PIN=44 + -DDEF_ETH_RST_PIN=43 -DDEF_NRF_CS_PIN=37 -DDEF_NRF_CE_PIN=38 -DDEF_NRF_IRQ_PIN=47 @@ -233,7 +369,7 @@ build_flags = ${env.build_flags} -DDEF_LED1=17 -DLED_ACTIVE_HIGH -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 - -DSPI_HAL + #-DARDUINO_USB_CDC_ON_BOOT=1 + -DLANG_DE monitor_filters = esp32_exception_decoder, colorize diff --git a/src/web/html/includes/header.html b/src/web/html/includes/header.html index ab3b0545..c363304c 100644 --- a/src/web/html/includes/header.html +++ b/src/web/html/includes/header.html @@ -1,6 +1,6 @@ - + diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 91de5047..c05099b7 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -6,16 +6,16 @@
- Live - Webserial - Settings + {#NAV_LIVE} + {#NAV_WEBSERIAL} + {#NAV_SETTINGS} Update System REST API - Documentation - About + {#NAV_DOCUMENTATION} + {#NAV_ABOUT} Login Logout diff --git a/src/web/html/index.html b/src/web/html/index.html index cfdaeee6..6ad2c787 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -20,18 +20,15 @@

-

Support this project:

+

{#SUPPORT}:

-

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

@@ -46,11 +43,11 @@ function apiCb(obj) { var e = document.getElementById("apiResult"); if(obj.success) { - e.innerHTML = " command executed"; + e.innerHTML = " {#COMMAND_EXE}"; getAjax("/api/index", parse); } else - e.innerHTML = " Error: " + obj.error; + e.innerHTML = " {#ERROR}: " + obj.error; } function setTime() { @@ -92,7 +89,7 @@ else { dSpan.innerHTML = ""; var e = inp("set", "sync from browser", 0, ["btn"], "set", "button"); - dSpan.appendChild(span("NTP timeserver unreachable. ")); + dSpan.appendChild(span("{#NTP_UNREACH}. ")); dSpan.appendChild(e); dSpan.appendChild(span("", ["span"], "apiResult")); e.addEventListener("click", setTime); @@ -101,15 +98,15 @@ if(obj.disNightComm) { if(((obj.ts_sunrise + obj.ts_offsSr) < obj.ts_now) && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) { - commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); + commInfo = "{#POLLING_STOP} " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo = "Night time, inverter polling disabled, "; + commInfo = "{#NIGHT_TIME}, "; if(obj.ts_now > (obj.ts_sunrise + obj.ts_offsSr)) { - commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); + commInfo += "{#PAUSED_AT} " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo += "will start polling at " + (new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')); + commInfo += "{#START_AT} " + (new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')); } } } @@ -124,33 +121,33 @@ if(false == i["enabled"]) { icon = iconWarn; cl = "icon-warn"; - avail = "disabled"; + avail = "{#DISABLED}"; } else if((false == i["is_avail"]) || (0 == ts)) { icon = iconInfo; cl = "icon-info"; - avail = "not yet available"; + avail = "{#NOT_YET_AVAIL}"; } else if(0 == i["ts_last_success"]) { - avail = "available but no data was received until now"; + avail = "{#AVAIL_NO_DATA}"; } else { - avail = "available and is "; + avail = "{#AVAIL} "; if(false == i["is_producing"]) - avail += "not producing"; + avail += "{#NOT_PRODUCING}"; else { icon = iconSuccessFull; - avail += "producing " + i.cur_pwr + "W"; + avail += "{#PRODUCING} " + i.cur_pwr + "W"; } } p.append( svg(icon, 30, 30, "icon " + cl), - span("Inverter #" + i["id"] + ": " + i["name"] + " is " + avail), + span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail), br() ); if(false == i["is_avail"]) { if(i["ts_last_success"] > 0) { var date = new Date(i["ts_last_success"] * 1000); - p.append(span("-> last successful transmission: " + toIsoDateStr(date)), br()); + p.append(span("-> {#LAST_SUCCESS}: " + toIsoDateStr(date)), br()); } } } @@ -168,11 +165,11 @@ if(null != release) { if(getVerInt("{#VERSION}") < getVerInt(release)) - p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#UPDATE_AVAIL}: " + release), br()); else if(getVerInt("{#VERSION}") > getVerInt(release)) - p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#USING_DEV_VERSION} {#VERSION}. {#DEV_ISSUE_RELEASE_VERSION}: " + release), br()); else - p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br()); + p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#RELEASE_INSTALLED}: " + release), br()); } document.getElementById("warn_info").replaceChildren(p); diff --git a/src/web/html/save.html b/src/web/html/save.html index 9b5b4864..e5d5a67d 100644 --- a/src/web/html/save.html +++ b/src/web/html/save.html @@ -8,7 +8,7 @@ {#HTML_NAV}
-
Saving settings...
+
{#SAVE_SETTINGS}
{#HTML_FOOTER} @@ -31,15 +31,15 @@ var meta = document.createElement('meta'); meta.httpEquiv = "refresh" if(!obj.reboot) { - html = "Settings successfully saved. Automatic page reload in 3 seconds."; + html = "{#SUCCESS_SAVED_RELOAD}"; meta.content = 3; } else { - html = "Settings successfully saved. Rebooting. Automatic redirect in " + obj.reload + " seconds."; + html = "{#SUCCESS_SAVED_REBOOT} " + obj.reload + " {#SECONDS}."; meta.content = obj.reload + "; URL=/"; } document.getElementsByTagName('head')[0].appendChild(meta); } else { - html = "Failed saving settings."; + html = "{#FAILED_SAVE}."; } } document.getElementById("html").innerHTML = html; diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 782c23be..4903db16 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -1,7 +1,7 @@ - Setup + {#SETTINGS} {#HTML_HEADER} @@ -10,77 +10,74 @@
- +
- Device Host Name + {#DEVICE_NAME}
-
Device Name
+
{#DEVICE_NAME}
-
Reboot Ahoy at midnight
+
{#REBOOT_AT_MIDNIGHT}
-
Dark Mode
+
{#DARK_MODE}
-
(empty browser cache or use CTRL + F5 after reboot to apply this setting)
+
{#DARK_MODE_NOTE}
- System Config + {#PINOUT_CONFIGURATION}

Status LEDs

-

Radio (NRF24L01+)

+

{#RADIO} (NRF24L01+)

-

Radio (CMT2300A)

-
(ESP32 only)
+

{#RADIO} (CMT2300A)

+
-

Serial Console

+

{#SERIAL_CONSOLE}

-
print inverter data
+
{#LOG_PRINT_INVERTER_DATA}
-
Serial Debug
+
{#LOG_SERIAL_DEBUG}
-
Privacy Mode
+
{#LOG_PRIVACY_MODE}
-
Print whole traces in Log
+
{#LOG_PRINT_TRACES}
- +
WiFi
-
AP Password (min. length: 8)
+
{#AP_PWD}
- -

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

-
-
Search Networks
-
+
{#SEARCH_NETWORKS}
+
-
Avail Networks
+
{#AVAIL_NETWORKS}
@@ -89,26 +86,25 @@
-
SSID is hidden
+
{#SSID_HIDDEN}
-
Password
+
{#PASSWORD}
- Static IP (optional) + {#STATIC_IP}

- Leave fields blank for DHCP
- The following fields are parsed in this format: 192.168.4.1 + {#NETWORK_HINT_BLANK}

-
IP Address
+
{#IP_ADDRESS}
-
Submask
+
{#SUBMASK}
@@ -126,58 +122,58 @@
- +
- Protection + {#PROTECTION}
-
Admin Password
+
{#ADMIN_PASSWORD}
-

Select pages which should be protected by password

+

{#PROTECTION_NOTE}

- +
- Inverter + {#INVERTER}
-
Interval [s]
+
{#INTERVAL}
-
Inverter Gap [ms]
+
{#INV_GAP}
-
Reset values and YieldDay at midnight
+
{#INV_RESET_MIDNIGHT}
-
Reset values when inverter polling pauses at sunset
+
{#INV_PAUSE_SUNSET}
-
Reset values when inverter status is 'not available'
+
{#INV_RESET_NOT_AVAIL}
-
Reset 'max' values at midnight
+
{#INV_RESET_MAX_MIDNIGHT}
-
Start without time sync (useful in AP-Only-Mode)
+
{#INV_START_WITHOUT_TIME}
-
Read Grid Profile
+
{#INV_READ_GRID_PROFILE}
-
Yield Efficiency (Standard 1.0)
+
{#INV_YIELD_EFF}
@@ -196,42 +192,42 @@
-
NTP Interval (in Minutes, min. 5 Minutes)
+
{#NTP_INTERVAL}
-
set System time
+
{#NTP_SET_SYS_TIME}
- -
+ +
-
System Time
+
{#NTP_SYS_TIME}
- +
- Sunrise & Sunset + {#SUNRISE_SUNSET}
-
Latitude (decimal)
+
{#LATITUDE}
-
Longitude (decimal)
+
{#LONGITUDE}
-
Offset (sunrise)
+
{#OFFSET_SUNRISE}
-
Offset (sunset)
+
{#OFFSET_SUNSET}
@@ -254,20 +250,20 @@
-
Username (optional)
+
{#MQTT_USER}
-
Password (optional)
+
{MQTT_PASSWORD}
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)

+

{#MQTT_NOTE}

-
Interval [s]
+
{#INTERVAL}
@@ -280,29 +276,29 @@
- +
- Display Config + {#DISPLAY_CONFIG}
-
Turn off while inverters are offline
+
{#DISP_OFF_INV}
-
Luminance
+
{#DISP_LUMINANCE}
-

Pinout

+

{#DISP_PINOUT}

-
Reboot device after successful save
+
{#BTN_REBOOT_SUCCESSFUL_SAVE}
@@ -311,11 +307,11 @@
- ERASE SETTINGS (not WiFi) + {#BTN_ERASE}
- Import / Export JSON Settings + {#IM_EXPORT}
-
Import
+
{#IMPORT}
@@ -326,9 +322,9 @@
-
Export
+
{#EXPORT}
- Export settings (JSON file) (only values, passwords will be removed!) + {#BTN_EXPORT} {#EXPORT_NOTE}
@@ -341,7 +337,7 @@ var ts = 0; var esp8266pins = [ - [255, "off / default"], + [255, "{#PIN_OFF}"], [0, "D3 (GPIO0)"], [1, "TX (GPIO1)"], [2, "D4 (GPIO2)"], @@ -358,12 +354,12 @@ [13, "D7 (GPIO13)"], [14, "D5 (GPIO14)"], [15, "D8 (GPIO15)"], - [16, "D0 (GPIO16 - no IRQ!)"] + [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"] ]; /*IF_ESP32*/ var esp32pins = [ - [255, "off / default"], + [255, "{#PIN_OFF}"], [0, "GPIO0"], [1, "TX (GPIO1)"], [2, "GPIO2 (LED)"], @@ -386,14 +382,14 @@ [27, "GPIO27"], [32, "GPIO32"], [33, "GPIO33"], - [34, "GPIO34 (in only)"], - [35, "GPIO35 (in only)"], - [36, "VP (GPIO36, in only)"], - [39, "VN (GPIO39, in only)"] + [34, "GPIO34 ({#PIN_INPUT_ONLY})"], + [35, "GPIO35 ({#PIN_INPUT_ONLY})"], + [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], + [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] ]; var esp32s3pins = [ [255, "off / default"], - [0, "GPIO0 (DONT USE - BOOT)"], + [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], [1, "GPIO1"], [2, "GPIO2"], [3, "GPIO3"], @@ -412,16 +408,16 @@ [16, "GPIO16"], [17, "GPIO17"], [18, "GPIO18"], - [19, "GPIO19 (DONT USE - USB-)"], - [20, "GPIO20 (DONT USE - USB+)"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"], + [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"], [21, "GPIO21"], - [26, "GPIO26 (PSRAM - not available)"], - [27, "GPIO27 (FLASH - not available)"], - [28, "GPIO28 (FLASH - not available)"], - [29, "GPIO29 (FLASH - not available)"], - [30, "GPIO30 (FLASH - not available)"], - [31, "GPIO31 (FLASH - not available)"], - [32, "GPIO32 (FLASH - not available)"], + [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"], + [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"], + [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"], + [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"], + [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], + [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], + [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], [33, "GPIO33 (not exposed on WROOM modules)"], [34, "GPIO34 (not exposed on WROOM modules)"], [35, "GPIO35"], @@ -434,8 +430,8 @@ [42, "GPIO42"], [43, "GPIO43"], [44, "GPIO44"], - [45, "GPIO45 (DONT USE - STRAPPING PIN)"], - [46, "GPIO46 (DONT USE - STRAPPING PIN)"], + [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"], [47, "GPIO47"], [48, "GPIO48"], ]; @@ -459,17 +455,17 @@ [15, "GPIO15 (PSRAM/FLASH)"], [16, "GPIO16 (PSRAM/FLASH)"], [17, "GPIO17 (PSRAM/FLASH)"], - [18, "GPIO18 (DONT USE - USB-)"], - [19, "GPIO19 (DONT USE - USB+)"], + [18, "GPIO18 ({#PIN_DONT_USE} - USB-)"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB+)"], [20, "GPIO20 (RX)"], [21, "GPIO21 (TX)"], ]; /*ENDIF_ESP32*/ var nrfPa = [ - [0, "MIN (recommended)"], + [0, "MIN ({#PIN_RECOMMENDED})"], [1, "LOW"], [2, "HIGH"], - [3, "MAX (experimental)"] + [3, "MAX ({#PIN_EXPERIMENTAL})"] ]; var esp32cmtPa = []; var esp32cmtFreq = []; @@ -487,8 +483,8 @@ } /*ENDIF_ESP32*/ var led_high_active = [ - [0, "low active"], - [1, "high active"], + [0, "{#PIN_LOW_ACTIVE}"], + [1, "{#PIN_HIGH_ACTIVE}"], ]; window.onload = function() { @@ -513,31 +509,31 @@ var e = document.getElementById("networks"); selDelAllOpt(e); if(obj["success"]) - e.appendChild(opt("-1", "scanning ...")) + e.appendChild(opt("-1", "{#NETWORK_SCANNING}")) else - e.appendChild(opt("-1", "Error: " + obj["error"])); + e.appendChild(opt("-1", "{#ERROR} " + obj["error"])); } function apiCbNtp(obj) { var e = document.getElementById("apiResultNtp"); if(obj["success"]) - e.innerHTML = "command executed, set new time ..."; + e.innerHTML = "{#NTP_COMMAND_EXE}"; else - e.innerHTML = "Error: " + obj["error"]; + e.innerHTML = "{#ERROR} " + obj["error"]; } function apiCbNtp2(obj) { var e = document.getElementById("apiResultNtp"); var date = new Date(obj["ts_now"] * 1000); - e.innerHTML = "synced at: " + toIsoDateStr(date) + ", difference: " + (ts - obj["ts_now"]) + "ms"; + e.innerHTML = "{#NTP_SYNCED_AT}: " + toIsoDateStr(date) + ", {#NTP_DIFF}: " + (ts - obj["ts_now"]) + "ms"; } function apiCbMqtt(obj) { var e = document.getElementById("apiResultMqtt"); if(obj["success"]) - e.innerHTML = "command executed"; + e.innerHTML = "{#MQTT_EXE}"; else - e.innerHTML = "Error: " + obj["error"]; + e.innerHTML = "{#ERROR} " + obj["error"]; } function setTime() { @@ -572,7 +568,7 @@ function hide() { document.getElementById("form").submit(); var e = document.getElementById("content"); - e.replaceChildren(span("upload started")); + e.replaceChildren(span("{#IMPORT_UPLOAD_STARTED}")); } function delIv() { @@ -626,7 +622,7 @@ if(!obj["pwd_set"]) e.value = ""; var d = document.getElementById("prot_mask"); - var a = ["Index", "Live", "Webserial", "Settings", "Update", "System"]; + var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System"]; var el = []; for(var i = 0; i < 6; i++) { var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); @@ -657,13 +653,13 @@ ml("th", {style: "width: 10%; text-align: center;"}, ""), ml("th", {}, "Name"), ml("th", {}, "Serial"), - ml("th", {style: "width: 10%; text-align: center;"}, "Edit"), - ml("th", {style: "width: 10%; text-align: center;"}, "Delete") + ml("th", {style: "width: 10%; text-align: center;"}, "{#INV_EDIT}"), + ml("th", {style: "width: 10%; text-align: center;"}, "{#INV_DELETE}") ])); for(let i = 0; i < obj.inverter.length; i++) { lines.push(ml("tr", {}, [ - ml("td", {}, badge(obj.inverter[i].enabled, (obj.inverter[i].enabled) ? "enabled" : "disabled")), + ml("td", {}, badge(obj.inverter[i].enabled, (obj.inverter[i].enabled) ? "{#ENABLED}" : "{#DISABLED}")), ml("td", {}, obj.inverter[i].name), ml("td", {}, String(obj.inverter[i].serial)), ml("td", {style: "text-align: center;", onclick: function() {ivModal(obj.inverter[i]);}}, svg(iconGear, 25, 25, "icon icon-fg pointer")), @@ -686,7 +682,7 @@ e.innerHTML = ""; // remove all childs e.append(ml("table", {class: "table"}, ml("tbody", {}, lines))); if(obj.max_num_inverters > obj.inverter.length) - e.append(ml("div", {class: "row my-3"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "add Inverter", class: "btn", onclick: function() { ivModal(add); }}, null)))); + e.append(ml("div", {class: "row my-3"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "{#BTN_INV_ADD}", class: "btn", onclick: function() { ivModal(add); }}, null)))); ivGlob(obj); } @@ -694,10 +690,10 @@ function ivModal(obj) { var lines = []; lines.push(ml("tr", {}, [ - ml("th", {style: "width: 10%;"}, "Input"), - ml("th", {}, "Max Module Power [Wp]"), + ml("th", {style: "width: 10%;"}, "{#INV_INPUT}"), + ml("th", {}, "{#INV_MAX_MODULE_POWER} [Wp]"), ml("th", {}, "Name (optional)"), - ml("th", {}, "Yield Correction [kWh] (optional)") + ml("th", {}, "{#INV_YIELD_CORR} [kWh] (optional)") ])); for(let i = 0; i < 6; i++) { @@ -718,14 +714,14 @@ var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null); var html = ml("div", {}, [ - tabs(["General", "Inputs", "Radio", "Advanced"]), - ml("div", {id: "divGeneral", class: "tab-content"}, [ + tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]), + ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [ ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-2"}, "Enable"), + ml("div", {class: "col-2"}, "{#INV_ENABLE}"), ml("div", {class: "col-10"}, cbEn) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-2 mt-2"}, "Serial"), + ml("div", {class: "col-2 mt-2"}, "{#INV_SERIAL}"), ml("div", {class: "col-10"}, ser) ]), ml("div", {class: "row mb-3"}, [ @@ -733,45 +729,45 @@ ml("div", {class: "col-10"}, ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null)) ]) ]), - ml("div", {id: "divInputs", class: "tab-content hide"}, [ + ml("div", {id: "div{#TAB_INPUTS}", class: "tab-content hide"}, [ ml("div", {class: "row mb-3"}, ml("table", {class: "table"}, ml("tbody", {}, lines) ) ) ]), - ml("div", {id: "divRadio", class: "tab-content hide"}, [ + ml("div", {id: "div{#TAB_RADIO}", class: "tab-content hide"}, [ ml("input", {type: "hidden", name: "isnrf"}, null), ml("div", {id: "setcmt"}, [ ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-3 mt-2"}, "Frequency"), + ml("div", {class: "col-3 mt-2"}, "{#INV_FREQUENCY}"), ml("div", {class: "col-9"}, sel("freq", esp32cmtFreq, obj.freq)) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-3 mt-2"}, "Power Level"), + ml("div", {class: "col-3 mt-2"}, "{#INV_POWER_LEVEL}"), ml("div", {class: "col-9"}, sel("cmtpa", esp32cmtPa, obj.pa)) ]), ]), ml("div", {id: "setnrf"}, ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-3 mt-2"}, "Power Level"), + ml("div", {class: "col-3 mt-2"}, "{#INV_POWER_LEVEL}"), ml("div", {class: "col-9"}, sel("nrfpa", nrfPa, obj.pa)) ]), ), ]), - ml("div", {id: "divAdvanced", class: "tab-content hide"}, [ + ml("div", {id: "div{#TAB_ADVANCED}", class: "tab-content hide"}, [ ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-10"}, "Pause communication during night (lat. and lon. need to be set)"), + ml("div", {class: "col-10"}, "{#INV_PAUSE_DURING_NIGHT}"), ml("div", {class: "col-2"}, cbDisNightCom) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default, MqTT only)"), + ml("div", {class: "col-10"}, "{#INV_INCLUDE_MQTT_SUM}"), ml("div", {class: "col-2"}, cbAddTotal) ]) ]), ml("div", {class: "row mt-5"}, [ ml("div", {class: "col-8", id: "res"}, ""), - ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "save", class: "btn", onclick: function() { ivSave(); }}, null)) + ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "{#BTN_SAVE}", class: "btn", onclick: function() { ivSave(); }}, null)) ]) ]); @@ -808,7 +804,7 @@ }) }); - modal("Edit inverter " + obj.name, html); + modal("{#INV_EDIT_MODAL}: " + obj.name, html); ser.dispatchEvent(new Event('change')); function ivSave() { @@ -839,7 +835,7 @@ function cb(obj2) { var e = document.getElementById("res"); if(!obj2.success) - e.innerHTML = "error: " + obj2.error; + e.innerHTML = "{#ERROR}" + obj2.error; else { modalClose(); getAjax("/api/inverter/list", parseIv); @@ -849,10 +845,10 @@ function ivDel(obj) { var html = ml("div", {class: "row"}, [ - ml("div", {class: "col-9"}, "do you realy want to delete inverter " + obj.name + "?"), - ml("div", {class: "col-3 a-r"}, ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "yes", class: "btn", onclick: function() { del(); }}, null))) + ml("div", {class: "col-9"}, "{#INV_DELETE_SURE} (" + obj.name + ")"), + ml("div", {class: "col-3 a-r"}, ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "{#BTN_YES}", class: "btn", onclick: function() { del(); }}, null))) ]); - modal("Delete inverter " + obj.name, html); + modal("{#INV_DELETE_MODAL}: " + obj.name, html); function del() { var o = new Object(); @@ -896,7 +892,7 @@ for(p of [["sunOffsSr", "offsSr"], ["sunOffsSs", "offsSs"]]) { const sel = document.getElementsByName(p[0])[0]; for(var i = -60; i <= 60; i++) { - sel.appendChild(opt(i, i + " minutes", (i == (obj[p[1]] / 60)))); + sel.appendChild(opt(i, i + " {#NTP_MINUTES}", (i == (obj[p[1]] / 60)))); } } } @@ -909,7 +905,7 @@ if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ - pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected'], ['led2', 'pinLed2', 'Night time']]; + pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; for(p of pins) { e.append( ml("div", {class: "row mb-3"}, [ @@ -922,13 +918,13 @@ } e.append( ml("div", { class: "row mb-3" }, [ - ml("div", { class: "col-12 col-sm-3 my-2" }, "LED polarity"), + ml("div", { class: "col-12 col-sm-3 my-2" }, "{#LED_POLARITY}"), ml("div", { class: "col-12 col-sm-9" }, sel('pinLedHighActive', led_high_active, obj.led_high_active) ) ]), ml("div", { class: "row mb-3" }, [ - ml("div", { class: "col-12 col-sm-3 my-2" }, "LED luminance (0-255)"), + ml("div", { class: "col-12 col-sm-3 my-2" }, "{#LED_LUMINANCE} (0-255)"), ml("div", { class: "col-12 col-sm-9" }, ml("input", {class: "text", type: "number", name: "pinLedLum", value: obj.led_lum, min: 0, max: 255}, null)) ]) ) @@ -948,7 +944,7 @@ e.replaceChildren ( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-8 col-sm-3 my-2"}, "NRF24 Enable"), + ml("div", {class: "col-8 col-sm-3 my-2"}, "{#NRF24_ENABLE}"), ml("div", {class: "col-4 col-sm-9"}, en) ]) ); @@ -982,7 +978,7 @@ e.replaceChildren ( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-8 col-sm-3 my-2"}, "CMT2300A Enable"), + ml("div", {class: "col-8 col-sm-3 my-2"}, "{#CMT_ENABLE}"), ml("div", {class: "col-4 col-sm-9"}, en) ]) ); @@ -1033,7 +1029,7 @@ ); } // keep display types grouped - var opts = [[0, "None"], + var opts = [[0, "{#DISP_NONE}"], [2, "SH1106 128x64 (1.3\")"], [5, "SSD1306 64x48 (0.66\" Wemos OLED Shield)"], [4, "SSD1306 128x32 (0.91\")"], @@ -1046,7 +1042,7 @@ var dtype_sel = sel("disp_typ", opts, obj["disp_typ"]); document.getElementById("dispType").append( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), + ml("div", {class: "col-12 col-sm-3 my-2"}, "{#DISP_TYPE}"), ml("div", {class: "col-12 col-sm-9"}, dtype_sel) ]) ); @@ -1057,22 +1053,22 @@ /*ENDIF_ESP32*/ document.getElementById("dispRot").append( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, "Rotation"), + ml("div", {class: "col-12 col-sm-3 my-2"}, "{#DISP_ROTATION}"), ml("div", {class: "col-12 col-sm-9"}, sel("disp_rot", opts, obj["disp_rot"])) ]) ); - var opts1 = [[0, "off"], [1, "pixel shift"], [2, "motion sensor"]]; + var opts1 = [[0, "{#DISP_SCREENSAVER_OFF}"], [1, "{#DISP_PIXEL_SHIFT}"], [2, "{#DISP_MOTION_SENS}"]]; var screensaver_sel = sel("disp_screensaver", opts1, obj["disp_screensaver"]); screensaver_sel.id = 'disp_screensaver'; document.getElementById("screenSaver").append( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, "Screensaver (OLED only)"), + ml("div", {class: "col-12 col-sm-3 my-2"}, "{#DISP_SCREENSAVER}"), ml("div", {class: "col-12 col-sm-9"}, screensaver_sel) ]) ); - var esp8266pirpins = [[255, "off / default"], + var esp8266pirpins = [[255, "{#PIN_OFF}"], [17, "A0"]]; document.getElementById("pirPin").append( ml("div", {class: "row mb-3"}, [ @@ -1155,12 +1151,12 @@ var s = document.getElementById("networks"); selDelAllOpt(s); if(root["networks"].length > 0) { - s.appendChild(opt("-1", "please select network")); + s.appendChild(opt("-1", "{#NETWORK_PLEASE_SELECT}")); for(i = 0; i < root["networks"].length; i++) { s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)")); } } else - s.appendChild(opt("-1", "no network found")); + s.appendChild(opt("-1", "{#NO_NETWORK_FOUND}")); } function selNet() { diff --git a/src/web/html/system.html b/src/web/html/system.html index f400db41..16c6343e 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -46,7 +46,7 @@ function irqBadge(state) { switch(state) { - case 0: return badge(false, "unknown", "warning"); break; + case 0: return badge(false, "{#UNKNOWN}", "warning"); break; case 1: return badge(true, "true"); break; default: return badge(false, "false"); break; } @@ -57,16 +57,16 @@ if(obj.radioNrf.en) { lines = [ - tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")), - tr("Interrupt Pin working", irqBadge(obj.radioNrf.irqOk)), - tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps"), + tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), + tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)), + tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"), tr("DTU Radio ID", obj.radioNrf.sn) ]; } else - lines = [tr("NRF24L01", badge(false, "not enabled"))]; + lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))]; document.getElementById("info").append( - headline("Radio NRF"), + headline("{#NRF24_RADIO}"), ml("table", {class: "table"}, ml("tbody", {}, lines) ) @@ -75,15 +75,15 @@ /*IF_ESP32*/ if(obj.radioCmt.en) { cmt = [ - tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")), - tr("Interrupt Pin working", irqBadge(obj.radioCmt.irqOk)), + tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), + tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)), tr("DTU Radio ID", obj.radioCmt.sn) ]; } else - cmt = [tr("CMT2300A", badge(false, "not enabled"))]; + cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))]; document.getElementById("info").append( - headline("Radio CMT"), + headline("{#CMT_RADIO}"), ml("table", {class: "table"}, ml("tbody", {}, cmt) ) @@ -94,13 +94,13 @@ function parseMqtt(obj) { if(obj.enabled) { lines = [ - tr("connected", badge(obj.connected, ((obj.connected) ? "true" : "false"))), + tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))), tr("#TX", obj.tx_cnt), tr("#RX", obj.rx_cnt) ]; } else - lines = tr("enabled", badge(false, "false")); + lines = tr("{#ENABLED}", badge(false, "{#FALSE}")); document.getElementById("info").append( headline("MqTT"), @@ -113,14 +113,14 @@ function parseIndex(obj) { if(obj.ts_sunrise > 0) { document.getElementById("info").append( - headline("Sun"), + headline("{#SUN}"), ml("table", {class: "table"}, ml("tbody", {}, [ - tr("Sunrise", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), - tr("Sunset", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), - tr("Communication start", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), - tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), - tr("Night behaviour", badge(obj.disNightComm, ((obj.disNightComm) ? "not" : "") + " communicating", "warning")) + tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), + tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), + tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), + tr("{#COMMUNICATION_STTOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), + tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning")) ]) ) ); diff --git a/src/web/html/update.html b/src/web/html/update.html index 4e56b1ad..f0e39fe7 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -9,14 +9,14 @@
- Select firmware file (*.bin) + {#SELECT_FILE} (*.bin) - +
@@ -31,7 +31,7 @@ function hide() { document.getElementById("form").submit(); var e = document.getElementById("content"); - e.replaceChildren(span("update started")); + e.replaceChildren(span("{#UPDATE_STARTED}")); } getAjax("/api/generic", parseGeneric); diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 27f5da35..8099539c 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -10,7 +10,7 @@
-

Every seconds the values are updated

+

{#EVERY} {#UPDATE_SECS}

{#HTML_FOOTER} @@ -70,20 +70,20 @@ ml("div", {class: "col"}, [ ml("div", {class: "p-2 total-h"}, ml("div", {class: "row"}, - ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL") + ml("div", {class: "col mx-2 mx-md-1"}, "{#TOTAL}") ), ), ml("div", {class: "p-2 total-bg"}, [ ml("div", {class: "row"}, [ - numBig(total[0], "W", "AC Power"), - numBig(total[1], "Wh", "Yield Day"), - numBig(total[2], "kWh", "Yield Total") + numBig(total[0], "W", "{#AC_POWER}"), + numBig(total[1], "Wh", "{#YIELD_DAY}"), + numBig(total[2], "kWh", "{#YIELD_TOTAL}") ]), ml("div", {class: "hr"}), ml("div", {class: "row"}, [ - numMid(total[3], "W", "Max Power"), - numMid(total[4], "W", "DC Power"), - numMid(total[5], "var", "Reactive Power") + numMid(total[3], "W", "{#MAX_POWER}"), + numMid(total[4], "W", "{#DC_POWER}"), + numMid(total[5], "var", "{#REACTIVE_POWER}") ]) ]) ]) @@ -119,31 +119,31 @@ getAjax("/api/inverter/version/" + obj.id, parseIvVersion); }}, obj.name)), ml("div", {class: "col a-c", onclick: function() {limitModal(obj)}}, [ - ml("span", {class: "d-none d-sm-block pointer"}, "Active Power Control: " + pwrLimit), - ml("span", {class: "d-block d-sm-none pointer"}, "APC: " + pwrLimit) + ml("span", {class: "d-none d-sm-block pointer"}, "{#ACTIVE_POWER_CONTROL}: " + pwrLimit), + ml("span", {class: "d-block d-sm-none pointer"}, "{#APC}: " + pwrLimit) ]), ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() { getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm); - }}, ("Alarms: " + obj.alarm_cnt))), + }}, ("{#ALARMS}: " + obj.alarm_cnt))), ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText) ]) ), ml("div", {class: "p-2 " + clbg}, [ ml("div", {class: "row"},[ - numBig(obj.ch[0][2], "W", "AC Power"), - numBig(obj.ch[0][7], "Wh", "Yield Day"), - numBig(obj.ch[0][6], "kWh", "Yield Total") + numBig(obj.ch[0][2], "W", "{#AC_POWER}"), + numBig(obj.ch[0][7], "Wh", "{#YIELD_DAY}"), + numBig(obj.ch[0][6], "kWh", "{#YIELD_TOTAL}") ]), ml("div", {class: "hr"}), ml("div", {class: "row mt-2"},[ - numMid(obj.ch[0][11], "W", "Max AC Power", {class: "fs-6 tooltip", data: maxAcPwr}), - numMid(obj.ch[0][8], "W", "DC Power"), - numMid(obj.ch[0][0], "V", "AC Voltage"), - numMid(obj.ch[0][1], "A", "AC Current"), - numMid(obj.ch[0][3], "Hz", "Frequency"), - numMid(obj.ch[0][9], "%", "Efficiency"), - numMid(obj.ch[0][10], "var", "Reactive Power"), - numMid(obj.ch[0][4], "", "Power Factor") + numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}), + numMid(obj.ch[0][8], "W", "{#DC_POWER}"), + numMid(obj.ch[0][0], "V", "{#DC_VOLTAGE}"), + numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"), + numMid(obj.ch[0][3], "Hz", "{#FREQUENCY}"), + numMid(obj.ch[0][9], "%", "{#EFFICIENCY}"), + numMid(obj.ch[0][10], "var", "{#REACTIVE_POWER}"), + numMid(obj.ch[0][4], "", "{#POWER_FACTOR}") ]) ]) ]) @@ -172,26 +172,26 @@ ml("div", {class: "p-2 a-c " + clh}, name), ml("div", {class: "p-2 " + clbg}, [ ml("div", {class: "row"}, [ - numCh(vals[2], units[2], "DC Power"), - numCh(vals[6], units[2], "Max Power"), - numCh(vals[5], units[5], "Irradiation"), - numCh(vals[3], units[3], "Yield Day"), - numCh(vals[4], units[4], "Yield Total"), - numCh(vals[0], units[0], "DC Voltage"), - numCh(vals[1], units[1], "DC Current") + numCh(vals[2], units[2], "{#DC_POWER}"), + numCh(vals[6], units[2], "{#MAX_POWER}"), + numCh(vals[5], units[5], "{#IRRADIATION}"), + numCh(vals[3], units[3], "{#YIELD_DAY}"), + numCh(vals[4], units[4], "{#YIELD_TOTAL}"), + numCh(vals[0], units[0], "{#DC_VOLTAGE}"), + numCh(vals[1], units[1], "{#DC_CURRENT}") ]) ]) ]); } function tsInfo(obj) { - var ageInfo = "Last received data requested at: "; + var ageInfo = "{#LAST_RECEIVED}: "; if(obj.ts_last_success > 0) { var date = new Date(obj.ts_last_success * 1000); ageInfo += toIsoDateStr(date); } else - ageInfo += "nothing received"; + ageInfo += "{#NOTHING_RECEIVED}"; if(obj.rssi > -127) { if(obj.generation < 2) @@ -252,10 +252,10 @@ var offs = new Date().getTimezoneOffset() * -60; html.push( ml("div", {class: "row"}, [ - ml("div", {class: "col"}, ml("strong", {}, "Event")), + ml("div", {class: "col"}, ml("strong", {}, "{#EVENT}")), ml("div", {class: "col"}, ml("strong", {}, "ID")), ml("div", {class: "col"}, ml("strong", {}, "Start")), - ml("div", {class: "col"}, ml("strong", {}, "End")) + ml("div", {class: "col"}, ml("strong", {}, "{#END}")) ]) ); @@ -271,7 +271,7 @@ ); } } - modal("Alarms of inverter " + obj.iv_name, ml("div", {}, html)); + modal("{#ALARMS_MODAL}: " + obj.iv_name, ml("div", {}, html)); } function parseIvVersion(obj) { @@ -283,7 +283,7 @@ case 3: model = "HMT-"; break; default: model = "???-"; break; } - model += String(obj.max_pwr) + " (Serial: " + obj.serial + ")"; + model += String(obj.max_pwr) + " ({#SERIAL}: " + obj.serial + ")"; var html = ml("table", {class: "table"}, [ @@ -291,15 +291,15 @@ tr("Model", model), tr("Firmware Version / Build", String(obj.fw_ver) + " (build: " + String(obj.fw_date) + " " + String(obj.fw_time) + ")"), tr("Hardware Version / Build", (obj.hw_ver/100).toFixed(2) + " (build: " + String(obj.prod_cw) + "/" + String(obj.prod_year) + ")"), - tr("Hardware Number", obj.part_num.toString(16)), + tr("{#HW_NUMBER}", obj.part_num.toString(16)), tr("Bootloader Version", (obj.boot_ver/100).toFixed(2)), - tr("Grid Profile", ml("input", {type: "button", value: "show", class: "btn", onclick: function() { + tr("Grid Profile", ml("input", {type: "button", value: "{#BTN_SHOW}", class: "btn", onclick: function() { modalClose(); getAjax("/api/inverter/grid/" + obj.id, showGridProfile); }}, null)) ]) ]) - modal("Info for inverter " + obj.name, ml("div", {}, html)) + modal("{#INV_INFO}: " + obj.name, ml("div", {}, html)) } function getGridValue(g) { @@ -332,9 +332,9 @@ )) content.push(ml("div", {class: "row my-2"}, [ ml("div", {class: "col-4"}, ml("b", {}, "Name")), - ml("div", {class: "col-3"}, ml("b", {}, "Value")), - ml("div", {class: "col-3"}, ml("b", {}, "Range")), - ml("div", {class: "col-2"}, ml("b", {}, "Default")) + ml("div", {class: "col-3"}, ml("b", {}, "{#VALUE}")), + ml("div", {class: "col-3"}, ml("b", {}, "{#RANGE}")), + ml("div", {class: "col-2"}, ml("b", {}, "{#DEFAULT}")) ])) for(e of g.info.group) { if(Array.isArray(e[id])) { @@ -362,10 +362,10 @@ var v = getGridValue(glob); if(null === g) { if(0 == obj.grid.length) { - content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Profile was not read until now, maybe turned off?")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}?")))) } else { - content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "Unknown Profile")))) - content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Please open a new issue at https://github.com/lumapu/ahoy and copy the raw data into it.")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}.")))) content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid)))) } } else { @@ -378,7 +378,7 @@ } } - modal("Grid Profile for inverter " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content]))) + modal("{#PROFILE_MODAL}: " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content]))) }) } @@ -386,56 +386,56 @@ function parseIvRadioStats(obj) { var html = ml("table", {class: "table"}, [ ml("tbody", {}, [ - tr2(["TX count", obj.tx_cnt, ""]), - tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + " %"]), - tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + " %"]), - tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + " %"]), - tr2(["RX fragments", obj.frame_cnt, ""]), - tr2(["TX retransmits", obj.retransmits, ""]), - tr2(["Inverter loss rate", "lost " + obj.ivLoss + " of " + obj.ivSent, String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + " %"]), - tr2(["DTU loss rate", "lost " + obj.dtuLoss + " of " + obj.dtuSent, String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + " %"]) + tr2(["{#TX_COUNT}", obj.tx_cnt, ""]), + tr2(["{#RX_SUCCESS}", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["{#RX_FAIL}", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["{#RX_NO_ANSWER}", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["{#RX_FRAGMENTS}", obj.frame_cnt, ""]), + tr2(["{#TX_RETRANSMITS}", obj.retransmits, ""]), + tr2(["{#INV_LOSS_RATE}", "{#LOST_1} " + obj.ivLoss + " {#LOST_2} " + obj.ivSent + " {#LOST_3}", String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + " %"]), + tr2(["{#DTU_LOSS_RATE}", "{#LOST_1} " + obj.dtuLoss + " {#LOST_2} " + obj.dtuSent + " {#LOST_3}", String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + " %"]) ]) ]) - modal("Radio statistics for inverter " + obj.name, ml("div", {}, html)) + modal("{#RADIO_STAT_MODAL}: " + obj.name, ml("div", {}, html)) } function limitModal(obj) { var opt = [["pct", "%"], ["watt", "W"]]; var html = ml("div", {}, [ ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"), + ml("div", {class: "col-12 col-sm-5 my-2"}, "{#LIMIT_VALUE}"), ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number", step: "0.1", min: 1}, "")), ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct")) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-8 col-sm-5"}, "Keep limit over inverter restart"), + ml("div", {class: "col-8 col-sm-5"}, "{#KEEP_LIMIT}"), ml("div", {class: "col-4 col-sm-7"}, ml("input", {type: "checkbox", name: "keep"})) ]), ml("div", {class: "row my-3"}, - ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "Apply", class: "btn", onclick: function() { + ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "{#BTN_APPLY}", class: "btn", onclick: function() { applyLimit(obj.id); }}, null)) ), ml("div", {class: "row my-4"}, [ - ml("div", {class: "col-12 col-sm-5 my-2"}, "Control"), + ml("div", {class: "col-12 col-sm-5 my-2"}, "{#CONTROL}"), ml("div", {class: "col col-sm-7 a-r"}, [ - ml("input", {type: "button", value: "restart", class: "btn", onclick: function() { + ml("input", {type: "button", value: "{#RESTART}", class: "btn", onclick: function() { applyCtrl(obj.id, "restart"); }}, null), - ml("input", {type: "button", value: "turn off", class: "btn mx-1", onclick: function() { + ml("input", {type: "button", value: "{#TURN_OFF}", class: "btn mx-1", onclick: function() { applyCtrl(obj.id, "power", 0); }}, null), - ml("input", {type: "button", value: "turn on", class: "btn", onclick: function() { + ml("input", {type: "button", value: "{#TURN_ON}", class: "btn", onclick: function() { applyCtrl(obj.id, "power", 1); }}, null) ]) ]), ml("div", {class: "row mt-1"}, [ - ml("div", {class: "col-12 col-sm-5 my-2"}, "Result"), + ml("div", {class: "col-12 col-sm-5 my-2"}, "{#RESULT}"), ml("div", {class: "col-sm-7 my-2"}, ml("span", {name: "pwrres"}, "-")) ]) ]); - modal("Active Power Control for inverter " + obj.name, html); + modal("{#POWER_LIMIT_MODAL}: " + obj.name, html); } function applyLimit(id) { @@ -470,19 +470,19 @@ function ctrlCb(obj) { var e = document.getElementsByName("pwrres")[0]; if(obj.success) { - e.innerHTML = "received command, waiting for inverter acknowledge ..."; + e.innerHTML = "{#CMD_RECEIVED_WAIT_ACK}"; tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000); } else - e.innerHTML = "Error: " + obj["error"]; + e.innerHTML = "{#ERROR}: " + obj["error"]; } function ctrlCb2(obj) { var e = document.getElementsByName("pwrres")[0]; if(obj.success) - e.innerHTML = "command received"; + e.innerHTML = "{#COMMAND_RECEIVED}"; else - e.innerHTML = "Error: " + obj["error"]; + e.innerHTML = "{#ERROR}: " + obj["error"]; } function updatePwrAck(obj) { @@ -492,7 +492,7 @@ clearInterval(tPwrAck); if(null == e) return; - e.innerHTML = "inverter acknowledged active power control command"; + e.innerHTML = "{#INV_ACK}"; } function parse(obj) { diff --git a/src/web/html/wizard.html b/src/web/html/wizard.html index 1ebfb65f..674823b2 100644 --- a/src/web/html/wizard.html +++ b/src/web/html/wizard.html @@ -24,23 +24,23 @@ function wifi() { return ml("div", {}, [ - ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "Welcome to AhoyDTU"))), - ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "Network Setup"))), - sect("Choose your WiFi Network", ml("select", {id: "net", onchange: () => {if(found) clearInterval(v)}}, ml("option", {value: "-1"}, "---"))), - sect("... or name it manually", ml("input", {id: "man", type: "text"})), - sect("WiFi Password", ml("input", {id: "pwd", type: "password"})), - ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "next", onclick: () => {saveWifi()}}, null))), - ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "stop wizard"))) + ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))), + ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))), + sect("{#CHOOSE_WIFI}", ml("select", {id: "net", onchange: () => {if(found) clearInterval(v)}}, ml("option", {value: "-1"}, "---"))), + sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})), + sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})), + ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))), + ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) ]) } function checkWifi() { c.replaceChildren( - ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "Welcome to AhoyDTU"))), - ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "Test Connection"))), - sect("AhoyDTU is trying to connect to your WiFi", ml("span", {id: "state"}, "connecting ...")), - ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "Finish", onclick: () => {redirect()}}, null))), - ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "stop wizard"))) + ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))), + ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))), + sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")), + ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))), + ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) ) v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 2500); } @@ -53,7 +53,7 @@ if("0.0.0.0" != obj["ip"]) { clearInterval(v) setHide("btn", false) - document.getElementById("state").innerHTML = "success, got following IP in your network: " + obj.ip + document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip } } @@ -68,7 +68,7 @@ var e = document.getElementById("net"); if(obj.networks.length > 0) { var a = [] - a.push(ml("option", {value: -1}, obj.networks.length + " Network(s) found")) + a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}")) for(n of obj.networks) { a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)")) found = true; diff --git a/src/web/lang.json b/src/web/lang.json new file mode 100644 index 00000000..d6c76ffc --- /dev/null +++ b/src/web/lang.json @@ -0,0 +1,1334 @@ +{ + "files": [ + { + "name": "general", + "list": [ + { + "token": "NAV_LIVE", + "en": "Live", + "de": "Daten" + }, + { + "token": "NAV_WEBSERIAL", + "en": "Webserial", + "de": "Web Konsole" + }, + { + "token": "NAV_SETTINGS", + "en": "Settings", + "de": "Einstellungen" + }, + { + "token": "NAV_DOCUMENTATION", + "en": "Documentation", + "de": "Dokumentation" + }, + { + "token": "NAV_ABOUT", + "en": "About", + "de": "Über" + } + ] + }, + { + "name": "wizard.html", + "list": [ + { + "token": "WELCOME", + "en": "Welcome to AhoyDTU", + "de": "Willkommen zu AhoyDTU" + }, + { + "token": "NETWORK_SETUP", + "en": "Network Setup", + "de": "Netzwerkeinstellung" + }, + { + "token": "CHOOSE_WIFI", + "en": "Choose your WiFi Network", + "de": "Wähle dein WiFi Netzwerk aus" + }, + { + "token": "WIFI_MANUAL", + "en": "... or name it manually", + "de": "... oder nenne es hier" + }, + { + "token": "WIFI_PASSWORD", + "en": "WiFi Password", + "de": "WiFi Passwort" + }, + { + "token": "STOP_WIZARD", + "en": "stop wizard", + "de": "Einrichtung beenden" + }, + { + "token": "BTN_NEXT", + "en": "next >>", + "de": "prüfen >>" + }, + { + "token": "TEST_CONNECTION", + "en": "Test Connection", + "de": "Verbindung wird überprüft" + }, + { + "token": "TRY_TO_CONNECT", + "en": "AhoyDTU is trying to connect to your WiFi", + "de": "AhoyDTU versucht eine Verindung mit deinem Netzwerk herzustellen" + }, + { + "token": "CONNECTING", + "en": "connecting ...", + "de": "verbinde ..." + }, + { + "token": "NETWORK_SUCCESS", + "en": "success, got following IP in your network: ", + "de": "Verindung erfolgreich. AhoyDTU hat die folgende IP bekommen: " + }, + { + "token": "BTN_FINISH", + "en": "Finish >>", + "de": "Fertig >>" + }, + { + "token": "NUM_NETWORKS_FOUND", + "en": "Network(s) found", + "de": "Netzwerk(e) gefunden" + } + ] + }, + { + "name": "setup.html", + "list": [ + { + "token": "SETTINGS", + "en": "Settings", + "de": "Einstellungen" + }, + { + "token": "SYSTEM_CONFIG", + "en": "System Config", + "de": "Systemkonfiguration" + }, + { + "token": "DEVICE_NAME", + "en": "Device Host Name", + "de": "Name der DTU" + }, + { + "token": "REBOOT_AT_MIDNIGHT", + "en": "Reboot Ahoy at midnight", + "de": "um Mitternacht neu starten" + }, + { + "token": "DARK_MODE", + "en": "Dark Mode", + "de": "dunkler Modus" + }, + { + "token": "DARK_MODE_NOTE", + "en": "(empty browser cache or use CTRL + F5 after reboot to apply this setting)", + "de": "(der Browser-Cache muss geleert oder STRG + F5 gedrückt werden, um diese Einstellung zu aktivieren)" + }, + { + "token": "PINOUT_CONFIGURATION", + "en": "Pinout Configuration", + "de": "Anschlusseinstellungen" + }, + { + "token": "RADIO", + "en": "Radio", + "de": "Funkmodul" + }, + { + "token": "SERIAL_CONSOLE", + "en": "Serial console", + "de": "Serielle Konsole" + }, + { + "token": "LOG_PRINT_INVERTER_DATA", + "en": "print inverter data", + "de": "Livedaten ausgeben" + }, + { + "token": "LOG_SERIAL_DEBUG", + "en": "Serial Debug", + "de": "Entwicklerinformationen ausgeben" + }, + { + "token": "LOG_PRIVACY_MODE", + "en": "Privacy Mode", + "de": "Privatsphärenmodus" + }, + { + "token": "LOG_PRINT_TRACES", + "en": "Print whole traces in Log", + "de": "alle Informationen in Log schreiben" + }, + { + "token": "NETWORK", + "en": "Network", + "de": "Netzwerk" + }, + { + "token": "AP_PWD", + "en": "AP Password (min. length: 8)", + "de": "AP Passwort (min. Länge: 8 Zeichen)" + }, + { + "token": "SEARCH_NETWORKS", + "en": "Search Networks", + "de": "Netzwerke suchen" + }, + { + "token": "BTN_SCAN", + "en": "scan", + "de": "Suche starten" + }, + { + "token": "AVAIL_NETWORKS", + "en": "Avail Networks", + "de": "Verfügbare Netzwerke" + }, + { + "token": "NETWORK_NOT_SCANNED", + "en": "not scanned", + "de": "nicht gesucht" + }, + { + "token": "SSID_HIDDEN", + "en": "SSID is hidden", + "de": "SSID ist versteckt" + }, + { + "token": "PASSWORD", + "en": "Password", + "de": "Passwort" + }, + { + "token": "STATIC_IP", + "en": "Static IP (optional)", + "de": "Statische IP (optional)" + }, + { + "token": "NETWORK_HINT_BLANK", + "en": "Leave fields blank for DHCP
The following fields are parsed in this format: 192.168.4.1", + "de": "Felder leer lassen um in den DHCP Modus zu wechseln.
Das Format der IP Adressen ist: 192.168.4.1" + }, + { + "token": "IP_ADDRESS", + "en": "IP Address", + "de": "IP Adresse" + }, + { + "token": "SUBMASK", + "en": "Submask", + "de": "Subnetzmaske" + }, + { + "token": "PROTECTION", + "en": "Protection", + "de": "Zugriffsschutz" + }, + { + "token": "ADMIN_PASSWORD", + "en": "Admin Password", + "de": "Administratorpasswort" + }, + { + "token": "PROTECTION_NOTE", + "en": "Select pages which should be protected by password", + "de": "Auswählen, welche Bereiche passwortgeschützt werden sollen" + }, + { + "token": "INVERTER", + "en": "Inverter", + "de": "Wechselrichter" + }, + { + "token": "INTERVAL", + "en": "Interval [s]", + "de": "Intervall [s]" + }, + { + "token": "INV_GAP", + "en": "Communication Gap [ms]", + "de": "Kommunikationslücke" + }, + { + "token": "INV_RESET_MIDNIGHT", + "en": "Reset values and YieldDay at midnight", + "de": "Werte und Gesamtertrag um Mitternacht zurücksetzen" + }, + { + "token": "INV_PAUSE_SUNSET", + "en": "Reset values at sunset", + "de": "Werte bei Sonnenuntergang zurücksetzen" + }, + { + "token": "INV_RESET_NOT_AVAIL", + "en": "Reset values when inverter status is 'not available'", + "de": "Werte zurücksetzen, sobald der Wechselrichter nicht erreichbar ist" + }, + { + "token": "INV_RESET_MAX_MIDNIGHT", + "en": "Reset 'max' values at midnight", + "de": "Maximalwerte bei Sonnenuntergang zurücksetzen" + }, + { + "token": "INV_START_WITHOUT_TIME", + "en": "Start without time sync (useful in AP-Only-Mode)", + "de": "Kommunikation starten ohne gültige Zeit (sinnvoll im AP Modus)" + }, + { + "token": "INV_READ_GRID_PROFILE", + "en": "Read Grid Profile", + "de": "Grid-Profil auslesen" + }, + { + "token": "INV_YIELD_EFF", + "en": "Yield Efficiency (default 1.0)", + "de": "Ertragseffizienz (Standard 1.0)" + }, + { + "token": "NTP_INTERVAL", + "en": "NTP Interval (in minutes, min. 5 minutes)", + "de": "NTP Intervall (in Minuten, min. 5 Minuten)" + }, + { + "token": "NTP_SET_SYS_TIME", + "en": "set system time", + "de": "Systemzeit setzten" + }, + { + "token": "BTN_FROM_BROWSER", + "en": "from browser", + "de": "vom Browser übernehmen" + }, + { + "token": "BTN_SYNC_NTP", + "en": "sync NTP", + "de": "NTP syncchronisieren" + }, + { + "token": "NTP_SYS_TIME", + "en": "System Time", + "de": "Systemzeit" + }, + { + "token": "SUNRISE_SUNSET", + "en": "Sunrise & Sunset", + "de": "Sonnenaufgang & -untergang" + }, + { + "token": "LATITUDE", + "en": "Latitude (decimal)", + "de": "Breitengrad (dezimal)" + }, + { + "token": "LONGITUDE", + "en": "Longitude (decimal)", + "de": "Längengrad (dezimal)" + }, + { + "token": "OFFSET_SUNRISE", + "en": "Offset (sunrise)", + "de": "Zeitversatz (Sonnenaufgang)" + }, + { + "token": "OFFSET_SUNSET", + "en": "Offset (sunset)", + "de": "Zeitversatz (Sonnenuntergang)" + }, + { + "token": "MQTT_USER", + "en": "Username (optional)", + "de": "Benutzername (optional)" + }, + { + "token": "MQTT_PASSWORD", + "en": "Password (optional)", + "de": "Passwort (optional)" + }, + { + "token": "MQTT_NOTE", + "en": "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)", + "de": "Wechselrichterdaten in fixem Intervall schicken, auch wenn es keine Änderung gab. Ein Wert von '0' deaktiviert das fixe Intervall, die Wechselrichterdaten werden übertragen, sobald neue zur Verfügung stehen. (Standard: 0)" + }, + { + "token": "DISPLAY_CONFIG", + "en": "Display Config", + "de": "Display Konfiguration" + }, + { + "token": "DISP_OFF_INV", + "en": "Turn off while inverters are offline", + "de": "schalte das Display aus, während die Wechselrichter aus sind" + }, + { + "token": "DISP_LUMINANCE", + "en": "Luminance", + "de": "Helligkeit" + }, + { + "token": "DISP_PINOUT", + "en": "Pinout", + "de": "Anschlusseinstellungen" + }, + { + "token": "BTN_REBOOT_SUCCESSFUL_SAVE", + "en": "Reboot device after successful save", + "de": "Nach erfolgreichem Speichern AhoyDTU neu starten" + }, + { + "token": "BTN_ERASE", + "en": "ERASE SETTINGS (not WiFi)", + "de": "Einstellungen zurücksetzen (nicht WiFi)" + }, + { + "token": "IM_EXPORT", + "en": "Import / Export JSON Settings", + "de": "Import / Export JSON Einstellungen" + }, + { + "token": "IMPORT", + "en": "Import", + "de": "Import" + }, + { + "token": "EXPORT", + "en": "Export", + "de": "Export" + }, + { + "token": "BTN_EXPORT", + "en": "Export settings (JSON file)", + "de": "Einstellungen exportieren (JSON Datei)" + }, + { + "token": "EXPORT_NOTE", + "en": "(only values, passwords will be skipped!)", + "de": "(nur Werte, Passwörter werden nicht mit exportiert!)" + }, + { + "token": "PIN_OFF", + "en": "off / default", + "de": "aus / Standard" + }, + { + "token": "PIN_NO_IRQ", + "en": "no IRQ!", + "de": "kein Interrupt!" + }, + { + "token": "PIN_INPUT_ONLY", + "en": "in only", + "de": "nur Eingang" + }, + { + "token": "PIN_DONT_USE", + "en": "DONT USE", + "de": "nicht benutzen" + }, + { + "token": "PIN_NOT_AVAIL", + "en": "not available", + "de": "nicht verfügbar" + }, + { + "token": "PIN_RECOMMENDED", + "en": "recommended", + "de": "empfohlen" + }, + { + "token": "PIN_EXPERIMENTAL", + "en": "experimental", + "de": "experimentell" + }, + { + "token": "PIN_LOW_ACTIVE", + "en": "low active", + "de": "low aktiv" + }, + { + "token": "PIN_HIGH_ACTIVE", + "en": "high active", + "de": "high aktiv" + }, + { + "token": "NETWORK_SCANNING", + "en": "scanning ...", + "de": "suche ..." + }, + { + "token": "ERROR", + "en": "Error:", + "de": "Fehler:" + }, + { + "token": "NTP_COMMAND_EXE", + "en": "command executed, set new time ...", + "de": "Befehl ausgeführt, setze neue Zeit ..." + }, + { + "token": "NTP_SYNCED_AT", + "en": "synced at", + "de": "syncchronisiert um" + }, + { + "token": "NTP_DIFF", + "en": "difference", + "de": "Unterschied" + }, + { + "token": "MQTT_EXE", + "en": "command executed", + "de": "Befehl ausgeführt" + }, + { + "token": "IMPORT_UPLOAD_STARTED", + "en": "upload started", + "de": "hochladen gestartet" + }, + { + "token": "INV_EDIT", + "en": "Edit", + "de": "Bearbeiten" + }, + { + "token": "INV_DELETE", + "en": "Delete", + "de": "Löschen" + }, + { + "token": "ENABLED", + "en": "enabled", + "de": "aktiviert" + }, + { + "token": "DISABLED", + "en": "disabled", + "de": "deaktiviert" + }, + { + "token": "BTN_INV_ADD", + "en": "add Inverter", + "de": "Wechselrichter hinzufuegen" + }, + { + "token": "INV_INPUT", + "en": "Input", + "de": "Eingang" + }, + { + "token": "INV_MAX_MODULE_POWER", + "en": "Max Module Power", + "de": "Maximale Panelleistung" + }, + { + "token": "INV_YIELD_CORR", + "en": "Yield Correction", + "de": "Ertragskorrektur" + }, + { + "token": "TAB_GENERAL", + "en": "General", + "de": "Allgemein" + }, + { + "token": "TAB_INPUTS", + "en": "Inputs", + "de": "Eingaenge" + }, + { + "token": "TAB_RADIO", + "en": "Radio", + "de": "Funkmodul" + }, + { + "token": "TAB_ADVANCED", + "en": "Advanced", + "de": "Erweitert" + }, + { + "token": "INV_ENABLE", + "en": "Enabled", + "de": "aktiviert" + }, + { + "token": "INV_SERIAL", + "en": "Serial", + "de": "Seriennummer" + }, + { + "token": "INV_FREQUENCY", + "en": "Frequency", + "de": "Frequenz" + }, + { + "token": "INV_POWER_LEVEL", + "en": "Power Level", + "de": "Sendeleistung" + }, + { + "token": "INV_PAUSE_DURING_NIGHT", + "en": "Pause communication during night (lat. and lon. need to be set)", + "de": "Kommunikation während der Nacht pausieren (Breiten- und Längengrad müssen gesetzt sein" + }, + { + "token": "INV_INCLUDE_MQTT_SUM", + "en": "Include inverter to sum of total (should be checked by default, MqTT only)", + "de": "Wechselrichter in Liste der aufzuaddierenden Wechselrichter aufnehmen (nur MqTT)" + }, + { + "token": "BTN_SAVE", + "en": "save", + "de": "speichern" + }, + { + "token": "INV_EDIT_MODAL", + "en": "Edit inverter", + "de": "Wechselrichter editieren" + }, + { + "token": "INV_DELETE_SURE", + "en": "do you realy want to delete inverter?", + "de": "Willst du den Wechselrichter wirklich löschen?" + }, + { + "token": "BTN_YES", + "en": "yes", + "de": "ja" + }, + { + "token": "INV_DELETE_MODAL", + "en": "Delete inverter", + "de": "Wechselrichter löschen " + }, + { + "token": "NTP_MINUTES", + "en": "minutes", + "de": "Minuten" + }, + { + "token": "LED_AT_LEAST_ONE_PRODUCING", + "en": "At least one inverter is producing", + "de": "mindestens ein Wechselrichter produziert" + }, + { + "token": "LED_MQTT_CONNECTED", + "en": "MqTT connected", + "de": "MqTT verbunden" + }, + { + "token": "LED_NIGHT_TIME", + "en": "Night time", + "de": "Nachtschaltung" + }, + { + "token": "LED_POLARITY", + "en": "LED polarity", + "de": "LED Polarität" + }, + { + "token": "LED_LUMINANCE", + "en": "LED luminance", + "de": "LED Helligkeit" + }, + { + "token": "NRF24_ENABLE", + "en": "NRF24 radio enable", + "de": "NRF24 Funkmodul aktivieren" + }, + { + "token": "CMT_ENABLE", + "en": "CMT2300A radio enable", + "de": "CMT2300A Funkmodul aktivieren" + }, + { + "token": "DISP_NONE", + "en": "None", + "de": "keins" + }, + { + "token": "DISP_TYPE", + "en": "Type", + "de": "Typ" + }, + { + "token": "DISP_ROTATION", + "en": "Rotation", + "de": "Drehung" + }, + { + "token": "DISP_SCREENSAVER_OFF", + "en": "off", + "de": "aus" + }, + { + "token": "DISP_PIXEL_SHIFT", + "en": "pixel shift", + "de": "Bildversatz" + }, + { + "token": "DISP_MOTION_SENS", + "en": "motion sensor", + "de": "Bewegungssensor" + }, + { + "token": "DISP_SCREENSAVER", + "en": "Screensaver (OLED only)", + "de": "Bildschrimschoner (nur OLED)" + }, + { + "token": "NETWORK_PLEASE_SELECT", + "en": "please select network", + "de": "bitte Netzwerk auswählen" + }, + { + "token": "NO_NETWORK_FOUND", + "en": "no network found", + "de": "kein Netzwerk gefunden" + } + ] + }, + { + "name": "system.html", + "list": [ + { + "token": "UNKNOWN", + "en": "unknown", + "de": "unbekannt" + }, + { + "token": "CONNECTED", + "en": "connected", + "de": "verbunden" + }, + { + "token": "NOT", + "en": "not", + "de": "nicht" + }, + { + "token": "IRQ_WORKING", + "en": "Interrupt Pin working", + "de": "Interrupt Pin funktoniert" + }, + { + "token": "NRF24_DATA_RATE", + "en": "NRF24 Data Rate", + "de": "NRF24 Geschwindigkeit" + }, + { + "token": "NOT_ENABLED", + "en": "not enabled", + "de": "nicht aktiviert" + }, + { + "token": "NRF24_RADIO", + "en": "Radio NRF24", + "de": "NRF24 Funkmodul" + }, + { + "token": "CMT_RADIO", + "en": "Radio CMT", + "de": "CMT2300A Funkmodul" + }, + { + "token": "TRUE", + "en": "true", + "de": "ja" + }, + { + "token": "FALSE", + "en": "false", + "de": "nein" + }, + { + "token": "ENABLED", + "en": "enabled", + "de": "aktiviert" + }, + { + "token": "SUN", + "en": "Sun", + "de": "Sonne" + }, + { + "token": "SUNRISE", + "en": "sunrise", + "de": "Sonnenaufgang" + }, + { + "token": "SUNSET", + "en": "sunset", + "de": "Sonnenuntergang" + }, + { + "token": "COMMUNICATION_START", + "en": "Communication start", + "de": "Start der Kommunikation" + }, + { + "token": "COMMUNICATION_STOP", + "en": "Communication stop", + "de": "Ende der Kommunikation" + }, + { + "token": "NIGHT_BEHAVE", + "en": "Night behaviour", + "de": "Verhalten bei Nacht" + }, + { + "token": "COMMUNICATING", + "en": "communicating", + "de": "aktiv" + } + ] + }, + { + "name": "index.html", + "list": [ + { + "token": "SUPPORT", + "en": "Support this project", + "de": "Dieses Projekt unterstützen" + }, + { + "token": "CHANGELOG", + "en": "Changelog", + "de": "Änderungshistorie" + }, + { + "token": "DISCUSS", + "en": "Discuss with us on", + "de": "Diskutiere mit uns bei" + }, + { + "token": "REPORT", + "en": "Report", + "de": "Melde " + }, + { + "token": "ISSUES", + "en": "Issues", + "de": "Fehler " + }, + { + "token": "CONTRIBUTE", + "en": "Contribute to", + "de": "Unterstütze bei" + }, + { + "token": "DOCUMENTATION", + "en": "documentation", + "de": "der Dokumentation" + }, + { + "token": "DEV_FIRMWARE", + "en": "development firmware", + "de": "Entwicklerversionen der Firmware" + }, + { + "token": "DEV_CHANGELOG", + "en": "Development Changelog", + "de": "Änderungshistorie der Entwicklerversionen" + }, + { + "token": "DON_MAKE", + "en": "make a", + "de": "Unterstüze mit" + }, + { + "token": "DON_MAKE", + "en": "make a", + "de": "Unterstüze z.B. mit" + }, + { + "token": "DONATION", + "en": "donation", + "de": "Paypal" + }, + { + "token": "COMMAND_EXE", + "en": "command executed", + "de": "Befehl ausgeführt" + }, + { + "token": "ERROR", + "en": "Error", + "de": "Fehler" + }, + { + "token": "NTP_UNREACH", + "en": "NTP timeserver unreachable", + "de": "NTP Zeitserver nicht erreichbar" + }, + { + "token": "POLLING_STOP", + "en": "Polling inverter(s), will pause at sunset", + "de": "Abfrage der Wechselrichter wird zum Sonnenuntergang stoppen" + }, + { + "token": "NIGHT_TIME", + "en": "Night time, inverter polling disabled", + "de": "Nacht, Wechselrichterabfrage deaktivert" + }, + { + "token": "PAUSED_AT", + "en": "paused at", + "de": "pausiert um" + }, + { + "token": "START_AT", + "en": "will start polling at", + "de": "wird starten um" + }, + { + "token": "DISABLED", + "en": "disabled", + "de": "deaktiviert" + }, + { + "token": "NOT_YET_AVAIL", + "en": "not yet available", + "de": "gerade nicht verfügbar" + }, + { + "token": "AVAIL", + "en": "available and is", + "de": "verfügbar und ist" + }, + { + "token": "AVAIL_NO_DATA", + "en": "available but no data was received until now", + "de": "verfügbar, aber bis jetzt keine Daten empfangen" + }, + { + "token": "NOT_PRODUCING", + "en": "not producing", + "de": "produziert nicht" + }, + { + "token": "PRODUCING", + "en": "producing", + "de": "produziert" + }, + { + "token": "INVERTER", + "en": "Inverter", + "de": "Wechselrichter" + }, + { + "token": "IS", + "en": "is", + "de": "ist" + }, + { + "token": "LAST_SUCCESS", + "en": "last successful transmission", + "de": "letzte erfolgreiche Übertragung" + }, + { + "token": "UPDATE_AVAIL", + "en": "Update available, current released version", + "de": "Update verfügbar, aktuell veröffentlichte Version" + }, + { + "token": "USING_DEV_VERSION", + "en": "You are using development version", + "de": "Du verwendest eine Entwicklerversion" + }, + { + "token": "DEV_ISSUE_RELEASE_VERSION", + "en": "In case of issues you may want to try the current stable release", + "de": "Wenn du Fehler feststellst solltest du die aktuelle Releaseversion verwenden" + }, + { + "token": "RELEASE_INSTALLED", + "en": "You are using the current stable release", + "de": "Du verwendest das aktuelle Release" + } + ] + }, + { + "name": "update.html", + "list": [ + { + "token": "SELECT_FILE", + "en": "Select firmware file", + "de": "Firmware Datei auswählen" + }, + { + "token": "BTN_UPDATE", + "en": "Update", + "de": "aktualisieren" + }, + { + "token": "BTN_UPDATE", + "en": "Update", + "de": "aktualisieren" + }, + { + "token": "DOWNLOADS", + "en": "Download latest Release and Development versions (without login)", + "de": "Lade die letzte Releaseversion oder Entwicklerversion herunter (ohne Login)" + }, + { + "token": "UPDATE_STARTED", + "en": "update started", + "de": "Aktualisierung gestartet" + } + ] + }, + { + "name": "visualization.html", + "list": [ + { + "token": "EVERY", + "en": "Every", + "de": "Alle" + }, + { + "token": "UPDATE_SECS", + "en": "seconds the values are updated", + "de": "Sekunden werden die Daten aktualisiert" + }, + { + "token": "TOTAL", + "en": "TOTAL", + "de": "GESAMT" + }, + { + "token": "AC_POWER", + "en": "AC Power", + "de": "AC Leistung" + }, + { + "token": "YIELD_DAY", + "en": "Yield Day", + "de": "Tagesertrag" + }, + { + "token": "YIELD_TOTAL", + "en": "Yield Total", + "de": "Gesamtertrag" + }, + { + "token": "MAX_POWER", + "en": "Max Power", + "de": "Maximale Leistung" + }, + { + "token": "DC_POWER", + "en": "DC Power", + "de": "DC Leistung" + }, + { + "token": "REACTIVE_POWER", + "en": "Reactive Power", + "de": "Blindleistung" + }, + { + "token": "ACTIVE_POWER_CONTROL", + "en": "Active Power Control", + "de": "Leistungsbegrenzung" + }, + { + "token": "APC", + "en": "APC", + "de": "Begr." + }, + { + "token": "ALARMS", + "en": "Alarms", + "de": "Meldungen" + }, + { + "token": "MAX_AC_POWER", + "en": "Max AC Power", + "de": "Max AC Leistung" + }, + { + "token": "DC_POWER", + "en": "DC Power", + "de": "DC Leistung" + }, + { + "token": "DC_VOLTAGE", + "en": "DC Voltage", + "de": "DC Spannung" + }, + { + "token": "AC_CURRENT", + "en": "AC Current", + "de": "AC Strom" + }, + { + "token": "FREQUENCY", + "en": "Frequency", + "de": "Frequenz" + }, + { + "token": "EFFICIENCY", + "en": "Efficiency", + "de": "Effizienz" + }, + { + "token": "POWER_FACTOR", + "en": "Power Factor", + "de": "Leistungsfaktor" + }, + { + "token": "IRRADIATION", + "en": "Irradiation", + "de": "Einstrahlung" + }, + { + "token": "DC_CURRENT", + "en": "DC Current", + "de": "DC Strom" + }, + { + "token": "LAST_RECEIVED", + "en": "Last received data requested at", + "de": "Zuletzt empfangene Daten wurden angefragt um" + }, + { + "token": "NOTHING_RECEIVED", + "en": "nothing received", + "de": "nichts empfangen" + }, + { + "token": "ALARMS_MODAL", + "en": "Alarms of inverter", + "de": "Meldungen von Wechselrichter" + }, + { + "token": "SERIAL", + "en": "Serial", + "de": "Seriennummer" + }, + { + "token": "BTN_SHOW", + "en": "show", + "de": "anzeigen" + }, + { + "token": "EVENT", + "en": "Event", + "de": "Ereignis" + }, + { + "token": "END", + "en": "End", + "de": "Ende" + }, + { + "token": "HW_NUMBER", + "en": "Hardware Number", + "de": "Hardware Nummer" + }, + { + "token": "INV_INFO", + "en": "Info for inverter", + "de": "Info für Wechselrichter" + }, + { + "token": "VALUE", + "en": "Value", + "de": "Wert" + }, + { + "token": "RANGE", + "en": "Range", + "de": "Bereich" + }, + { + "token": "DEFAULT", + "en": "Default", + "de": "Standard" + }, + { + "token": "PROFILE_NOT_READ", + "en": "Profile was not read until now, maybe turned off", + "de": "Grid-Profil wurde bis jetzt noch nicht gelesen oder das Auslesen ist nicht aktiv" + }, + { + "token": "UNKNOWN_PROFILE", + "en": "Unknown Profile", + "de": "Unbekanntes Profil" + }, + { + "token": "OPEN_ISSUE", + "en": "Please open a new issue at https://github.com/lumapu/ahoy and copy the raw data into it", + "de": "Bitte erstelle einen neuen Issue auf https://github.com/lumapu/ahoy und kopiere die Rohdaten hinein" + }, + { + "token": "PROFILE_MODAL", + "en": "Grid Profile for inverter", + "de": "Grid-Profil für Wechselrichter" + }, + { + "token": "TX_COUNT", + "en": "TX count", + "de": "Sendezähler" + }, + { + "token": "RX_SUCCESS", + "en": "RX success", + "de": "erfolgreicher Empfang" + }, + { + "token": "RX_FAIL", + "en": "RX fail", + "de": "fehlgeschlagener Empfang" + }, + { + "token": "RX_NO_ANSWER", + "en": "RX no answer", + "de": "keine Antwort" + }, + { + "token": "RX_FRAGMENTS", + "en": "RX fragments", + "de": "empfangene Fragmente" + }, + { + "token": "TX_RETRANSMITS", + "en": "TX retransmits", + "de": "erneute Sendeversuche" + }, + { + "token": "INV_LOSS_RATE", + "en": "Inverter loss rate", + "de": "Wechselrichter Empfangsqualität" + }, + { + "token": "DTU_LOSS_RATE", + "en": "DTU loss rate", + "de": "DTU Empfangsqualität" + }, + { + "token": "RADIO_STAT_MODAL", + "en": "Radio statistics for inverter", + "de": "Funkstatistik für Wechselrichter" + }, + { + "token": "LOST_1", + "en": "lost", + "de": "" + }, + { + "token": "LOST_2", + "en": "of", + "de": "von" + }, + { + "token": "LOST_3", + "en": "", + "de": "verloren" + }, + { + "token": "LIMIT_VALUE", + "en": "Limit Value", + "de": "Wert der Leistungsbegrenzung" + }, + { + "token": "KEEP_LIMIT", + "en": "Keep limit over inverter restart", + "de": "Leistungsbegrenzung dauerhaft speichern (im Wechselrichter)" + }, + { + "token": "BTN_APPLY", + "en": "Apply", + "de": "anwenden" + }, + { + "token": "CONTROL", + "en": "Control", + "de": "Ansteuern" + }, + { + "token": "RESTART", + "en": "restart", + "de": "neustarten" + }, + { + "token": "TURN_OFF", + "en": "turn off", + "de": "ausschalten" + }, + { + "token": "TURN_ON", + "en": "turn on", + "de": "anschalten" + }, + { + "token": "RESULT", + "en": "Result", + "de": "Status" + }, + { + "token": "POWER_LIMIT_MODAL", + "en": "Active Power Control for inverter", + "de": "Leistungsbegrenzung für Wechselrichter" + }, + { + "token": "CMD_RECEIVED_WAIT_ACK", + "en": "received command, waiting for inverter acknowledge ...", + "de": "Befehl erhalten, warte auf Bestäigung von Wechselrichter ..." + }, + { + "token": "COMMAND_RECEIVED", + "en": "command received", + "de": "Befehl erhalten" + }, + { + "token": "ERROR", + "en": "Error", + "de": "Fehler" + }, + { + "token": "INV_ACK", + "en": "inverter acknowledged active power control command", + "de": "Wechselrichter hat die Leistungsbegrenzung akzeptiert" + } + ] + }, + { + "name": "save.html", + "list": [ + { + "token": "SAVE_SETTINGS", + "en": "Saving settings...", + "de": "Einstellungen werden gespeichert ..." + }, + { + "token": "SUCCESS_SAVED_RELOAD", + "en": "Settings successfully saved. Automatic page reload in 3 seconds.", + "de": "Einstellungen erfolgreich gespeichert. Automatische Weiterleitung in 3 Sekunden" + }, + { + "token": "SUCCESS_SAVED_REBOOT", + "en": "Settings successfully saved. Rebooting. Automatic redirect in", + "de": "Einstellungen erfolgreich gespeichert. Automatische Weiterleitung nach Reboot in" + }, + { + "token": "SECONDS", + "en": "seconds", + "de": "Sekunden" + }, + { + "token": "FAILED_SAVE", + "en": "Failed saving settings", + "de": "Fehler beim Speichern" + } + ] + } + ] +} From 135a1f8032fba3c42cf965b7eb63fabdc320b78b Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 02:42:42 +0100 Subject: [PATCH 032/115] 0.8.47 * merge PR: add defines for retry attempts #1329 --- src/CHANGES.md | 1 + src/hm/Communication.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index c403f8b0..cddfc86a 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -7,6 +7,7 @@ * updated U8g2 to `2.35.9` * started to convert deprecated functions of new ArduinoJson `7.0.0` * started to have german translations of all variants (environments) #925 #1199 +* merge PR: add defines for retry attempts #1329 ## 0.8.46 - 2024-01-06 * improved communication diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 3c7c82a5..d0878dd5 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -119,7 +119,6 @@ class Communication : public CommQueue<> { setAttempt(); if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA); -/// statt 5:3 mState = States::WAIT; break; From 954b4ff7064abd02dd10cc19de648f491fcbebb8 Mon Sep 17 00:00:00 2001 From: you69man Date: Tue, 2 Jan 2024 11:36:14 +0100 Subject: [PATCH 033/115] implementation of power graph --- src/plugins/Display/Display.h | 3 + src/plugins/Display/Display_Mono.h | 145 ++++++++++++++++++++++ src/plugins/Display/Display_Mono_128X64.h | 74 ++++++----- src/plugins/Display/Display_Mono_84X48.h | 78 +++++++----- src/plugins/Display/Display_data.h | 2 + 5 files changed, 240 insertions(+), 62 deletions(-) diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 0cfbd710..387a6a0d 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -165,6 +165,9 @@ class Display { else mDisplayData.utcTs = 0; + mDisplayData.pGraphStartTime = mApp->getSunrise(); + mDisplayData.pGraphEndTime = mApp->getSunset(); + if (mMono ) { mMono->disp(); } diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 322e991e..1d925849 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -60,12 +60,23 @@ class DisplayMono { mLuminance = lum; mDisplay->setContrast(mLuminance); } + + monoMaintainDispSwitchState(); } protected: U8G2* mDisplay; DisplayData *mDisplayData; + float *mPgData=nullptr; + uint8_t mPgWidth=0; + uint8_t mPgHeight=0; + float mPgMaxPwr=0.0; +// float mPgMaxAvailPower = 0.0; + uint32_t mPgPeriod=0; // seconds + uint32_t mPgTimeOfDay=0; + uint8_t mPgLastPos=0; + uint8_t mType; uint16_t mDispWidth; uint16_t mDispHeight; @@ -81,9 +92,16 @@ class DisplayMono { uint8_t mExtra; int8_t mPixelshift=0; TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); + TimeMonitor mDispSwitchTime = TimeMonitor(10000, true); + uint8_t mDispSwitchState = 0; bool mDisplayActive = true; // always start with display on char mFmtText[DISP_FMT_TEXT_LEN]; + enum _dispSwitchState { + d_POWER_TEXT = 0, + d_POWER_GRAPH = 1, + }; + // Common initialization function to be called by subclasses void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { mDisplay = display; @@ -97,6 +115,133 @@ class DisplayMono { mDispHeight = mDisplay->getDisplayHeight(); } + void monoMaintainDispSwitchState(void) { + switch(mDispSwitchState) { + case d_POWER_TEXT: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = d_POWER_GRAPH; + mDispSwitchTime.startTimeMonitor(5000); + } + break; + case d_POWER_GRAPH: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = d_POWER_TEXT; + mDispSwitchTime.startTimeMonitor(10000); + } + break; + } + } + + void initPowerGraph(uint8_t width, uint8_t height) { + mPgWidth = width; + mPgHeight = height; + mPgData = new float[mPgWidth]; + //memset(mPgData, 0, mPgWidth); + resetPowerGraph(); +/* + Inverter<> *iv; + mPgMaxAvailPower = 0; + uint8_t nInv = mSys->getNumInverters(); + for (uint8_t i = 0; i < nInv; i++) { + iv = mSys->getInverterByPos(i); + if (iv == NULL) + continue; + for (uint8_t ch = 0; ch < 6; ch++) { + mPgMaxAvailPower += iv->config->chMaxPwr[ch]; + } + } + DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ + } + + void resetPowerGraph() { + if (mPgData != nullptr) { + mPgMaxPwr = 0.0; + mPgLastPos = 0; + for (uint8_t i = 0; i < mPgWidth; i++) + mPgData[i] = 0.0; + } + } + + uint8_t sss2pgpos(uint seconds_since_start) { + return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); + } + + void calcPowerGraphValues() { + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis + uint32_t oldTimeOfDay = mPgTimeOfDay; + mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time + if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data + resetPowerGraph(); + mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day + } + + void addPowerGraphEntry(float val) { + if (mDisplayData->utcTs > 0) { // precondition: utc time available + calcPowerGraphValues(); + //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 + mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); + mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis + } + } + + uint8_t getPowerGraphXpos(uint8_t p) { // + if ((p <= mPgLastPos) && (mPgLastPos > 0)) + return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis + else + return(0); + } + + uint8_t getPowerGraphYpos(uint8_t p) { + if (p < mPgWidth) + //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height + return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height + else + return(0); + } + + void plotPowerGraph(uint8_t xoff, uint8_t yoff) { + // draw axes + mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis + mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis + + // draw X scale + tmElements_t tm; + breakTime(mDisplayData->pGraphEndTime, tm); + uint8_t endHourPg = tm.Hour; + breakTime(mDisplayData->utcTs, tm); + uint8_t endHour = std::min(endHourPg, tm.Hour); + breakTime(mDisplayData->pGraphStartTime, tm); + tm.Hour += 1; + tm.Minute = 0; + tm.Second = 0; + for (; tm.Hour <= endHour; tm.Hour++) { + uint8_t x_pos_screen = getPowerGraphXpos(sss2pgpos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis + mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); + } + + // draw Y scale + uint16_t scale_y = 10; + uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr)); + if (maxpwr_int > 100) + scale_y = 100; + for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) { + uint8_t ypos = yoff - static_cast(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis + mDisplay->drawPixel(xoff + 1, ypos); + } + + // draw curve + for (uint8_t i = 1; i <= mPgLastPos; i++) { + mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), + xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + } + + // print max power value + mDisplay->setFont(u8g2_font_4x6_tr); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); + mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); + } + + // pixelshift screensaver with wipe effect void calcPixelShift(int range) { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index afb581dd..8f762dde 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -34,6 +34,8 @@ class DisplayMono128X64 : public DisplayMono { } calcLinePositions(); + initPowerGraph(mDispWidth - 20, mLineYOffsets[4] - mLineYOffsets[1]); + printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); printText(mDisplayData->version, l_Version, 0xff); @@ -61,23 +63,16 @@ class DisplayMono128X64 : public DisplayMono { // calculate current pixelshift for pixelshift screensaver calcPixelShift(pixelShiftRange); - // print total power + // add new power data to power graph if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - - printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); + addPowerGraphEntry(mDisplayData->totalPower); } // print Date and time if (0 != mDisplayData->utcTs) printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - // dynamic status bar, alternatively: + // alternatively: // print ip address if (!(mExtra % 5) && (mDisplayData->ipAddress)) { snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); @@ -115,22 +110,41 @@ class DisplayMono128X64 : public DisplayMono { mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol } - // print yields - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol + if (mDispSwitchState == d_POWER_TEXT) { - if (mDisplayData->totalYieldDay > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_YieldDay, 0xff); + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - if (mDisplayData->totalYieldTotal > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); - printText(mFmtText, l_YieldTotal, 0xff); + printText(mFmtText, l_TotalPower, 0xff); + } else { + printText("offline", l_TotalPower, 0xff); + } + + // print yields + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol + + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_YieldDay, 0xff); + + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); + printText(mFmtText, l_YieldTotal, 0xff); + + } else { + // plot power graph + plotPowerGraph(10 + mPixelshift, mLineYOffsets[4] - 1); + } // draw dynamic RSSI bars int xoffs; @@ -142,10 +156,11 @@ class DisplayMono128X64 : public DisplayMono { for (int i = 0; i < 4; i++) { int radio_rssi_threshold = -60 - i * 10; int wifi_rssi_threshold = -60 - i * 10; + uint8_t barwidth = std::min(4 - i, 3); if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + mDisplay->drawBox(mDispWidth - barwidth - xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); } // draw dynamic antenna and WiFi symbols mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); @@ -160,9 +175,6 @@ class DisplayMono128X64 : public DisplayMono { mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); mDisplay->sendBuffer(); - - mDisplay->sendBuffer(); - mExtra++; } @@ -198,8 +210,8 @@ class DisplayMono128X64 : public DisplayMono { mLineYOffsets[i] = yOff; dsc = mDisplay->getDescent(); yOff -= dsc; - if (l_Time==i) // prevent time and status line to touch - yOff+=1; // -> one pixels space + if (l_Time == i) // prevent time and status line to touch + yOff++; // -> one pixels space i++; } while(l_MAX_LINES>i); } diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index 7e5c157f..7f0281ed 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -5,7 +5,6 @@ #pragma once #include "Display_Mono.h" -#include "../../utils/dbg.h" class DisplayMono84X48 : public DisplayMono { public: @@ -23,6 +22,9 @@ class DisplayMono84X48 : public DisplayMono { u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData); calcLinePositions(); + + initPowerGraph(mDispWidth - 16, mLineYOffsets[4] - mLineYOffsets[1] - 2); + printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); printText(mDisplayData->version, l_Version, 0xff); @@ -45,16 +47,9 @@ class DisplayMono84X48 : public DisplayMono { mDisplay->drawPixel(mDispWidth-1, mDispHeight-1); */ - // print total power + // add new power data to power graph if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - - printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); + addPowerGraphEntry(mDisplayData->totalPower); } // print Date and time @@ -80,31 +75,51 @@ class DisplayMono84X48 : public DisplayMono { printText(mFmtText, l_Status, 0xff); } - // print yields - printText("\x88", l_YieldDay, 10); // day symbol - printText("\x83", l_YieldTotal, 10); // total symbol + if (mDispSwitchState == d_POWER_TEXT) { - if (mDisplayData->totalYieldDay > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_YieldDay, 0xff); + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - if (mDisplayData->totalYieldTotal > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); - printText(mFmtText, l_YieldTotal, 0xff); + printText(mFmtText, l_TotalPower, 0xff); + } else { + printText("offline", l_TotalPower, 0xff); + } + + // print day yield + printText("\x88", l_YieldDay, 10); // day symbol + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_YieldDay, 0xff); + + // print total yield + printText("\x83", l_YieldTotal, 10); // total symbol + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); + printText(mFmtText, l_YieldTotal, 0xff); + + } else { + // plot power graph + plotPowerGraph(8, mLineYOffsets[4] - 1); + } - // draw dynamic Nokia RSSI bars + // draw dynamic RSSI bars int rssi_bar_height = 7; - for (int i=0; i<4;i++) { - int radio_rssi_threshold = -60 - i*10; // radio rssi not yet tested in reality! - int wifi_rssi_threshold = -60 - i*10; + for (int i = 0; i < 4; i++) { + int radio_rssi_threshold = -60 - i * 10; + int wifi_rssi_threshold = -60 - i * 10; + uint8_t barwidth = std::min(4 - i, 3); if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(0, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height); + mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth-4+i, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height); + mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); } // draw dynamic antenna and WiFi symbols @@ -150,7 +165,7 @@ class DisplayMono84X48 : public DisplayMono { yOff += asc; mLineYOffsets[i] = yOff; dsc = mDisplay->getDescent(); - if (l_TotalPower!=i) // power line needs no descent spacing + if (l_TotalPower != i) // power line needs no descent spacing yOff -= dsc; yOff++; // instead lets spend one pixel space between all lines i++; @@ -158,7 +173,8 @@ class DisplayMono84X48 : public DisplayMono { } inline void setLineFont(uint8_t line) { - if ((line == l_TotalPower) || (line == l_Ahoy)) + if ((line == l_TotalPower) || + (line == l_Ahoy)) mDisplay->setFont(u8g2_font_logisoso16_tr); else mDisplay->setFont(u8g2_font_5x8_symbols_ahoy); diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h index a7a7ecee..2017403a 100644 --- a/src/plugins/Display/Display_data.h +++ b/src/plugins/Display/Display_data.h @@ -9,6 +9,8 @@ struct DisplayData { float totalYieldDay=0.0f; // indicate day yield (Wh) float totalYieldTotal=0.0f; // indicate total yield (kWh) uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized + uint32_t pGraphStartTime=0; // starttime for power graph (e.g. sunRise) + uint32_t pGraphEndTime=0; // starttime for power graph (e.g. sunSet) uint8_t nrProducing=0; // indicate number of producing inverters uint8_t nrSleeping=0; // indicate number of sleeping inverters bool WifiSymbol = false; // indicate if WiFi is connected From 4496981b3b9eba17231eb0c83052f745f6e21d89 Mon Sep 17 00:00:00 2001 From: you69man Date: Sat, 6 Jan 2024 20:39:17 +0100 Subject: [PATCH 034/115] add option for show ratio --- src/config/settings.h | 4 ++++ src/plugins/Display/Display.h | 8 ++++--- src/plugins/Display/Display_Mono.h | 27 ++++++++++++++++------- src/plugins/Display/Display_Mono_128X32.h | 3 ++- src/plugins/Display/Display_Mono_128X64.h | 3 ++- src/plugins/Display/Display_Mono_64X48.h | 3 ++- src/plugins/Display/Display_Mono_84X48.h | 3 ++- src/web/RestApi.h | 25 +++++++++++---------- src/web/html/setup.html | 11 +++++++++ src/web/web.h | 1 + 10 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/config/settings.h b/src/config/settings.h index 8ce71167..83e2eaa3 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -165,6 +165,7 @@ typedef struct { uint8_t type; bool pwrSaveAtIvOffline; uint8_t screenSaver; + uint8_t graph_ratio; uint8_t rot; //uint16_t wakeUp; //uint16_t sleepAt; @@ -461,6 +462,7 @@ class settings { mCfg.plugin.display.pwrSaveAtIvOffline = false; mCfg.plugin.display.contrast = 60; mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility + mCfg.plugin.display.graph_ratio = 50; mCfg.plugin.display.rot = 0; mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL @@ -697,6 +699,7 @@ class settings { disp[F("type")] = mCfg.plugin.display.type; disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; disp[F("screenSaver")] = mCfg.plugin.display.screenSaver; + disp[F("graph_ratio")] = mCfg.plugin.display.graph_ratio; disp[F("rotation")] = mCfg.plugin.display.rot; //disp[F("wake")] = mCfg.plugin.display.wakeUp; //disp[F("sleep")] = mCfg.plugin.display.sleepAt; @@ -713,6 +716,7 @@ class settings { getVal(disp, F("type"), &mCfg.plugin.display.type); getVal(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline); getVal(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver); + getVal(disp, F("graph_ratio"), &mCfg.plugin.display.graph_ratio); getVal(disp, F("rotation"), &mCfg.plugin.display.rot); //mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")]; diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 387a6a0d..0b06cc58 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -54,7 +54,7 @@ class Display { default: mMono = NULL; break; } if(mMono) { - mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast); + mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast, mCfg->graph_ratio); mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData); } @@ -75,10 +75,12 @@ class Display { } void tickerSecond() { + bool request_refresh = false; + if (mMono != NULL) - mMono->loop(mCfg->contrast, motionSensorActive()); + request_refresh = mMono->loop(mCfg->contrast, motionSensorActive()); - if (mNewPayload || (((++mLoopCnt) % 5) == 0)) { + if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) { DataScreen(); mNewPayload = false; mLoopCnt = 0; diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 1d925849..76447ce3 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -26,12 +26,12 @@ class DisplayMono { DisplayMono() {}; virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; - virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) = 0; + virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) = 0; virtual void disp(void) = 0; // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor // can be overridden by subclasses - virtual void loop(uint8_t lum, bool motion) { + virtual bool loop(uint8_t lum, bool motion) { bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor @@ -61,7 +61,7 @@ class DisplayMono { mDisplay->setContrast(mLuminance); } - monoMaintainDispSwitchState(); + return(monoMaintainDispSwitchState()); } protected: @@ -84,6 +84,7 @@ class DisplayMono { bool mEnPowerSave; uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor uint8_t mLuminance; + uint8_t mGraphRatio; uint8_t mLoopCnt; uint8_t mLineXOffsets[5] = {}; @@ -92,8 +93,8 @@ class DisplayMono { uint8_t mExtra; int8_t mPixelshift=0; TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); - TimeMonitor mDispSwitchTime = TimeMonitor(10000, true); - uint8_t mDispSwitchState = 0; + TimeMonitor mDispSwitchTime = TimeMonitor(); + uint8_t mDispSwitchState; bool mDisplayActive = true; // always start with display on char mFmtText[DISP_FMT_TEXT_LEN]; @@ -113,23 +114,33 @@ class DisplayMono { mDisplay->clearBuffer(); mDispWidth = mDisplay->getDisplayWidth(); mDispHeight = mDisplay->getDisplayHeight(); + mDispSwitchTime.stopTimeMonitor(); + mDispSwitchState = d_POWER_TEXT; + if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode + mDispSwitchState = d_POWER_GRAPH; + else if (mGraphRatio != 0) + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start time monitor only if ratio is neither 0 nor 100 } - void monoMaintainDispSwitchState(void) { + bool monoMaintainDispSwitchState(void) { + bool change = false; switch(mDispSwitchState) { case d_POWER_TEXT: if (mDispSwitchTime.isTimeout()) { mDispSwitchState = d_POWER_GRAPH; - mDispSwitchTime.startTimeMonitor(5000); + mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms + change = true; } break; case d_POWER_GRAPH: if (mDispSwitchTime.isTimeout()) { mDispSwitchState = d_POWER_TEXT; - mDispSwitchTime.startTimeMonitor(10000); + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); + change = true; } break; } + return change; } void initPowerGraph(uint8_t width, uint8_t height) { diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index fa0cacdf..edc79b75 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -12,10 +12,11 @@ class DisplayMono128X32 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; + mGraphRatio = graph_ratio; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 8f762dde..c578f9a4 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -12,10 +12,11 @@ class DisplayMono128X64 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; + mGraphRatio = graph_ratio; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 68cac96f..5d1bf6ac 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -12,10 +12,11 @@ class DisplayMono64X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; + mGraphRatio = graph_ratio; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index 7f0281ed..a996f52d 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -12,10 +12,11 @@ class DisplayMono84X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; + mGraphRatio = graph_ratio; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/web/RestApi.h b/src/web/RestApi.h index de4fe8e3..7ef4349b 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -671,18 +671,19 @@ class RestApi { } 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_screensaver")] = (uint8_t)mConfig->plugin.display.screenSaver; - 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; - obj[F("pir_pin")] = mConfig->plugin.display.pirPin; + obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; + obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; + obj[F("disp_screensaver")] = (uint8_t)mConfig->plugin.display.screenSaver; + obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; + obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; + obj[F("disp_graph_ratio")] = (uint8_t)mConfig->plugin.display.graph_ratio; + 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; + obj[F("pir_pin")] = mConfig->plugin.display.pirPin; } void getMqttInfo(JsonObject obj) { diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 4903db16..4759f893 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -294,6 +294,15 @@

{#DISP_PINOUT}

+

Graph options

+
+
Graph Size
+
+
+
+
Show ratio (0-100 %)
+
+
@@ -1087,6 +1096,8 @@ hideDispPins(pins, parseInt(dtype_sel.value)) }); + document.getElementsByName("disp_graph_ratio")[0].value = obj["disp_graph_ratio"]; + hideDispPins(pins, obj.disp_typ); } diff --git a/src/web/web.h b/src/web/web.h index eae106a9..1b4f5702 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -584,6 +584,7 @@ class Web { // display mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); mConfig->plugin.display.screenSaver = request->arg("disp_screensaver").toInt(); + mConfig->plugin.display.graph_ratio = request->arg("disp_graph_ratio").toInt(); 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(); From c82cc1c77ec2b4df3678d8bb7a829d0d03025408 Mon Sep 17 00:00:00 2001 From: you69man Date: Sat, 6 Jan 2024 22:15:29 +0100 Subject: [PATCH 035/115] add option for graph size --- src/config/settings.h | 4 + src/plugins/Display/Display.h | 2 +- src/plugins/Display/Display_Mono.h | 5 +- src/plugins/Display/Display_Mono_128X32.h | 3 +- src/plugins/Display/Display_Mono_128X64.h | 135 ++++++++++++++-------- src/plugins/Display/Display_Mono_64X48.h | 3 +- src/plugins/Display/Display_Mono_84X48.h | 84 ++++++++++---- src/web/RestApi.h | 1 + src/web/html/setup.html | 15 ++- src/web/web.h | 1 + 10 files changed, 172 insertions(+), 81 deletions(-) diff --git a/src/config/settings.h b/src/config/settings.h index 83e2eaa3..28739db3 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -166,6 +166,7 @@ typedef struct { bool pwrSaveAtIvOffline; uint8_t screenSaver; uint8_t graph_ratio; + uint8_t graph_size; uint8_t rot; //uint16_t wakeUp; //uint16_t sleepAt; @@ -463,6 +464,7 @@ class settings { mCfg.plugin.display.contrast = 60; mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility mCfg.plugin.display.graph_ratio = 50; + mCfg.plugin.display.graph_size = 2; mCfg.plugin.display.rot = 0; mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL @@ -700,6 +702,7 @@ class settings { disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; disp[F("screenSaver")] = mCfg.plugin.display.screenSaver; disp[F("graph_ratio")] = mCfg.plugin.display.graph_ratio; + disp[F("graph_size")] = mCfg.plugin.display.graph_size; disp[F("rotation")] = mCfg.plugin.display.rot; //disp[F("wake")] = mCfg.plugin.display.wakeUp; //disp[F("sleep")] = mCfg.plugin.display.sleepAt; @@ -717,6 +720,7 @@ class settings { getVal(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline); getVal(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver); getVal(disp, F("graph_ratio"), &mCfg.plugin.display.graph_ratio); + getVal(disp, F("graph_size"), &mCfg.plugin.display.graph_size); getVal(disp, F("rotation"), &mCfg.plugin.display.rot); //mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")]; diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 0b06cc58..6c8bb9cb 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -54,7 +54,7 @@ class Display { default: mMono = NULL; break; } if(mMono) { - mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast, mCfg->graph_ratio); + mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast, mCfg->graph_ratio, mCfg->graph_size); mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData); } diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 76447ce3..c5aa2ed3 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -26,7 +26,7 @@ class DisplayMono { DisplayMono() {}; virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; - virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) = 0; + virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 0; virtual void disp(void) = 0; // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor @@ -85,6 +85,7 @@ class DisplayMono { uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor uint8_t mLuminance; uint8_t mGraphRatio; + uint8_t mGraphSize; uint8_t mLoopCnt; uint8_t mLineXOffsets[5] = {}; @@ -119,7 +120,7 @@ class DisplayMono { if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode mDispSwitchState = d_POWER_GRAPH; else if (mGraphRatio != 0) - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start time monitor only if ratio is neither 0 nor 100 + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 } bool monoMaintainDispSwitchState(void) { diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index edc79b75..1b3927b3 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -12,11 +12,12 @@ class DisplayMono128X32 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; mGraphRatio = graph_ratio; + mGraphSize = graph_size; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index c578f9a4..32459c89 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -12,11 +12,12 @@ class DisplayMono128X64 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; mGraphRatio = graph_ratio; + mGraphSize = graph_size; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { @@ -35,7 +36,33 @@ class DisplayMono128X64 : public DisplayMono { } calcLinePositions(); - initPowerGraph(mDispWidth - 20, mLineYOffsets[4] - mLineYOffsets[1]); + switch(mGraphSize) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + case 0: + graph_first_line = 1; + graph_last_line = 2; + break; + case 1: + graph_first_line = 2; + graph_last_line = 3; + break; + case 2: + graph_first_line = 1; + graph_last_line = 3; + break; + case 3: + graph_first_line = 2; + graph_last_line = 4; + break; + case 4: + default: + graph_first_line = 1; + graph_last_line = 4; + break; + } + + widthShrink = (mScreenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver + + initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); @@ -73,46 +100,47 @@ class DisplayMono128X64 : public DisplayMono { if (0 != mDisplayData->utcTs) printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - // alternatively: - // print ip address - if (!(mExtra % 5) && (mDisplayData->ipAddress)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); - printText(mFmtText, l_Status, 0xff); - } - // print status of inverters - else { - sun_pos = -1; - moon_pos = -1; - setLineFont(l_Status); - if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); - else if (0 == mDisplayData->nrSleeping) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - sun_pos = 0; - } - else if (0 == mDisplayData->nrProducing) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - moon_pos = 0; + if (showLine(l_Status)) { + // alternatively: + // print ip address + if (!(mExtra % 5) && (mDisplayData->ipAddress)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); + printText(mFmtText, l_Status, 0xff); } + // print status of inverters else { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); - sun_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); - moon_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " "); + sun_pos = -1; + moon_pos = -1; + setLineFont(l_Status); + if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + else if (0 == mDisplayData->nrSleeping) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + sun_pos = 0; + } + else if (0 == mDisplayData->nrProducing) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + moon_pos = 0; + } + else { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); + sun_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); + moon_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " "); + } + printText(mFmtText, l_Status, 0xff); + + pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; + mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); + if (sun_pos!=-1) + mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol + if (moon_pos!=-1) + mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol } - printText(mFmtText, l_Status, 0xff); - - pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; - mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); - if (sun_pos!=-1) - mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol - if (moon_pos!=-1) - mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol } - if (mDispSwitchState == d_POWER_TEXT) { - + if (showLine(l_TotalPower)) { // print total power if (mDisplayData->nrProducing > 0) { if (mDisplayData->totalPower > 9999.0) @@ -124,8 +152,10 @@ class DisplayMono128X64 : public DisplayMono { } else { printText("offline", l_TotalPower, 0xff); } + } - // print yields + if (showLine(l_YieldDay)) { + // print day yield mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol @@ -135,45 +165,44 @@ class DisplayMono128X64 : public DisplayMono { else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); printText(mFmtText, l_YieldDay, 0xff); + } + if (showLine(l_YieldTotal)) { + // print total yield if (mDisplayData->totalYieldTotal > 9999.0) snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); printText(mFmtText, l_YieldTotal, 0xff); + } - } else { + if (mDispSwitchState == d_POWER_GRAPH) { // plot power graph - plotPowerGraph(10 + mPixelshift, mLineYOffsets[4] - 1); + plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1); } // draw dynamic RSSI bars - int xoffs; - if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver - xoffs = pixelShiftRange/2; - else - xoffs = 0; int rssi_bar_height = 9; for (int i = 0; i < 4; i++) { int radio_rssi_threshold = -60 - i * 10; int wifi_rssi_threshold = -60 - i * 10; uint8_t barwidth = std::min(4 - i, 3); if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); + mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth - barwidth - xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); + mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); } // draw dynamic antenna and WiFi symbols mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); char sym[]=" "; sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF - mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->drawStr(widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym); if (mDisplayData->MQTTSymbol) sym[0] = 'J'; // MQTT else sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi - mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym); mDisplay->sendBuffer(); mExtra++; @@ -197,7 +226,11 @@ class DisplayMono128X64 : public DisplayMono { l_MAX_LINES = 5, }; + uint8_t graph_first_line; + uint8_t graph_last_line; + const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!) + uint8_t widthShrink; void calcLinePositions() { uint8_t yOff = 0; @@ -239,4 +272,8 @@ class DisplayMono128X64 : public DisplayMono { dispX += mPixelshift; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } + + bool showLine(uint8_t line) { + return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + } }; diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 5d1bf6ac..e55f5ac3 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -12,11 +12,12 @@ class DisplayMono64X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; mGraphRatio = graph_ratio; + mGraphSize = graph_size; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index a996f52d..ee2aebd5 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -12,11 +12,12 @@ class DisplayMono84X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio) { + void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { mEnPowerSave = enPowerSave; mScreenSaver = screenSaver; mLuminance = lum; mGraphRatio = graph_ratio; + mGraphSize = graph_size; } void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { @@ -24,7 +25,31 @@ class DisplayMono84X48 : public DisplayMono { monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData); calcLinePositions(); - initPowerGraph(mDispWidth - 16, mLineYOffsets[4] - mLineYOffsets[1] - 2); + switch(mGraphSize) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + case 0: + graph_first_line = 1; + graph_last_line = 2; + break; + case 1: + graph_first_line = 2; + graph_last_line = 3; + break; + case 2: + graph_first_line = 1; + graph_last_line = 3; + break; + case 3: + graph_first_line = 2; + graph_last_line = 4; + break; + case 4: + default: + graph_first_line = 1; + graph_last_line = 4; + break; + } + + initPowerGraph(mDispWidth - 16, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); @@ -57,27 +82,28 @@ class DisplayMono84X48 : public DisplayMono { if (0 != mDisplayData->utcTs) printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - // alternatively: - // print ip address - if (!(mExtra % 5) && (mDisplayData->ipAddress)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); - printText(mFmtText, l_Status, 0xff); - } - // print status of inverters - else { - if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); - else if (0 == mDisplayData->nrSleeping) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol - else if (0 == mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x87"); // moon symbol - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping); - printText(mFmtText, l_Status, 0xff); + if (showLine(l_Status)) { + // alternatively: + // print ip address + if (!(mExtra % 5) && (mDisplayData->ipAddress)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); + printText(mFmtText, l_Status, 0xff); + } + // print status of inverters + else { + if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + else if (0 == mDisplayData->nrSleeping) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol + else if (0 == mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x87"); // moon symbol + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping); + printText(mFmtText, l_Status, 0xff); + } } - if (mDispSwitchState == d_POWER_TEXT) { - + if (showLine(l_TotalPower)) { // print total power if (mDisplayData->nrProducing > 0) { if (mDisplayData->totalPower > 9999.0) @@ -89,7 +115,9 @@ class DisplayMono84X48 : public DisplayMono { } else { printText("offline", l_TotalPower, 0xff); } + } + if (showLine(l_YieldDay)) { // print day yield printText("\x88", l_YieldDay, 10); // day symbol if (mDisplayData->totalYieldDay > 9999.0) @@ -97,7 +125,9 @@ class DisplayMono84X48 : public DisplayMono { else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); printText(mFmtText, l_YieldDay, 0xff); + } + if (showLine(l_YieldTotal)) { // print total yield printText("\x83", l_YieldTotal, 10); // total symbol if (mDisplayData->totalYieldTotal > 9999.0) @@ -105,10 +135,11 @@ class DisplayMono84X48 : public DisplayMono { else snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); printText(mFmtText, l_YieldTotal, 0xff); + } - } else { + if (mDispSwitchState == d_POWER_GRAPH) { // plot power graph - plotPowerGraph(8, mLineYOffsets[4] - 1); + plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1); } // draw dynamic RSSI bars @@ -155,6 +186,9 @@ class DisplayMono84X48 : public DisplayMono { l_MAX_LINES = 5, }; + uint8_t graph_first_line; + uint8_t graph_last_line; + void calcLinePositions() { uint8_t yOff = 0; uint8_t i = 0; @@ -191,6 +225,10 @@ class DisplayMono84X48 : public DisplayMono { dispX = col; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } + + bool showLine(uint8_t line) { + return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + } }; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 7ef4349b..3d4a6c0d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -677,6 +677,7 @@ class RestApi { obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; obj[F("disp_graph_ratio")] = (uint8_t)mConfig->plugin.display.graph_ratio; + obj[F("disp_graph_size")] = (uint8_t)mConfig->plugin.display.graph_size; 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; diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 4759f893..6d0b3710 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -295,10 +295,7 @@

Graph options

-
-
Graph Size
-
-
+
Show ratio (0-100 %)
@@ -1098,6 +1095,16 @@ document.getElementsByName("disp_graph_ratio")[0].value = obj["disp_graph_ratio"]; + var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + var graph_size_sel = sel("disp_graph_size", opts2, obj["disp_graph_size"]); + graph_size_sel.id = 'disp_graph_size'; + document.getElementById("graphSize").append( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, "Graph size"), + ml("div", {class: "col-12 col-sm-9"}, graph_size_sel) + ]) + ); + hideDispPins(pins, obj.disp_typ); } diff --git a/src/web/web.h b/src/web/web.h index 1b4f5702..38ecf171 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -585,6 +585,7 @@ class Web { mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); mConfig->plugin.display.screenSaver = request->arg("disp_screensaver").toInt(); mConfig->plugin.display.graph_ratio = request->arg("disp_graph_ratio").toInt(); + mConfig->plugin.display.graph_size = request->arg("disp_graph_size").toInt(); 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(); From 17df96ed680f609b351b3f2a2c693dbd6844c31f Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 12:18:40 +0100 Subject: [PATCH 036/115] github action test --- .github/workflows/compile_development.yml | 102 +++++++++++++++++----- src/utils/helper.cpp | 2 +- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 4b688fca..37d3950b 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -5,14 +5,85 @@ on: branches: development* paths-ignore: - '**.md' # Do no build on *.md changes + jobs: - build: + check: runs-on: ubuntu-latest + if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03' + continue-on-error: true + steps: + - uses: actions/checkout@v3 + build-en: + needs: check + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + variant: + - esp8266 + - esp8266-prometheus + - esp8285 + - esp32-wroom32 + - esp32-wroom32-prometheus + - esp32-wroom32-ethernet + - esp32-s2-mini + - esp32-c3-mini + - opendtufusion + - opendtufusion-ethernet steps: - uses: actions/checkout@v3 + - uses: benjlevesque/short-sha@v2.1 + id: short-sha + with: + length: 7 + + - name: Cache Pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v3 with: - ref: development03 + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Setup Python + uses: actions/setup-python@v4.3.0 + with: + python-version: "3.x" + + - name: Install PlatformIO + run: | + python -m pip install setuptools --upgrade pip + pip install --upgrade platformio + + - name: Run PlatformIO + run: pio run -d src -e ${{ matrix.variant }} + + build-de: + needs: check + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + variant: + - esp8266-de + - esp8266-prometheus-de + - esp8285-de + - esp32-wroom32-de + - esp32-wroom32-prometheus-de + - esp32-wroom32-ethernet-de + - esp32-s2-mini-de + - esp32-c3-mini-de + - opendtufusion-de + - opendtufusion-ethernet-de + steps: + - uses: actions/checkout@v3 - uses: benjlevesque/short-sha@v2.1 id: short-sha with: @@ -43,29 +114,12 @@ jobs: pip install --upgrade platformio - name: Run PlatformIO - run: > - pio run -d src - --environment esp8266 - --environment esp8266-prometheus - --environment esp8285 - --environment esp32-wroom32 - --environment esp32-wroom32-prometheus - --environment esp32-wroom32-ethernet - --environment esp32-s2-mini - --environment esp32-c3-mini - --environment opendtufusion - --environment opendtufusion-ethernet - --environment esp8266-de - --environment esp8266-prometheus-de - --environment esp8285-de - --environment esp32-wroom32-de - --environment esp32-wroom32-prometheus-de - --environment esp32-wroom32-ethernet-de - --environment esp32-s2-mini-de - --environment esp32-c3-mini-de - --environment opendtufusion-de - --environment opendtufusion-ethernet-de + run: pio run -d src -e ${{ matrix.variant }} + deploy: + needs: [build-en, build-de] + runs-on: ubuntu-latest + steps: - name: Copy boot_app0.bin run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 9aea1054..5d68adc4 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -76,7 +76,7 @@ namespace ah { sprintf(str, "n/a"); else { t = (t + (millis() % 1000)) / 1000; - sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), millis() % 1000); + sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), (uint16_t)(millis() % 1000)); } return String(str); } From 72d678e32b4dad00df3c1d3d8d9514f6131e2c95 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 12:30:26 +0100 Subject: [PATCH 037/115] github action test --- .github/workflows/compile_development.yml | 48 ++++++++++++++++------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 37d3950b..e3fa489d 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -65,6 +65,13 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Create Artifact + uses: actions/upload-artifact@v3 + with: + name: ahoydtu_dev + path: | + src/firmware/* + build-de: needs: check runs-on: ubuntu-latest @@ -116,12 +123,25 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Create Artifact + uses: actions/upload-artifact@v3 + with: + name: ahoydtu_dev + path: | + src/firmware/* + deploy: needs: [build-en, build-de] runs-on: ubuntu-latest steps: - - name: Copy boot_app0.bin - run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin + #- name: Copy boot_app0.bin + # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin + + - name: Get Artifacts + - uses: actions/download-artifact@master + with: + name: ahoydtu_dev + path: src/firmware/* - name: Rename Binary files id: rename-binary-files @@ -135,18 +155,18 @@ jobs: env: VERSION: ${{ steps.rename-binary-files.outputs.name }} - - name: Create Manifest - working-directory: src - run: python ../scripts/buildManifest.py - - - name: Create Artifact - uses: actions/upload-artifact@v3 - with: - name: ahoydtu_dev - path: | - src/firmware/* - src/User_Manual.md - src/install.html +# - name: Create Manifest +# working-directory: src +# run: python ../scripts/buildManifest.py +# +# - name: Create Artifact +# uses: actions/upload-artifact@v3 +# with: +# name: ahoydtu_dev +# path: | +# src/firmware/* +# src/User_Manual.md +# src/install.html - name: Rename firmware directory run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }} From c66089cb47d581826e832f7c2025812a253e9a58 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 12:31:49 +0100 Subject: [PATCH 038/115] github action test --- .github/workflows/compile_development.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index e3fa489d..197d1e37 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -138,10 +138,10 @@ jobs: # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin - name: Get Artifacts - - uses: actions/download-artifact@master - with: - name: ahoydtu_dev - path: src/firmware/* + uses: actions/download-artifact@master + with: + name: ahoydtu_dev + path: src/firmware/* - name: Rename Binary files id: rename-binary-files From 8d81db44d2cb0893f73e66163bc6f1295ba59287 Mon Sep 17 00:00:00 2001 From: kscholty Date: Sun, 7 Jan 2024 18:07:19 +0100 Subject: [PATCH 039/115] Added better pin selection for S2 (mini) and improved default pins for S2 and C3 --- src/platformio.ini | 20 ++++++++++++++++++++ src/web/html/setup.html | 14 +++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/platformio.ini b/src/platformio.ini index e9eb282b..a2397596 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -195,6 +195,11 @@ build_flags = ${env.build_flags} -DDEF_NRF_MISO_PIN=9 -DDEF_NRF_MOSI_PIN=11 -DDEF_NRF_SCLK_PIN=7 + -DDEF_CMT_CSB=16 + -DDEF_CMT_FCSB=18 + -DDEF_CMT_IRQ=33 + -DDEF_CMT_SDIO=35 + -DDEF_CMT_SCLK=37 monitor_filters = esp32_exception_decoder @@ -209,6 +214,11 @@ build_flags = ${env.build_flags} -DDEF_NRF_MISO_PIN=9 -DDEF_NRF_MOSI_PIN=11 -DDEF_NRF_SCLK_PIN=7 + -DDEF_CMT_CSB=16 + -DDEF_CMT_FCSB=18 + -DDEF_CMT_IRQ=33 + -DDEF_CMT_SDIO=35 + -DDEF_CMT_SCLK=37 -DLANG_DE monitor_filters = esp32_exception_decoder @@ -224,6 +234,11 @@ build_flags = ${env.build_flags} -DDEF_NRF_MISO_PIN=3 -DDEF_NRF_MOSI_PIN=4 -DDEF_NRF_SCLK_PIN=2 + -DDEF_CMT_CSB=255 + -DDEF_CMT_FCSB=255 + -DDEF_CMT_IRQ=255 + -DDEF_CMT_SDIO=255 + -DDEF_CMT_SCLK=255 monitor_filters = esp32_exception_decoder @@ -238,6 +253,11 @@ build_flags = ${env.build_flags} -DDEF_NRF_MISO_PIN=3 -DDEF_NRF_MOSI_PIN=4 -DDEF_NRF_SCLK_PIN=2 + -DDEF_CMT_CSB=255 + -DDEF_CMT_FCSB=255 + -DDEF_CMT_IRQ=255 + -DDEF_CMT_SDIO=255 + -DDEF_CMT_SCLK=255 -DLANG_DE monitor_filters = esp32_exception_decoder diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 4903db16..c503339b 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -387,7 +387,7 @@ [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] ]; - var esp32s3pins = [ + var esp32sXpins = [ [255, "off / default"], [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], [1, "GPIO1"], @@ -418,8 +418,8 @@ [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], - [33, "GPIO33 (not exposed on WROOM modules)"], - [34, "GPIO34 (not exposed on WROOM modules)"], + [33, "GPIO33 (not exposed on S3-WROOM modules)"], + [34, "GPIO34 (not exposed on S3-WROOM modules)"], [35, "GPIO35"], [36, "GPIO36"], [37, "GPIO37"], @@ -902,7 +902,7 @@ var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; @@ -938,7 +938,7 @@ var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ @@ -971,7 +971,7 @@ var e = document.getElementById("cmt"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var pinList = esp32pins; - if ("ESP32-S3" == system["chip_model"]) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; en.checked = obj["en"]; @@ -1005,7 +1005,7 @@ var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; + if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ From c9135e06e1bd25029fc06ec075fa29d10c7e0ec0 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 20:30:52 +0100 Subject: [PATCH 040/115] github action test --- .github/workflows/compile_development.yml | 23 +-- scripts/getVersion.py | 187 +++++----------------- 2 files changed, 50 insertions(+), 160 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 197d1e37..d20c4627 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -65,6 +65,9 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Rename Firmware + run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + - name: Create Artifact uses: actions/upload-artifact@v3 with: @@ -123,12 +126,15 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Rename Firmware + run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + - name: Create Artifact uses: actions/upload-artifact@v3 with: name: ahoydtu_dev path: | - src/firmware/* + firmware/* deploy: needs: [build-en, build-de] @@ -141,19 +147,18 @@ jobs: uses: actions/download-artifact@master with: name: ahoydtu_dev - path: src/firmware/* + path: firmware/* - - name: Rename Binary files - id: rename-binary-files - working-directory: src - run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT + - name: Get Version from code + id: version_name + run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Set Version uses: cschleiden/replace-tokens@v1 with: files: tools/esp8266/User_Manual.md env: - VERSION: ${{ steps.rename-binary-files.outputs.name }} + VERSION: ${{ steps.version_name.outputs.name }} # - name: Create Manifest # working-directory: src @@ -169,12 +174,12 @@ jobs: # src/install.html - name: Rename firmware directory - run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }} + run: mv src/firmware src/${{ steps.version_name.outputs.name }} - name: Deploy uses: nogsantos/scp-deploy@master with: - src: src/${{ steps.rename-binary-files.outputs.name }}/ + src: src/${{ steps.version_name.outputs.name }}/ host: ${{ secrets.FW_SSH_HOST }} remote: ${{ secrets.FW_SSH_DIR }}/dev port: ${{ secrets.FW_SSH_PORT }} diff --git a/scripts/getVersion.py b/scripts/getVersion.py index ce34d26e..8d6d1cf8 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -2,6 +2,7 @@ import os import shutil import gzip from datetime import date +import sys def genOtaBin(path): arr = [] @@ -32,8 +33,8 @@ def gzip_bin(bin_file, gzip_file): with gzip.open(gzip_file, "wb", compresslevel = 9) as f: shutil.copyfileobj(fp, f) -def readVersion(path, infile): - f = open(path + infile, "r") +def getVersion(path_define): + f = open(path_define, "r") lines = f.readlines() f.close() @@ -48,160 +49,44 @@ def readVersion(path, infile): if(p != -1): version += line[p+13:].rstrip() + "." versionnumber += line[p+13:].rstrip() + "." - - os.mkdir(path + "firmware/") - os.mkdir(path + "firmware/ESP8266/") - os.mkdir(path + "firmware/ESP8285/") - os.mkdir(path + "firmware/ESP32/") - os.mkdir(path + "firmware/ESP32-S2/") - os.mkdir(path + "firmware/ESP32-S3/") - os.mkdir(path + "firmware/ESP32-C3/") - os.mkdir(path + "firmware/ESP32-S3-ETH/") - sha = os.getenv("SHA",default="sha") -## ENGLISH VERSIONS - versionout = version[:-1] + "_" + sha + "_esp8266.bin" - src = path + ".pio/build/esp8266/firmware.bin" - dst = path + "firmware/ESP8266/" + versionout - os.rename(src, dst) + return [version, versionnumber] - versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" - src = path + ".pio/build/esp8266-prometheus/firmware.bin" - dst = path + "firmware/ESP8266/" + versionout - os.rename(src, dst) +def renameFw(path_define, env): + version = getVersion(path_define)[0] - versionout = version[:-1] + "_" + sha + "_esp8285.bin" - src = path + ".pio/build/esp8285/firmware.bin" - dst = path + "firmware/ESP8285/" + versionout - os.rename(src, dst) - gzip_bin(dst, dst + ".gz") - - versionout = version[:-1] + "_" + sha + "_esp32.bin" - src = path + ".pio/build/esp32-wroom32/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" - src = path + ".pio/build/esp32-wroom32-prometheus/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32_ethernet.bin" - src = path + ".pio/build/esp32-wroom32-ethernet/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s2-mini.bin" - src = path + ".pio/build/esp32-s2-mini/firmware.bin" - dst = path + "firmware/ESP32-S2/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32c3-mini.bin" - src = path + ".pio/build/esp32-c3-mini/firmware.bin" - dst = path + "firmware/ESP32-C3/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s3.bin" - src = path + ".pio/build/opendtufusion/firmware.bin" - dst = path + "firmware/ESP32-S3/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s3_ethernet.bin" - src = path + ".pio/build/opendtufusion-ethernet/firmware.bin" - dst = path + "firmware/ESP32-S3-ETH/" + versionout - os.rename(src, dst) - -## GERMAN VERSIONS - versionout = version[:-1] + "_" + sha + "_esp8266-de.bin" - src = path + ".pio/build/esp8266-de/firmware.bin" - dst = path + "firmware/ESP8266/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp8266_prometheus-de.bin" - src = path + ".pio/build/esp8266-prometheus-de/firmware.bin" - dst = path + "firmware/ESP8266/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp8285-de.bin" - src = path + ".pio/build/esp8285-de/firmware.bin" - dst = path + "firmware/ESP8285/" + versionout - os.rename(src, dst) - gzip_bin(dst, dst + ".gz") - - versionout = version[:-1] + "_" + sha + "_esp32-de.bin" - src = path + ".pio/build/esp32-wroom32-de/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32_prometheus-de.bin" - src = path + ".pio/build/esp32-wroom32-prometheus-de/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32_ethernet-de.bin" - src = path + ".pio/build/esp32-wroom32-ethernet-de/firmware.bin" - dst = path + "firmware/ESP32/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s2-mini-de.bin" - src = path + ".pio/build/esp32-s2-mini-de/firmware.bin" - dst = path + "firmware/ESP32-S2/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32c3-mini-de.bin" - src = path + ".pio/build/esp32-c3-mini-de/firmware.bin" - dst = path + "firmware/ESP32-C3/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s3-de.bin" - src = path + ".pio/build/opendtufusion-de/firmware.bin" - dst = path + "firmware/ESP32-S3/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_" + sha + "_esp32s3_ethernet-de.bin" - src = path + ".pio/build/opendtufusion-ethernet-de/firmware.bin" - dst = path + "firmware/ESP32-S3-ETH/" + versionout - os.rename(src, dst) - -## BOOTLOADER AND PARTITIONS - # other ESP32 bin files - src = path + ".pio/build/esp32-wroom32/" - dst = path + "firmware/ESP32/" - os.rename(src + "bootloader.bin", dst + "bootloader.bin") - os.rename(src + "partitions.bin", dst + "partitions.bin") - genOtaBin(dst) - - # other ESP32-S2 bin files - src = path + ".pio/build/esp32-s2-mini/" - dst = path + "firmware/ESP32-S2/" - os.rename(src + "bootloader.bin", dst + "bootloader.bin") - os.rename(src + "partitions.bin", dst + "partitions.bin") - genOtaBin(dst) + os.mkdir("firmware/") + fwDir = "" + if env[:7] == "esp8266": + fwDir = "ESP8266/" + elif env[:7] == "esp8285": + fwDir = "ESP8285/" + elif env[:7] == "esp32-w": + fwDir = "ESP32/" + elif env[:8] == "esp32-s2": + fwDir = "ESP32-S2/" + elif env[:4] == "open": + fwDir = "ESP32-S3/" + elif env[:8] == "esp32-c3": + fwDir = "ESP32-C3/" + os.mkdir("firmware/" + fwDir) + sha = os.getenv("SHA",default="sha") - # other ESP32-C3 bin files - src = path + ".pio/build/esp32-c3-mini/" - dst = path + "firmware/ESP32-C3/" - os.rename(src + "bootloader.bin", dst + "bootloader.bin") - os.rename(src + "partitions.bin", dst + "partitions.bin") - genOtaBin(dst) + dst = "firmware/" + fwDir + fname = version[:-1] + "_" + sha + "_" + env + ".bin" - # other ESP32-S3 bin files - src = path + ".pio/build/opendtufusion/" - dst = path + "firmware/ESP32-S3/" - os.rename(src + "bootloader.bin", dst + "bootloader.bin") - os.rename(src + "partitions.bin", dst + "partitions.bin") - genOtaBin(dst) + os.rename(".pio/build/" + env + "/firmware.bin", dst + fname) - # other ESP32-S3-Eth bin files - src = path + ".pio/build/opendtufusion-ethernet/" - dst = path + "firmware/ESP32-S3-ETH/" - os.rename(src + "bootloader.bin", dst + "bootloader.bin") - os.rename(src + "partitions.bin", dst + "partitions.bin") - genOtaBin(dst) + if env[:5] == "esp32": + os.rename(".pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin") + os.rename(".pio/build/" + env + "/partitions.bin", dst + "partitions.bin") + genOtaBin(dst) - os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html") + if env[:7] == "esp8285": + gzip_bin(dst + fname, dst + fname[:-4] + ".gz") - print("name=" + versionnumber[:-1] ) - - -readVersion("", "defines.h") +if len(sys.argv) == 1: + print("name=" + getVersion("src/defines.h")[1][:-1]) +else: + # arg1: environment + renameFw("src/defines.h", sys.argv[1]) From 585fd5ca7e6010b6c4cc96f4794a7d131414f6f8 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 20:34:45 +0100 Subject: [PATCH 041/115] github action test --- .github/workflows/compile_development.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index d20c4627..c00029a2 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -66,7 +66,7 @@ jobs: run: pio run -d src -e ${{ matrix.variant }} - name: Rename Firmware - run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Create Artifact uses: actions/upload-artifact@v3 @@ -127,7 +127,7 @@ jobs: run: pio run -d src -e ${{ matrix.variant }} - name: Rename Firmware - run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Create Artifact uses: actions/upload-artifact@v3 @@ -151,7 +151,7 @@ jobs: - name: Get Version from code id: version_name - run: python ../scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Set Version uses: cschleiden/replace-tokens@v1 From 7f425e094a1335eb657ad5e35f250d1f9b47de43 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 20:40:25 +0100 Subject: [PATCH 042/115] github action test --- scripts/getVersion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/getVersion.py b/scripts/getVersion.py index 8d6d1cf8..a60a772d 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -75,11 +75,11 @@ def renameFw(path_define, env): dst = "firmware/" + fwDir fname = version[:-1] + "_" + sha + "_" + env + ".bin" - os.rename(".pio/build/" + env + "/firmware.bin", dst + fname) + os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname) if env[:5] == "esp32": - os.rename(".pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin") - os.rename(".pio/build/" + env + "/partitions.bin", dst + "partitions.bin") + os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin") + os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin") genOtaBin(dst) if env[:7] == "esp8285": From 63d15f217f1f40542623b3a3ee675c4ab026c9ad Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 20:45:31 +0100 Subject: [PATCH 043/115] github action test --- .github/workflows/compile_development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index c00029a2..47976029 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -73,7 +73,7 @@ jobs: with: name: ahoydtu_dev path: | - src/firmware/* + firmware/* build-de: needs: check From 57ba359fa33a9dcb28a4bffbd64bfd5491d5ee3b Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 20:55:15 +0100 Subject: [PATCH 044/115] github action test --- .github/workflows/compile_development.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 47976029..6d7991c0 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -145,9 +145,6 @@ jobs: - name: Get Artifacts uses: actions/download-artifact@master - with: - name: ahoydtu_dev - path: firmware/* - name: Get Version from code id: version_name @@ -174,12 +171,12 @@ jobs: # src/install.html - name: Rename firmware directory - run: mv src/firmware src/${{ steps.version_name.outputs.name }} + run: mv firmware ${{ steps.version_name.outputs.name }} - name: Deploy uses: nogsantos/scp-deploy@master with: - src: src/${{ steps.version_name.outputs.name }}/ + src: ${{ steps.version_name.outputs.name }}/ host: ${{ secrets.FW_SSH_HOST }} remote: ${{ secrets.FW_SSH_DIR }}/dev port: ${{ secrets.FW_SSH_PORT }} From 4b3a670294004ac9b47edb22a0469ba8e8960d86 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:00:12 +0100 Subject: [PATCH 045/115] github action test --- .github/workflows/compile_development.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 6d7991c0..0725cb2c 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -140,6 +140,7 @@ jobs: needs: [build-en, build-de] runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 #- name: Copy boot_app0.bin # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin From b70c9bfe26cfb2d53c2311a78f946eb9831de999 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:09:38 +0100 Subject: [PATCH 046/115] github action test --- .github/workflows/compile_development.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 0725cb2c..61a060e8 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -145,7 +145,12 @@ jobs: # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin - name: Get Artifacts - uses: actions/download-artifact@master + uses: actions/download-artifact@v4 + path: firmware + with: + name: ahoydtu_dev + - name: Display structure of downloaded files + run: ls -R - name: Get Version from code id: version_name From 78c37625388fd579f16cddb8f546596049819533 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:10:59 +0100 Subject: [PATCH 047/115] github action test --- .github/workflows/compile_development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 61a060e8..9cc1f530 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -146,9 +146,9 @@ jobs: - name: Get Artifacts uses: actions/download-artifact@v4 - path: firmware with: name: ahoydtu_dev + path: firmware - name: Display structure of downloaded files run: ls -R From fabe410967c5ec8abf3b9f4d2f06cd375ae2bcca Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:26:05 +0100 Subject: [PATCH 048/115] github action test --- .github/workflows/compile_development.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 9cc1f530..a4fd3c57 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -69,11 +69,10 @@ jobs: run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Create Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ahoydtu_dev - path: | - firmware/* + path: firmware/* build-de: needs: check @@ -130,11 +129,10 @@ jobs: run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT - name: Create Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ahoydtu_dev - path: | - firmware/* + path: firmware/* deploy: needs: [build-en, build-de] From 9889cc334d4a1a4c39a70293c9886551882f3b8a Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:31:00 +0100 Subject: [PATCH 049/115] github action test --- .github/workflows/compile_development.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index a4fd3c57..364a5962 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -131,7 +131,7 @@ jobs: - name: Create Artifact uses: actions/upload-artifact@v4 with: - name: ahoydtu_dev + name: dev-${{ matrix.variant }} path: firmware/* deploy: @@ -144,9 +144,6 @@ jobs: - name: Get Artifacts uses: actions/download-artifact@v4 - with: - name: ahoydtu_dev - path: firmware - name: Display structure of downloaded files run: ls -R From 6677116badef88874795f8659cf8cceb4ad23d55 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 21:42:52 +0100 Subject: [PATCH 050/115] github action test --- .github/workflows/compile_development.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 364a5962..2e3dcb31 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -71,7 +71,7 @@ jobs: - name: Create Artifact uses: actions/upload-artifact@v4 with: - name: ahoydtu_dev + name: dev-${{ matrix.variant }} path: firmware/* build-de: @@ -144,8 +144,11 @@ jobs: - name: Get Artifacts uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: firmware - name: Display structure of downloaded files - run: ls -R + run: ls -R firmware - name: Get Version from code id: version_name From 3c5be9ae35470169f04842c4b1ece9067a59efd5 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 7 Jan 2024 22:25:59 +0100 Subject: [PATCH 051/115] 0.8.48 * merge PR: pin selection for ESP-32 S2 #1334 * merge PR: enhancement: power graph display option #1330 --- .github/workflows/compile_development.yml | 15 - src/CHANGES.md | 4 + src/defines.h | 2 +- src/plugins/Display/Display_Mono.h | 459 +++++++++++----------- src/plugins/Display/Display_Mono_128X64.h | 4 +- src/plugins/Display/Display_Mono_84X48.h | 4 +- 6 files changed, 238 insertions(+), 250 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 2e3dcb31..7eaf9b34 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -147,8 +147,6 @@ jobs: with: merge-multiple: true path: firmware - - name: Display structure of downloaded files - run: ls -R firmware - name: Get Version from code id: version_name @@ -161,19 +159,6 @@ jobs: env: VERSION: ${{ steps.version_name.outputs.name }} -# - name: Create Manifest -# working-directory: src -# run: python ../scripts/buildManifest.py -# -# - name: Create Artifact -# uses: actions/upload-artifact@v3 -# with: -# name: ahoydtu_dev -# path: | -# src/firmware/* -# src/User_Manual.md -# src/install.html - - name: Rename firmware directory run: mv firmware ${{ steps.version_name.outputs.name }} diff --git a/src/CHANGES.md b/src/CHANGES.md index cddfc86a..5073a2c9 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.48 - 2024-01-07 +* merge PR: pin selection for ESP-32 S2 #1334 +* merge PR: enhancement: power graph display option #1330 + ## 0.8.47 - 2024-01-06 * reduce GxEPD2 lib to compile faster * upgraded GxEPD2 lib to `1.5.3` diff --git a/src/defines.h b/src/defines.h index 87b4101d..bd5cc7ff 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 47 +#define VERSION_PATCH 48 //------------------------------------- typedef struct { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index c5aa2ed3..c7235815 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -22,256 +22,255 @@ #include "../../utils/timemonitor.h" class DisplayMono { - public: - DisplayMono() {}; - - virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; - virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 0; - virtual void disp(void) = 0; - - // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor - // can be overridden by subclasses - virtual bool loop(uint8_t lum, bool motion) { - - bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && - ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor - - if (mDisplayActive) { - if (!dispConditions) { - if (mDisplayTime.isTimeout()) { // switch display off after timeout - mDisplayActive = false; - mDisplay->setPowerSave(true); - DBGPRINTLN("**** Display off ****"); + public: + DisplayMono() {}; + + virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; + virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 0; + virtual void disp(void) = 0; + + // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor + // can be overridden by subclasses + virtual bool loop(uint8_t lum, bool motion) { + + bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && + ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor + + if (mDisplayActive) { + if (!dispConditions) { + if (mDisplayTime.isTimeout()) { // switch display off after timeout + mDisplayActive = false; + mDisplay->setPowerSave(true); + DBGPRINTLN("**** Display off ****"); + } } + else + mDisplayTime.reStartTimeMonitor(); // keep display on } - else - mDisplayTime.reStartTimeMonitor(); // keep display on - } - else { - if (dispConditions) { - mDisplayActive = true; - mDisplayTime.reStartTimeMonitor(); // switch display on - mDisplay->setPowerSave(false); - DBGPRINTLN("**** Display on ****"); + else { + if (dispConditions) { + mDisplayActive = true; + mDisplayTime.reStartTimeMonitor(); // switch display on + mDisplay->setPowerSave(false); + DBGPRINTLN("**** Display on ****"); + } + } + + if(mLuminance != lum) { + mLuminance = lum; + mDisplay->setContrast(mLuminance); } - } - if(mLuminance != lum) { - mLuminance = lum; + return(monoMaintainDispSwitchState()); + } + + protected: + enum class DispSwitchState { + TEXT, + GRAPH + }; + + protected: + U8G2* mDisplay; + DisplayData *mDisplayData; + + float *mPgData = nullptr; + uint8_t mPgWidth = 0; + uint8_t mPgHeight = 0; + float mPgMaxPwr = 0.0; + uint32_t mPgPeriod = 0; // seconds + uint32_t mPgTimeOfDay = 0; + uint8_t mPgLastPos = 0; + + uint8_t mType; + uint16_t mDispWidth; + uint16_t mDispHeight; + + bool mEnPowerSave; + uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor + uint8_t mLuminance; + uint8_t mGraphRatio; + uint8_t mGraphSize; + + uint8_t mLoopCnt; + uint8_t mLineXOffsets[5] = {}; + uint8_t mLineYOffsets[5] = {}; + + uint8_t mExtra; + int8_t mPixelshift=0; + TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); + TimeMonitor mDispSwitchTime = TimeMonitor(); + DispSwitchState mDispSwitchState = DispSwitchState::TEXT; + bool mDisplayActive = true; // always start with display on + char mFmtText[DISP_FMT_TEXT_LEN]; + + // Common initialization function to be called by subclasses + void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { + mDisplay = display; + mType = type; + mDisplayData = displayData; + mDisplay->begin(); + mDisplay->setPowerSave(false); // always start with display on mDisplay->setContrast(mLuminance); - } - - return(monoMaintainDispSwitchState()); - } - - protected: - U8G2* mDisplay; - DisplayData *mDisplayData; - - float *mPgData=nullptr; - uint8_t mPgWidth=0; - uint8_t mPgHeight=0; - float mPgMaxPwr=0.0; -// float mPgMaxAvailPower = 0.0; - uint32_t mPgPeriod=0; // seconds - uint32_t mPgTimeOfDay=0; - uint8_t mPgLastPos=0; - - uint8_t mType; - uint16_t mDispWidth; - uint16_t mDispHeight; - - bool mEnPowerSave; - uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor - uint8_t mLuminance; - uint8_t mGraphRatio; - uint8_t mGraphSize; - - uint8_t mLoopCnt; - uint8_t mLineXOffsets[5] = {}; - uint8_t mLineYOffsets[5] = {}; - - uint8_t mExtra; - int8_t mPixelshift=0; - TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); - TimeMonitor mDispSwitchTime = TimeMonitor(); - uint8_t mDispSwitchState; - bool mDisplayActive = true; // always start with display on - char mFmtText[DISP_FMT_TEXT_LEN]; - - enum _dispSwitchState { - d_POWER_TEXT = 0, - d_POWER_GRAPH = 1, - }; - - // Common initialization function to be called by subclasses - void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { - mDisplay = display; - mType = type; - mDisplayData = displayData; - mDisplay->begin(); - mDisplay->setPowerSave(false); // always start with display on - mDisplay->setContrast(mLuminance); - mDisplay->clearBuffer(); - mDispWidth = mDisplay->getDisplayWidth(); - mDispHeight = mDisplay->getDisplayHeight(); - mDispSwitchTime.stopTimeMonitor(); - mDispSwitchState = d_POWER_TEXT; - if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode - mDispSwitchState = d_POWER_GRAPH; - else if (mGraphRatio != 0) - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 - } - - bool monoMaintainDispSwitchState(void) { - bool change = false; - switch(mDispSwitchState) { - case d_POWER_TEXT: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = d_POWER_GRAPH; - mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms - change = true; - } - break; - case d_POWER_GRAPH: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = d_POWER_TEXT; - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); - change = true; - } - break; - } - return change; - } - - void initPowerGraph(uint8_t width, uint8_t height) { - mPgWidth = width; - mPgHeight = height; - mPgData = new float[mPgWidth]; - //memset(mPgData, 0, mPgWidth); - resetPowerGraph(); + mDisplay->clearBuffer(); + mDispWidth = mDisplay->getDisplayWidth(); + mDispHeight = mDisplay->getDisplayHeight(); + mDispSwitchTime.stopTimeMonitor(); + if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode + mDispSwitchState = DispSwitchState::GRAPH; + else if (mGraphRatio != 0) + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 + } + + bool monoMaintainDispSwitchState(void) { + bool change = false; + switch(mDispSwitchState) { + case DispSwitchState::TEXT: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::GRAPH; + mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms + change = true; + } + break; + case DispSwitchState::GRAPH: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::TEXT; + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); + change = true; + } + break; + } + return change; + } + + void initPowerGraph(uint8_t width, uint8_t height) { + mPgWidth = width; + mPgHeight = height; + mPgData = new float[mPgWidth]; + //memset(mPgData, 0, mPgWidth); + resetPowerGraph(); /* - Inverter<> *iv; - mPgMaxAvailPower = 0; - uint8_t nInv = mSys->getNumInverters(); - for (uint8_t i = 0; i < nInv; i++) { - iv = mSys->getInverterByPos(i); - if (iv == NULL) - continue; - for (uint8_t ch = 0; ch < 6; ch++) { - mPgMaxAvailPower += iv->config->chMaxPwr[ch]; + Inverter<> *iv; + mPgMaxAvailPower = 0; + uint8_t nInv = mSys->getNumInverters(); + for (uint8_t i = 0; i < nInv; i++) { + iv = mSys->getInverterByPos(i); + if (iv == NULL) + continue; + for (uint8_t ch = 0; ch < 6; ch++) { + mPgMaxAvailPower += iv->config->chMaxPwr[ch]; + } } - } - DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ - } - - void resetPowerGraph() { - if (mPgData != nullptr) { - mPgMaxPwr = 0.0; - mPgLastPos = 0; - for (uint8_t i = 0; i < mPgWidth; i++) - mPgData[i] = 0.0; + DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ } - } - uint8_t sss2pgpos(uint seconds_since_start) { - return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); - } + void resetPowerGraph() { + if (mPgData != nullptr) { + mPgMaxPwr = 0.0; + mPgLastPos = 0; + for (uint8_t i = 0; i < mPgWidth; i++) + mPgData[i] = 0.0; + } + } - void calcPowerGraphValues() { - mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis - uint32_t oldTimeOfDay = mPgTimeOfDay; - mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time - if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data - resetPowerGraph(); - mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day - } - - void addPowerGraphEntry(float val) { - if (mDisplayData->utcTs > 0) { // precondition: utc time available - calcPowerGraphValues(); - //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 - mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); - mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis - } - } - - uint8_t getPowerGraphXpos(uint8_t p) { // - if ((p <= mPgLastPos) && (mPgLastPos > 0)) - return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - else - return(0); - } - - uint8_t getPowerGraphYpos(uint8_t p) { - if (p < mPgWidth) - //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height - return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - else - return(0); - } - - void plotPowerGraph(uint8_t xoff, uint8_t yoff) { - // draw axes - mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis - mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis - - // draw X scale - tmElements_t tm; - breakTime(mDisplayData->pGraphEndTime, tm); - uint8_t endHourPg = tm.Hour; - breakTime(mDisplayData->utcTs, tm); - uint8_t endHour = std::min(endHourPg, tm.Hour); - breakTime(mDisplayData->pGraphStartTime, tm); - tm.Hour += 1; - tm.Minute = 0; - tm.Second = 0; - for (; tm.Hour <= endHour; tm.Hour++) { - uint8_t x_pos_screen = getPowerGraphXpos(sss2pgpos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis - mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); + uint8_t sss2pgpos(uint seconds_since_start) { + return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); } - // draw Y scale - uint16_t scale_y = 10; - uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr)); - if (maxpwr_int > 100) - scale_y = 100; - for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) { - uint8_t ypos = yoff - static_cast(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis - mDisplay->drawPixel(xoff + 1, ypos); + void calcPowerGraphValues() { + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis + uint32_t oldTimeOfDay = mPgTimeOfDay; + mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time + if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data + resetPowerGraph(); + mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day + } + + void addPowerGraphEntry(float val) { + if (mDisplayData->utcTs > 0) { // precondition: utc time available + calcPowerGraphValues(); + //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 + mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); + mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis + } } - // draw curve - for (uint8_t i = 1; i <= mPgLastPos; i++) { - mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), - xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + uint8_t getPowerGraphXpos(uint8_t p) { // + if ((p <= mPgLastPos) && (mPgLastPos > 0)) + return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis + else + return(0); } - // print max power value - mDisplay->setFont(u8g2_font_4x6_tr); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); - mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); - } - - // pixelshift screensaver with wipe effect - void calcPixelShift(int range) { - int8_t mod = (millis() / 10000) % ((range >> 1) << 2); - mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; - } + uint8_t getPowerGraphYpos(uint8_t p) { + if (p < mPgWidth) + //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height + return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height + else + return(0); + } + + void plotPowerGraph(uint8_t xoff, uint8_t yoff) { + // draw axes + mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis + mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis + + // draw X scale + tmElements_t tm; + breakTime(mDisplayData->pGraphEndTime, tm); + uint8_t endHourPg = tm.Hour; + breakTime(mDisplayData->utcTs, tm); + uint8_t endHour = std::min(endHourPg, tm.Hour); + breakTime(mDisplayData->pGraphStartTime, tm); + tm.Hour += 1; + tm.Minute = 0; + tm.Second = 0; + for (; tm.Hour <= endHour; tm.Hour++) { + uint8_t x_pos_screen = getPowerGraphXpos(sss2pgpos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis + mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); + } + + // draw Y scale + uint16_t scale_y = 10; + uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr)); + if (maxpwr_int > 100) + scale_y = 100; + for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) { + uint8_t ypos = yoff - static_cast(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis + mDisplay->drawPixel(xoff + 1, ypos); + } + + // draw curve + for (uint8_t i = 1; i <= mPgLastPos; i++) { + mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), + xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + } + + // print max power value + mDisplay->setFont(u8g2_font_4x6_tr); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); + mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); + } + + // pixelshift screensaver with wipe effect + void calcPixelShift(int range) { + int8_t mod = (millis() / 10000) % ((range >> 1) << 2); + mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; + } }; /* adapted 5x8 Font for low-res displays with symbols Symbols: - \x80 ... antenna - \x81 ... WiFi - \x82 ... suncurve - \x83 ... sum/sigma - \x84 ... antenna crossed - \x85 ... WiFi crossed - \x86 ... sun - \x87 ... moon - \x88 ... calendar/day - \x89 ... MQTT */ + \x80 ... antenna + \x81 ... WiFi + \x82 ... suncurve + \x83 ... sum/sigma + \x84 ... antenna crossed + \x85 ... WiFi crossed + \x86 ... sun + \x87 ... moon + \x88 ... calendar/day + \x89 ... MQTT */ const uint8_t u8g2_font_5x8_symbols_ahoy[1049] U8G2_FONT_SECTION("u8g2_font_5x8_symbols_ahoy") = "j\0\3\2\4\4\3\4\5\10\10\0\377\6\377\6\0\1\61\2b\4\0 \5\0\304\11!\7a\306" "\212!\11\42\7\63\335\212\304\22#\16u\304\232R\222\14JePJI\2$\14u\304\252l\251m" diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 32459c89..c8338691 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -176,7 +176,7 @@ class DisplayMono128X64 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == d_POWER_GRAPH) { + if (mDispSwitchState == DispSwitchState::GRAPH) { // plot power graph plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1); } @@ -274,6 +274,6 @@ class DisplayMono128X64 : public DisplayMono { } bool showLine(uint8_t line) { - return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line))); } }; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index ee2aebd5..b195e17c 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -137,7 +137,7 @@ class DisplayMono84X48 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == d_POWER_GRAPH) { + if (mDispSwitchState == DispSwitchState::GRAPH) { // plot power graph plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1); } @@ -227,7 +227,7 @@ class DisplayMono84X48 : public DisplayMono { } bool showLine(uint8_t line) { - return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line))); } }; From 2b4de00a894141048d94b2d713689f62753df1c8 Mon Sep 17 00:00:00 2001 From: VArt67 <132200455+VArt67@users.noreply.github.com> Date: Sun, 7 Jan 2024 23:25:50 +0100 Subject: [PATCH 052/115] * Added history charts to web and to Display_Mono_128x64 --- src/app.cpp | 10 +- src/app.h | 6 + src/appInterface.h | 4 + src/config/config.h | 3 + src/config/settings.h | 7 +- src/plugins/Display/Display.h | 18 +- src/plugins/Display/Display_Mono.h | 8 + src/plugins/Display/Display_Mono_128X64.h | 261 ++++++++++++++-------- src/plugins/Display/Display_data.h | 2 + src/plugins/history.cpp | 94 ++++++++ src/plugins/history.h | 86 +++++++ src/web/RestApi.h | 121 +++++++++- src/web/html/api.js | 2 +- src/web/html/history.html | 135 +++++++++++ src/web/html/includes/nav.html | 1 + src/web/html/style.css | 20 ++ src/web/web.h | 10 +- 17 files changed, 691 insertions(+), 97 deletions(-) create mode 100644 src/plugins/history.cpp create mode 100644 src/plugins/history.h create mode 100644 src/web/html/history.html diff --git a/src/app.cpp b/src/app.cpp index 0bba14da..9ec3d86c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,7 +7,7 @@ #include "app.h" #include "utils/sun.h" - +#include "plugins/history.h" //----------------------------------------------------------------------------- app::app() : ah::Scheduler {} {} @@ -91,6 +91,11 @@ void app::setup() { #endif #endif + mTotalPowerHistory = new TotalPowerHistory(); + mTotalPowerHistory->setup(this, &mSys, mConfig); + mYieldDayHistory = new YieldDayHistory(); + mYieldDayHistory->setup(this, &mSys, mConfig); + mPubSerial.setup(mConfig, &mSys, &mTimestamp); #if !defined(ETHERNET) @@ -148,6 +153,9 @@ void app::regularTickers(void) { #if !defined(ETHERNET) //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif + + everySec(std::bind(&TotalPowerHistory::tickerSecond, mTotalPowerHistory), "totalPowerHistory"); + everySec(std::bind(&YieldDayHistory::tickerSecond, mYieldDayHistory), "yieldDayHistory"); } #if defined(ETHERNET) diff --git a/src/app.h b/src/app.h index a24cccb3..029f0f1e 100644 --- a/src/app.h +++ b/src/app.h @@ -55,6 +55,7 @@ typedef PubSerial PubSerialType; #include "plugins/Display/Display_data.h" typedef Display DisplayType; #endif +#include "plugins/history.h" class app : public IApp, public ah::Scheduler { public: @@ -243,6 +244,9 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } + TotalPowerHistory *getTotalPowerHistoryPtr() { return mTotalPowerHistory; }; + YieldDayHistory *getYieldDayHistoryPtr() { return mYieldDayHistory; }; + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true @@ -350,6 +354,8 @@ class app : public IApp, public ah::Scheduler { DisplayType mDisplay; DisplayData mDispData; #endif + TotalPowerHistory *mTotalPowerHistory; + YieldDayHistory *mYieldDayHistory; }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 34dc5ddc..89d52d9e 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -14,6 +14,8 @@ #include "ESPAsyncWebServer.h" #endif +class TotalPowerHistory; +class YieldDayHistory; //#include "hms/hmsRadio.h" #if defined(ESP32) //typedef CmtRadio> CmtRadioType; @@ -63,6 +65,8 @@ class IApp { virtual bool getProtection(AsyncWebServerRequest *request) = 0; + virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0; + virtual YieldDayHistory *getYieldDayHistoryPtr() = 0; virtual void* getRadioObj(bool nrf) = 0; }; diff --git a/src/config/config.h b/src/config/config.h index 2cb9bcd3..393c78e3 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -36,6 +36,9 @@ // CONFIGURATION - COMPILE TIME //------------------------------------- +// Draw power chart in MONO-Display +#define DISPLAY_CHART 1 + // ethernet #if defined(ETHERNET) diff --git a/src/config/settings.h b/src/config/settings.h index fe2ad1b0..ec61d733 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -41,6 +41,7 @@ #define PROT_MASK_SYSTEM 0x0020 #define PROT_MASK_API 0x0040 #define PROT_MASK_MQTT 0x0080 +#define PROT_MASK_HISTORY 0x0100 #define DEF_PROT_INDEX 0x0001 #define DEF_PROT_LIVE 0x0000 @@ -50,7 +51,7 @@ #define DEF_PROT_SYSTEM 0x0020 #define DEF_PROT_API 0x0000 #define DEF_PROT_MQTT 0x0000 - +#define DEF_PROT_HISTORY 0x0000 typedef struct { uint8_t ip[4]; // ip address @@ -373,7 +374,7 @@ class settings { // 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; + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; mCfg.sys.darkMode = false; mCfg.sys.schedReboot = false; // restore temp settings @@ -546,7 +547,7 @@ class settings { 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; + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; } } diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 0cfbd710..2f0a94bf 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -99,7 +99,8 @@ class Display { uint8_t nrprod = 0; uint8_t nrsleep = 0; - int8_t minQAllInv = 4; + uint8_t nrAvailable = 0; + int8_t minQAllInv = 4; Inverter<> *iv; record_t<> *rec; @@ -114,6 +115,8 @@ class Display { nrprod++; else nrsleep++; + if (iv->isAvailable()) + nrAvailable++; rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -141,6 +144,7 @@ class Display { // prepare display data mDisplayData.nrProducing = nrprod; mDisplayData.nrSleeping = nrsleep; + mDisplayData.nrAvailable = nrAvailable; mDisplayData.totalPower = totalPower; mDisplayData.totalYieldDay = totalYieldDay; mDisplayData.totalYieldTotal = totalYieldTotal; @@ -165,7 +169,17 @@ class Display { else mDisplayData.utcTs = 0; - if (mMono ) { + const uint32_t sunriseTime = mApp->getSunrise(); + if (mDisplayData.utcTs == 0) + mDisplayData.sunIsShining = true; // Start with sunshine :-) + else { + mDisplayData.sunIsShining = false; + // new sunrise is calculated after sunset + user-offset + if (utc > sunriseTime) + mDisplayData.sunIsShining = true; + } + + if (mMono) { mMono->disp(); } #if defined(ESP32) && !defined(ETHERNET) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 3e998b6d..f8c81914 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- #pragma once +#include "config/config.h" #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds #define DISP_FMT_TEXT_LEN 32 @@ -101,6 +102,13 @@ class DisplayMono { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } + +#ifdef DISPLAY_CHART +#define DISP_WATT_ARR_LENGTH 128 // Number of WATT history values + float m_wattArr[DISP_WATT_ARR_LENGTH + 1]; // ring buffer for watt history + uint16_t m_wattListIdx; // index for next Element to write into WattArr + void drawPowerChart(); +#endif }; /* adapted 5x8 Font for low-res displays with symbols diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index afb581dd..88c72979 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -9,6 +9,12 @@ class DisplayMono128X64 : public DisplayMono { public: DisplayMono128X64() : DisplayMono() { +#ifdef DISPLAY_CHART + for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) + m_wattArr[i] = 0.0; + m_wattListIdx = 0; + mDrawChart = false; +#endif mExtra = 0; } @@ -41,7 +47,6 @@ class DisplayMono128X64 : public DisplayMono { } void disp(void) { - uint8_t pos, sun_pos, moon_pos; mDisplay->clearBuffer(); @@ -61,109 +66,149 @@ class DisplayMono128X64 : public DisplayMono { // calculate current pixelshift for pixelshift screensaver calcPixelShift(pixelShiftRange); - // print total power - if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - - printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); +#ifdef DISPLAY_CHART + static uint32_t dataUpdateTime = mDisplayData->utcTs + 60; // update chart every minute + if (mDisplayData->utcTs >= dataUpdateTime) + { + dataUpdateTime = mDisplayData->utcTs + 60; // next minute + m_wattArr[m_wattListIdx] = mDisplayData->totalPower; + m_wattListIdx = (m_wattListIdx + 1) % (DISP_WATT_ARR_LENGTH); } - // print Date and time - if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); + if (mDrawChart && mDisplayData->sunIsShining && (mDisplayData->nrAvailable > 0)) + { + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); + printText(mFmtText, l_Time, 10); + } else { + printText("offline", l_Time, 0xff); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_Status, 10); + + drawPowerChart(); - // dynamic status bar, alternatively: - // print ip address - if (!(mExtra % 5) && (mDisplayData->ipAddress)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); - printText(mFmtText, l_Status, 0xff); } - // print status of inverters - else { - sun_pos = -1; - moon_pos = -1; - setLineFont(l_Status); - if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); - else if (0 == mDisplayData->nrSleeping) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - sun_pos = 0; + else +#endif + { + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); + + printText(mFmtText, l_TotalPower, 0xff); + } else { + printText("offline", l_TotalPower, 0xff); + } + + // print Date and time + if (0 != mDisplayData->utcTs) + printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); + + // dynamic status bar, alternatively: + // print ip address + if (!(mExtra % 5) && (mDisplayData->ipAddress)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); + printText(mFmtText, l_Status, 0xff); + } + // print status of inverters + else { + uint8_t pos, sun_pos, moon_pos; + sun_pos = -1; + moon_pos = -1; + setLineFont(l_Status); + if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + else if (0 == mDisplayData->nrSleeping) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + sun_pos = 0; } else if (0 == mDisplayData->nrProducing) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - moon_pos = 0; + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + moon_pos = 0; } else { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); - sun_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); - moon_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " "); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); + sun_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText + 2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); + moon_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText + 7, DISP_FMT_TEXT_LEN, " "); + } + printText(mFmtText, l_Status, 0xff); + + pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; + mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); + if (sun_pos != -1) + mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol + if (moon_pos != -1) + mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol } - printText(mFmtText, l_Status, 0xff); - - pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; - mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); - if (sun_pos!=-1) - mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol - if (moon_pos!=-1) - mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol - } - // print yields - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol + // print yields + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol - if (mDisplayData->totalYieldDay > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_YieldDay, 0xff); + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_YieldDay, 0xff); - if (mDisplayData->totalYieldTotal > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); - printText(mFmtText, l_YieldTotal, 0xff); + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); + printText(mFmtText, l_YieldTotal, 0xff); - // draw dynamic RSSI bars - int xoffs; - if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver - xoffs = pixelShiftRange/2; - else - xoffs = 0; - int rssi_bar_height = 9; - for (int i = 0; i < 4; i++) { - int radio_rssi_threshold = -60 - i * 10; - int wifi_rssi_threshold = -60 - i * 10; - if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - } - // draw dynamic antenna and WiFi symbols - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - char sym[]=" "; - sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF - mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); - - if (mDisplayData->MQTTSymbol) - sym[0] = 'J'; // MQTT - else - sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi - mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); - mDisplay->sendBuffer(); + // draw dynamic RSSI bars + int xoffs; + if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver + xoffs = pixelShiftRange / 2; + else + xoffs = 0; + int rssi_bar_height = 9; + for (int i = 0; i < 4; i++) { + int radio_rssi_threshold = -60 - i * 10; + int wifi_rssi_threshold = -60 - i * 10; + if (mDisplayData->RadioRSSI > radio_rssi_threshold) + mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + if (mDisplayData->WifiRSSI > wifi_rssi_threshold) + mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + } + // draw dynamic antenna and WiFi symbols + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + char sym[] = " "; + sym[0] = mDisplayData->RadioSymbol ? 'A' : 'E'; // NRF + mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + if (mDisplayData->MQTTSymbol) + sym[0] = 'J'; // MQTT + else + sym[0] = mDisplayData->WifiSymbol ? 'B' : 'F'; // Wifi + mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->sendBuffer(); + } mDisplay->sendBuffer(); - + mExtra++; + +#ifdef DISPLAY_CHART + static uint32_t switchDisplayTime = mDisplayData->utcTs + 20; + if (mDisplayData->utcTs >= switchDisplayTime) { + switchDisplayTime = mDisplayData->utcTs + 20; + mDrawChart = !mDrawChart; + } +#endif } private: @@ -226,4 +271,46 @@ class DisplayMono128X64 : public DisplayMono { dispX += mPixelshift; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } + +#ifdef DISPLAY_CHART + bool mDrawChart ; + + void drawPowerChart() { + const int hight = 40; // chart hight + + // Clear area + // mDisplay->draw_rectangle(0, 63 - hight, DISP_WATT_ARR_LENGTH, 63, OLED::SOLID, OLED::BLACK); + mDisplay->setDrawColor(0); + mDisplay->drawBox(0, 63 - hight, DISP_WATT_ARR_LENGTH, hight); + mDisplay->setDrawColor(1); + + // Get max value for scaling + float maxValue = 0.0; + for (int i = 0; i < DISP_WATT_ARR_LENGTH; i++) { + float fValue = m_wattArr[i]; + if (fValue > maxValue) + maxValue = fValue; + } + // calc divider to fit into chart hight + int divider = round(maxValue / (float)hight); + if (divider < 1) + divider = 1; + + // draw chart bars + // Start display of data right behind last written data + uint16_t idx = m_wattListIdx; + for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) { + float fValue = m_wattArr[idx]; + int iValue = roundf(fValue); + iValue /= divider; + if (iValue > hight) + iValue = hight; + // mDisplay->draw_line(i, 63 - iValue, i, 63); + // mDisplay->drawVLine(i, 63 - iValue, iValue); + if (iValue>0) + mDisplay->drawLine(i, 63 - iValue, i, 63); + idx = (idx + 1) % (DISP_WATT_ARR_LENGTH); + } + } +#endif }; diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h index a400377d..1ccc5f7d 100644 --- a/src/plugins/Display/Display_data.h +++ b/src/plugins/Display/Display_data.h @@ -11,12 +11,14 @@ struct DisplayData { uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized uint8_t nrProducing=0; // indicate number of producing inverters uint8_t nrSleeping=0; // indicate number of sleeping inverters + uint8_t nrAvailable=0; // number of available (comunicating) inverters bool WifiSymbol = false; // indicate if WiFi is connected bool RadioSymbol = false; // indicate if radio module is connecting and working bool MQTTSymbol = false; // indicate if MQTT is connected int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio IPAddress ipAddress; // indicate ip adress of ahoy + bool sunIsShining; // indicate if time is between sunrise and sunset }; #endif /*__DISPLAY_DATA__*/ diff --git a/src/plugins/history.cpp b/src/plugins/history.cpp new file mode 100644 index 00000000..451d3e1e --- /dev/null +++ b/src/plugins/history.cpp @@ -0,0 +1,94 @@ + +#include "plugins/history.h" + +#include "appInterface.h" +#include "config/config.h" +#include "utils/dbg.h" + +void TotalPowerHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + mRefreshCycle = mConfig->inst.sendInterval; + mMaximumDay = 0; + + // Debug + //for (uint16_t i = 0; i < HISTORY_DATA_ARR_LENGTH *1.5; i++) { + // addValue(i); + //} +} + +void TotalPowerHistory::tickerSecond() { + ++mLoopCnt; + if ((mLoopCnt % mRefreshCycle) == 0) { + //DPRINTLN(DBG_DEBUG,F("TotalPowerHistory::tickerSecond > refreshCycle" + String(mRefreshCycle) + "|" + String(mLoopCnt) + "|" + String(mRefreshCycle % mLoopCnt)); + mLoopCnt = 0; + float totalPower = 0; + float totalPowerDay = 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; + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalPowerDay += iv->getChannelFieldValue(CH0, FLD_MP, rec); + } + if (totalPower > 0) { + uint16_t iTotalPower = roundf(totalPower); + DPRINTLN(DBG_DEBUG, F("[TotalPowerHistory]: addValue(iTotalPower)=") + String(iTotalPower)); + addValue(iTotalPower); + } + if (totalPowerDay > 0) { + mMaximumDay = roundf(totalPowerDay); + } + } +} + +void YieldDayHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + mRefreshCycle = 60; // every minute + mDayStored = false; +}; + +void YieldDayHistory::tickerSecond() { + ++mLoopCnt; + if ((mLoopCnt % mRefreshCycle) == 0) { + mLoopCnt = 0; + // check for sunset. if so store yield of day once + uint32_t sunsetTime = mApp->getSunset(); + uint32_t sunriseTime = mApp->getSunrise(); + uint32_t currentTime = mApp->getTimestamp(); + DPRINTLN(DBG_DEBUG,F("[YieldDayHistory] current | rise | set -> ") + String(currentTime) + " | " + String(sunriseTime) + " | " + String(sunsetTime)); + + if (currentTime > sunsetTime) { + if (!mDayStored) { + DPRINTLN(DBG_DEBUG,F("currentTime > sunsetTime ") + String(currentTime) + " > " + String(sunsetTime)); + float totalYieldDay = -0.1; + 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; + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + } + if (totalYieldDay > 0) { + uint16_t iTotalYieldDay = roundf(totalYieldDay); + DPRINTLN(DBG_DEBUG,F("addValue(iTotalYieldDay)=") + String(iTotalYieldDay)); + addValue(iTotalYieldDay); + mDayStored = true; + } + } + } else { + if (currentTime > sunriseTime) { + DPRINTLN(DBG_DEBUG,F("currentTime > sunriseTime ") + String(currentTime) + " > " + String(sunriseTime)); + mDayStored = false; + } + } + } +} \ No newline at end of file diff --git a/src/plugins/history.h b/src/plugins/history.h new file mode 100644 index 00000000..f6485b17 --- /dev/null +++ b/src/plugins/history.h @@ -0,0 +1,86 @@ +#ifndef __HISTORY_DATA_H__ +#define __HISTORY_DATA_H__ + +#include "utils/helper.h" +#include "defines.h" +#include "hm/hmSystem.h" + +typedef HmSystem HmSystemType; +class IApp; + +#define HISTORY_DATA_ARR_LENGTH 256 + +class HistoryData { + public: + HistoryData() { + for (int i = 0; i < HISTORY_DATA_ARR_LENGTH; i++) + m_dataArr[i] = 0; + m_listIdx = 0; + m_dispIdx = 0; + m_wrapped = false; + }; + void addValue(uint16_t value) + { + if (m_wrapped) // after 1st time array wrap we have to increas the display index + m_dispIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + m_dataArr[m_listIdx] = value; + m_listIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + if (m_listIdx == 0) + m_wrapped = true; + }; + + uint16_t valueAt(int i){ + uint16_t idx = m_dispIdx + i; + idx = idx % HISTORY_DATA_ARR_LENGTH; + uint16_t value = m_dataArr[idx]; + return value; + }; + + private: + uint16_t m_dataArr[HISTORY_DATA_ARR_LENGTH + 1]; // ring buffer for watt history + uint16_t m_listIdx; // index for next Element to write into WattArr + uint16_t m_dispIdx; // index for 1st Element to display from WattArr + bool m_wrapped; +}; + +class TotalPowerHistory : public HistoryData { + public: + TotalPowerHistory() : HistoryData() { + mLoopCnt = 0; + }; + + void setup(IApp *app, HmSystemType *sys, settings_t *config); + void tickerSecond(); + uint16_t getMaximumDay() { return mMaximumDay; } + + private: + IApp *mApp; + HmSystemType *mSys; + settings *mSettings; + settings_t *mConfig; + uint16_t mRefreshCycle; + uint16_t mLoopCnt; + + uint16_t mMaximumDay; +}; + +class YieldDayHistory : public HistoryData { + public: + YieldDayHistory() : HistoryData(){ + mLoopCnt = 0; + }; + + void setup(IApp *app, HmSystemType *sys, settings_t *config); + void tickerSecond(); + + private: + IApp *mApp; + HmSystemType *mSys; + settings *mSettings; + settings_t *mConfig; + uint16_t mRefreshCycle; + uint16_t mLoopCnt; + bool mDayStored; +}; + +#endif \ No newline at end of file diff --git a/src/web/RestApi.h b/src/web/RestApi.h index a74f4f14..45c2670d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -22,6 +22,8 @@ #include "ESPAsyncWebServer.h" #endif +#include "plugins/history.h" + #if defined(F) && defined(ESP32) #undef F #define F(sl) (sl) @@ -50,8 +52,10 @@ class RestApi { mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; - 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("/api/insertYieldDayHistory", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), + std::bind(&RestApi::onApiPostYieldDHistory, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + 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("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); @@ -102,6 +106,8 @@ class RestApi { else if(path == "setup/networks") getNetworks(root); #endif /* !defined(ETHERNET) */ else if(path == "live") getLive(request,root); + else if (path == "powerHistory") getPowerHistory(request, root); + else if (path == "yieldDayHistory") getYieldDayHistory(request, root); else { if(path.substring(0, 12) == "inverter/id/") getInverter(root, request->url().substring(17).toInt()); @@ -136,6 +142,83 @@ class RestApi { #endif } + void onApiPostYieldDHistory(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) + { + uint32_t total = request->contentLength(); + DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final); + + if (0 == index) { + if (NULL != mTmpBuf) + delete[] mTmpBuf; + mTmpBuf = new uint8_t[total + 1]; + mTmpSize = total; + } + if (mTmpSize >= (len + index)) + memcpy(&mTmpBuf[index], data, len); + + if (!final) + return; // not last frame - nothing to do + + mTmpSize = len + index ; // correct the total size + mTmpBuf[mTmpSize] = 0; + +#ifndef ESP32 + DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap +#else + DynamicJsonDocument json(12000)); // does this work? I have no ESP32 :-( +#endif + DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize); + json.shrinkToFit(); + JsonObject obj = json.as(); + + + // Debugging + // mTmpBuf[mTmpSize] = 0; + // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf); + + if (!err && obj) + { + // insert data into yieldDayHistory object + HistoryData *p; + if (obj["maximumDay"]>0) // this is power history data + p = mApp->getTotalPowerHistoryPtr(); + else + p = mApp->getYieldDayHistoryPtr(); + + size_t cnt = obj[F("value")].size(); + DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt)); + + for (uint16_t i = 0; i < cnt; i++) { + uint16_t val = obj[F("value")][i]; + p->addValue(val); + // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); + } + } + else + { + switch (err.code()) { + case DeserializationError::Ok: + break; + case DeserializationError::IncompleteInput: + DPRINTLN(DBG_DEBUG, F("Incomplete input")); + break; + case DeserializationError::InvalidInput: + DPRINTLN(DBG_DEBUG, F("Invalid input")); + break; + case DeserializationError::NoMemory: + DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes"); + break; + default: + DPRINTLN(DBG_DEBUG, F("Deserialization failed")); + break; + } + } + + request->send(204); // Success with no page load + delete[] mTmpBuf; + mTmpBuf = NULL; + } + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); @@ -196,6 +279,8 @@ class RestApi { ep[F("setup")] = url + F("setup"); ep[F("system")] = url + F("system"); ep[F("live")] = url + F("live"); + ep[F("powerHistory")] = url + F("powerHistory"); + ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); } @@ -769,6 +854,38 @@ class RestApi { } } + void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; + uint16_t maximum = 0; + TotalPowerHistory *p = mApp->getTotalPowerHistoryPtr(); + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = p->valueAt(fld); + obj[F("value")][fld] = value; + if (value > maximum) + maximum = value; + } + obj[F("maximum")] = maximum; + obj[F("maximumDay")] = p->getMaximumDay(); + } + + void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 86400; // 1 day + obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; + uint16_t maximum = 0; + YieldDayHistory *p = mApp->getYieldDayHistoryPtr(); + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = p->valueAt(fld); + obj[F("value")][fld] = value; + if (value > maximum) + maximum = value; + } + obj[F("maximum")] = maximum; + } + + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; diff --git a/src/web/html/api.js b/src/web/html/api.js index b059cc69..3b8b266d 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -84,7 +84,7 @@ function topnav() { } function parseNav(obj) { - for(i = 0; i < 11; i++) { + for(i = 0; i < 12; i++) { if(i == 2) continue; var l = document.getElementById("nav"+i); diff --git a/src/web/html/history.html b/src/web/html/history.html new file mode 100644 index 00000000..5b6c1fb5 --- /dev/null +++ b/src/web/html/history.html @@ -0,0 +1,135 @@ + + + + + History + {#HTML_HEADER} + + + + + + + {#HTML_NAV} +
+
+

Total Power history

+
+
+

+ Maximum day: W. Last value: W.
+ Maximum graphics: W. Updated every seconds

+
+

Yield per day history

+
+
+

+ Maximum value: Wh
+ Updated every seconds

+
+ +

Insert data into Yield per day history

+
+ Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call +
+ + +
+
+

+
+
+ {#HTML_FOOTER} + + + + + + \ No newline at end of file diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 91de5047..2f13c2e8 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -7,6 +7,7 @@
Live + History Webserial Settings diff --git a/src/web/html/style.css b/src/web/html/style.css index b31ae7c9..14a1b002 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -33,6 +33,26 @@ textarea { color: var(--fg2); } +svg rect {fill: #0000AA;} +svg.chart { + background: #f2f2f2; + border: 2px solid gray; + padding: 1px; +} + +div.chartDivContainer { + padding: 1px; + margin: 1px; +} +div.chartdivContainer span { + color: var(--fg2); +} +div.chartDiv { + padding: 0px; + margin: 0px; +} + + .topnav { background-color: var(--nav-bg); position: fixed; diff --git a/src/web/web.h b/src/web/web.h index 1e87547b..38c927f7 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -36,6 +36,7 @@ #include "html/h/update_html.h" #include "html/h/visualization_html.h" #include "html/h/about_html.h" +#include "html/h/history_html.h" #define WEB_SERIAL_BUF_SIZE 2048 @@ -80,6 +81,7 @@ class Web { 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("/history", HTTP_ANY, std::bind(&Web::onHistory, this, std::placeholders::_1)); #ifdef ENABLE_PROMETHEUS_EP mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); @@ -249,6 +251,8 @@ class Web { 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_HISTORY) != PROT_MASK_HISTORY) + request->redirect(F("/history")); 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) @@ -264,7 +268,7 @@ class Web { } } - void getPage(AsyncWebServerRequest *request, uint8_t mask, const uint8_t *zippedHtml, uint32_t len) { + void getPage(AsyncWebServerRequest *request, uint16_t mask, const uint8_t *zippedHtml, uint32_t len) { if (CHECK_MASK(mConfig->sys.protectionMask, mask)) checkProtection(request); @@ -594,6 +598,10 @@ class Web { getPage(request, PROT_MASK_LIVE, visualization_html, visualization_html_len); } + void onHistory(AsyncWebServerRequest *request) { + getPage(request, PROT_MASK_HISTORY, history_html, history_html_len); + } + void onAbout(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len); response->addHeader(F("Content-Encoding"), "gzip"); From 3aa4751689a68a99044fac9686c66a89172ed7f3 Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 8 Jan 2024 21:54:27 +0100 Subject: [PATCH 053/115] 0.8.49 * fix send total values if inverter state is different from `OFF` #1331 * fix german language issues #1335 --- src/CHANGES.md | 4 ++++ src/defines.h | 2 +- src/publisher/pubMqttIvData.h | 2 +- src/web/html/system.html | 6 +++--- src/web/lang.json | 6 +++--- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 5073a2c9..a11a4e2b 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.49 - 2024-01-08 +* fix send total values if inverter state is different from `OFF` #1331 +* fix german language issues #1335 + ## 0.8.48 - 2024-01-07 * merge PR: pin selection for ESP-32 S2 #1334 * merge PR: enhancement: power graph display option #1330 diff --git a/src/defines.h b/src/defines.h index bd5cc7ff..b205297f 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 48 +#define VERSION_PATCH 49 //------------------------------------- typedef struct { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index a7deadfb..0a59fdd6 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -141,7 +141,7 @@ class PubMqttIvData { // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { - if(mIv->getStatus() > InverterStatus::STARTING) { + if(mIv->getStatus() != InverterStatus::OFF) { if(mIv->config->add2Total) { mTotalFound = true; switch (rec->assign[mPos].fieldId) { diff --git a/src/web/html/system.html b/src/web/html/system.html index 16c6343e..ab0b5289 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -47,8 +47,8 @@ function irqBadge(state) { switch(state) { case 0: return badge(false, "{#UNKNOWN}", "warning"); break; - case 1: return badge(true, "true"); break; - default: return badge(false, "false"); break; + case 1: return badge(true, "{#TRUE}"); break; + default: return badge(false, "{#FALSE}"); break; } } @@ -119,7 +119,7 @@ tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), - tr("{#COMMUNICATION_STTOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), + tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning")) ]) ) diff --git a/src/web/lang.json b/src/web/lang.json index d6c76ffc..c7243728 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -876,7 +876,7 @@ { "token": "NIGHT_TIME", "en": "Night time, inverter polling disabled", - "de": "Nacht, Wechselrichterabfrage deaktivert" + "de": "Wechselrichterabfrage deaktivert (Nacht)" }, { "token": "PAUSED_AT", @@ -886,7 +886,7 @@ { "token": "START_AT", "en": "will start polling at", - "de": "wird starten um" + "de": "Abfrage startet am" }, { "token": "DISABLED", @@ -901,7 +901,7 @@ { "token": "AVAIL", "en": "available and is", - "de": "verfügbar und ist" + "de": "verfügbar und" }, { "token": "AVAIL_NO_DATA", From e88809c7955d286b564c4063aa5da89224e7b0c3 Mon Sep 17 00:00:00 2001 From: you69man Date: Mon, 8 Jan 2024 23:46:19 +0100 Subject: [PATCH 054/115] disable graph per default --- src/config/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/settings.h b/src/config/settings.h index 28739db3..2da326cc 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -463,7 +463,7 @@ class settings { mCfg.plugin.display.pwrSaveAtIvOffline = false; mCfg.plugin.display.contrast = 60; mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility - mCfg.plugin.display.graph_ratio = 50; + mCfg.plugin.display.graph_ratio = 0; mCfg.plugin.display.graph_size = 2; mCfg.plugin.display.rot = 0; mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA From 08dc6d21947d86fd762c9b26f8eb8a67bc66dbed Mon Sep 17 00:00:00 2001 From: you69man Date: Mon, 8 Jan 2024 23:51:56 +0100 Subject: [PATCH 055/115] pass full display config to Display_Mono classes for better maintainability --- src/plugins/Display/Display.h | 7 ++-- src/plugins/Display/Display_Mono.h | 46 +++++++++-------------- src/plugins/Display/Display_Mono_128X32.h | 14 +++---- src/plugins/Display/Display_Mono_128X64.h | 24 +++++------- src/plugins/Display/Display_Mono_64X48.h | 14 +++---- src/plugins/Display/Display_Mono_84X48.h | 17 ++++----- 6 files changed, 48 insertions(+), 74 deletions(-) diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 6c8bb9cb..0ce49c33 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -50,12 +50,11 @@ class Display { mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mDisplayData.version); break; #endif - default: mMono = NULL; break; } if(mMono) { - mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast, mCfg->graph_ratio, mCfg->graph_size); - mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData); + mMono->config(mCfg); + mMono->init(&mDisplayData); } // setup PIR pin for motion sensor @@ -78,7 +77,7 @@ class Display { bool request_refresh = false; if (mMono != NULL) - request_refresh = mMono->loop(mCfg->contrast, motionSensorActive()); + request_refresh = mMono->loop(motionSensorActive()); if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) { DataScreen(); diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index c7235815..785f1334 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -25,23 +25,22 @@ class DisplayMono { public: DisplayMono() {}; - virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; - virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 0; + virtual void init(DisplayData *displayData) = 0; + virtual void config(display_t *cfg) = 0; virtual void disp(void) = 0; // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor // can be overridden by subclasses - virtual bool loop(uint8_t lum, bool motion) { + virtual bool loop(bool motion) { - bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && - ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor + bool dispConditions = (!mCfg->pwrSaveAtIvOffline || (mDisplayData->nrProducing > 0)) && + ((mCfg->screenSaver != 2) || motion); // screensaver 2 .. motionsensor if (mDisplayActive) { if (!dispConditions) { if (mDisplayTime.isTimeout()) { // switch display off after timeout mDisplayActive = false; mDisplay->setPowerSave(true); - DBGPRINTLN("**** Display off ****"); } } else @@ -52,16 +51,15 @@ class DisplayMono { mDisplayActive = true; mDisplayTime.reStartTimeMonitor(); // switch display on mDisplay->setPowerSave(false); - DBGPRINTLN("**** Display on ****"); } } - if(mLuminance != lum) { - mLuminance = lum; + if(mLuminance != mCfg->contrast) { + mLuminance = mCfg->contrast; mDisplay->setContrast(mLuminance); } - return(monoMaintainDispSwitchState()); + return(monoMaintainDispSwitchState()); // return flag, if display content should be updated immediately } protected: @@ -71,7 +69,8 @@ class DisplayMono { }; protected: - U8G2* mDisplay; + display_t *mCfg; + U8G2 *mDisplay; DisplayData *mDisplayData; float *mPgData = nullptr; @@ -82,17 +81,10 @@ class DisplayMono { uint32_t mPgTimeOfDay = 0; uint8_t mPgLastPos = 0; - uint8_t mType; uint16_t mDispWidth; uint16_t mDispHeight; - - bool mEnPowerSave; - uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor uint8_t mLuminance; - uint8_t mGraphRatio; - uint8_t mGraphSize; - uint8_t mLoopCnt; uint8_t mLineXOffsets[5] = {}; uint8_t mLineYOffsets[5] = {}; @@ -105,9 +97,8 @@ class DisplayMono { char mFmtText[DISP_FMT_TEXT_LEN]; // Common initialization function to be called by subclasses - void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { + void monoInit(U8G2* display, DisplayData *displayData) { mDisplay = display; - mType = type; mDisplayData = displayData; mDisplay->begin(); mDisplay->setPowerSave(false); // always start with display on @@ -116,10 +107,10 @@ class DisplayMono { mDispWidth = mDisplay->getDisplayWidth(); mDispHeight = mDisplay->getDisplayHeight(); mDispSwitchTime.stopTimeMonitor(); - if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode + if (mCfg->graph_ratio == 100) // if graph ratio is 100% start in graph mode mDispSwitchState = DispSwitchState::GRAPH; - else if (mGraphRatio != 0) - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 + else if (mCfg->graph_ratio != 0) + mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100 } bool monoMaintainDispSwitchState(void) { @@ -128,14 +119,14 @@ class DisplayMono { case DispSwitchState::TEXT: if (mDispSwitchTime.isTimeout()) { mDispSwitchState = DispSwitchState::GRAPH; - mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms + mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms change = true; } break; case DispSwitchState::GRAPH: if (mDispSwitchTime.isTimeout()) { mDispSwitchState = DispSwitchState::TEXT; - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); + mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); change = true; } break; @@ -147,7 +138,6 @@ class DisplayMono { mPgWidth = width; mPgHeight = height; mPgData = new float[mPgWidth]; - //memset(mPgData, 0, mPgWidth); resetPowerGraph(); /* Inverter<> *iv; @@ -195,7 +185,7 @@ class DisplayMono { } } - uint8_t getPowerGraphXpos(uint8_t p) { // + uint8_t getPowerGraphXpos(uint8_t p) { if ((p <= mPgLastPos) && (mPgLastPos > 0)) return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis else @@ -255,7 +245,7 @@ class DisplayMono { // pixelshift screensaver with wipe effect void calcPixelShift(int range) { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); - mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; + mPixelshift = mCfg->screenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } }; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index 1b3927b3..6ab21a6b 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -12,17 +12,13 @@ class DisplayMono128X32 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { - mEnPowerSave = enPowerSave; - mScreenSaver = screenSaver; - mLuminance = lum; - mGraphRatio = graph_ratio; - mGraphSize = graph_size; + void config(display_t *cfg) { + mCfg = cfg; } - void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data), type, displayData); + void init(DisplayData *displayData) { + u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); + monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); calcLinePositions(); printText("Ahoy!", 0); printText("ahoydtu.de", 2); diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index c8338691..b4c1cfe4 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -12,31 +12,27 @@ class DisplayMono128X64 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { - mEnPowerSave = enPowerSave; - mScreenSaver = screenSaver; - mLuminance = lum; - mGraphRatio = graph_ratio; - mGraphSize = graph_size; + void config(display_t *cfg) { + mCfg = cfg; } - void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - switch (type) { + void init(DisplayData *displayData) { + u8g2_cb_t *rot = (u8g2_cb_t *)(( mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); + switch (mCfg->type) { case 1: - monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData); + monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); break; case 2: - monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData); + monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); break; case 6: default: - monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, reset, clock, data), type, displayData); + monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); break; } calcLinePositions(); - switch(mGraphSize) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; case 0: graph_first_line = 1; graph_last_line = 2; @@ -60,7 +56,7 @@ class DisplayMono128X64 : public DisplayMono { break; } - widthShrink = (mScreenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver + widthShrink = (mCfg->screenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index e55f5ac3..68aa3cc4 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -12,18 +12,14 @@ class DisplayMono64X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { - mEnPowerSave = enPowerSave; - mScreenSaver = screenSaver; - mLuminance = lum; - mGraphRatio = graph_ratio; - mGraphSize = graph_size; + void config(display_t *cfg) { + mCfg = cfg; } - void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + void init(DisplayData *displayData) { + u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); // Wemos OLed Shield is not defined in u8 lib -> use nearest compatible - monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data), type, displayData); + monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); calcLinePositions(); printText("Ahoy!", 0); diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index b195e17c..51c5eafe 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -12,20 +12,17 @@ class DisplayMono84X48 : public DisplayMono { mExtra = 0; } - void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) { - mEnPowerSave = enPowerSave; - mScreenSaver = screenSaver; - mLuminance = lum; - mGraphRatio = graph_ratio; - mGraphSize = graph_size; + void config(display_t *cfg) { + mCfg = cfg; } - void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData); + void init(DisplayData *displayData) { + u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); + + monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, mCfg->disp_clk, mCfg->disp_data, mCfg->disp_cs, mCfg->disp_dc, 0xff), displayData); calcLinePositions(); - switch(mGraphSize) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; case 0: graph_first_line = 1; graph_last_line = 2; From c9c4f20ec3f4292455aee92198a826cf664c1756 Mon Sep 17 00:00:00 2001 From: you69man Date: Mon, 8 Jan 2024 23:53:48 +0100 Subject: [PATCH 056/115] add logic to settings to hide useless options depending on display type, and respect i18n --- src/web/html/setup.html | 56 ++++++++++++++++++++++++----------------- src/web/lang.json | 52 +++++++++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/web/html/setup.html b/src/web/html/setup.html index da9785ee..00b0477c 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -287,18 +287,20 @@
-
+
{#DISP_LUMINANCE}

{#DISP_PINOUT}

-

Graph options

-
-
-
Show ratio (0-100 %)
-
+
+

{#GRAPH_OPTIONS}

+
+
+
{#GRAPH_SHOW_RATIO}
+
+
@@ -1095,12 +1097,12 @@ document.getElementsByName("disp_graph_ratio")[0].value = obj["disp_graph_ratio"]; - var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]]; + var opts2 = [[0, "{#GRAPH_LINES_1_2}"], [1, "{#GRAPH_LINES_2_3}"], [2, "{#GRAPH_LINES_1_3}"], [3, "{#GRAPH_LINES_2_4}"], [4, "{#GRAPH_LINES_1_4}"]]; var graph_size_sel = sel("disp_graph_size", opts2, obj["disp_graph_size"]); graph_size_sel.id = 'disp_graph_size'; document.getElementById("graphSize").append( ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-12 col-sm-3 my-2"}, "Graph size"), + ml("div", {class: "col-12 col-sm-3 my-2"}, "{#GRAPH_POSITION}"), ml("div", {class: "col-12 col-sm-9"}, graph_size_sel) ]) ); @@ -1113,14 +1115,14 @@ // It depends on fix pin array (see var pins) // var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']]; const pinMap = new Map([ - [0, [0,0,0,0,0,0]], //none - [1, [1,1,0,0,0,0]], //SSD1306_128X64 - [2, [1,1,0,0,0,0]], //SH1106_128X64 - [3, [1,1,1,1,0,0]], //PCD8544_84X48 /nokia5110 - [4, [1,1,0,0,0,0]], //SSD1306_128X32 - [5, [1,1,0,0,0,0]], //SSD1306_128x64 - [6, [1,1,0,0,0,0]], //SSD1309_128x64 - [10, [1,1,1,1,1,1]] //ePaper + [0, [0,0,0,0,0,0]], //none + [1, [1,1,0,0,0,0]], //SSD1306_128X64 + [2, [1,1,0,0,0,0]], //SH1106_128X64 + [3, [1,1,1,1,0,0]], //PCD8544_84X48 /nokia5110 + [4, [1,1,0,0,0,0]], //SSD1306_128X32 + [5, [1,1,0,0,0,0]], //SSD1306_64X48 + [6, [1,1,0,0,0,0]], //SSD1309_128x64 + [10, [1,1,1,1,1,1]] //ePaper ]) for(var i = 0; i < pins.length; i++) { var cl = document.getElementById("row_" + pins[i][1]).classList; @@ -1132,14 +1134,22 @@ } } - var screenSaver = document.getElementById("disp_screensaver").value; + const optionsMap = new Map([ // options: [Graph, Luminance, Screensaver] + [0, [0,0,0]], //none + [1, [1,1,1]], //SSD1306_128X64 + [2, [1,1,1]], //SH1106_128X64 + [3, [1,1,0]], //PCD8544_84X48 /nokia5110 + [4, [0,1,1]], //SSD1306_128X32 + [5, [0,1,1]], //SSD1306_64X48 + [6, [1,1,1]], //SSD1309_128x64 + [10, [0,0,0]] //ePaper + ]) - if (2==screenSaver) { // show pir pin only for motion screensaver - setHide("pirPin", false); - } - else { // no pir pin for all others - setHide("pirPin", true); - } + var screenSaver = document.getElementById("disp_screensaver").value; + setHide("graphOptions", !optionsMap.get(dispType)[0]); + setHide("luminanceOption", !optionsMap.get(dispType)[1]); + setHide("screenSaver", !optionsMap.get(dispType)[2]); + setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver } function tick() { diff --git a/src/web/lang.json b/src/web/lang.json index d6c76ffc..4663949d 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -301,7 +301,7 @@ { "token": "NTP_SET_SYS_TIME", "en": "set system time", - "de": "Systemzeit setzten" + "de": "Systemzeit setzen" }, { "token": "BTN_FROM_BROWSER", @@ -311,7 +311,7 @@ { "token": "BTN_SYNC_NTP", "en": "sync NTP", - "de": "NTP syncchronisieren" + "de": "NTP synchronisieren" }, { "token": "NTP_SYS_TIME", @@ -366,7 +366,7 @@ { "token": "DISP_OFF_INV", "en": "Turn off while inverters are offline", - "de": "schalte das Display aus, während die Wechselrichter aus sind" + "de": "Schalte das Display aus, während die Wechselrichter aus sind" }, { "token": "DISP_LUMINANCE", @@ -378,6 +378,46 @@ "en": "Pinout", "de": "Anschlusseinstellungen" }, + { + "token": "GRAPH_OPTIONS", + "en": "Graph options", + "de": "Graph Einstellungen" + }, + { + "token": "GRAPH_SHOW_RATIO", + "en": "Graph show ratio (0-100%)", + "de": "Graph Anzeigeverhältnis (0-100%)" + }, + { + "token": "GRAPH_POSITION", + "en": "Graph pos (from/to)", + "de": "Graph Position (von/bis)" + }, + { + "token": "GRAPH_LINES_1_2", + "en": "Line 1-2", + "de": "Zeile 1-2" + }, + { + "token": "GRAPH_LINES_2_3", + "en": "Line 2-3", + "de": "Zeile 2-3" + }, + { + "token": "GRAPH_LINES_1_3", + "en": "Line 1-3", + "de": "Zeile 1-3" + }, + { + "token": "GRAPH_LINES_2_4", + "en": "Line 2-4", + "de": "Zeile 2-4" + }, + { + "token": "GRAPH_LINES_1_4", + "en": "Line 1-4", + "de": "Zeile 1-4" + }, { "token": "BTN_REBOOT_SUCCESSFUL_SAVE", "en": "Reboot device after successful save", @@ -476,7 +516,7 @@ { "token": "NTP_SYNCED_AT", "en": "synced at", - "de": "syncchronisiert um" + "de": "synchronisiert um" }, { "token": "NTP_DIFF", @@ -491,7 +531,7 @@ { "token": "IMPORT_UPLOAD_STARTED", "en": "upload started", - "de": "hochladen gestartet" + "de": "Hochladen gestartet" }, { "token": "INV_EDIT", @@ -681,7 +721,7 @@ { "token": "DISP_SCREENSAVER", "en": "Screensaver (OLED only)", - "de": "Bildschrimschoner (nur OLED)" + "de": "Bildschirmschoner (nur OLED)" }, { "token": "NETWORK_PLEASE_SELECT", From cc9ba1b808811f3005371c029a86f0f6135c9af9 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 00:11:36 +0100 Subject: [PATCH 057/115] improved, but currently compiles with errors --- src/app.cpp | 10 +- src/app.h | 18 +- src/appInterface.h | 14 +- src/config/config.h | 3 - src/config/settings.h | 1 + src/plugins/Display/Display.h | 18 +- src/plugins/Display/Display_Mono.h | 8 - src/plugins/Display/Display_Mono_128X64.h | 261 ++++++++-------------- src/plugins/Display/Display_data.h | 2 - src/plugins/history.cpp | 94 -------- src/plugins/history.h | 187 +++++++++------- src/web/RestApi.h | 109 ++------- 12 files changed, 234 insertions(+), 491 deletions(-) delete mode 100644 src/plugins/history.cpp diff --git a/src/app.cpp b/src/app.cpp index 9ec3d86c..5a31a1d0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,7 +7,7 @@ #include "app.h" #include "utils/sun.h" -#include "plugins/history.h" + //----------------------------------------------------------------------------- app::app() : ah::Scheduler {} {} @@ -91,10 +91,7 @@ void app::setup() { #endif #endif - mTotalPowerHistory = new TotalPowerHistory(); - mTotalPowerHistory->setup(this, &mSys, mConfig); - mYieldDayHistory = new YieldDayHistory(); - mYieldDayHistory->setup(this, &mSys, mConfig); + mHistory.setup(this, &mSys, mConfig, &mTimestamp); mPubSerial.setup(mConfig, &mSys, &mTimestamp); @@ -154,8 +151,7 @@ void app::regularTickers(void) { //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif - everySec(std::bind(&TotalPowerHistory::tickerSecond, mTotalPowerHistory), "totalPowerHistory"); - everySec(std::bind(&YieldDayHistory::tickerSecond, mYieldDayHistory), "yieldDayHistory"); + everySec(std::bind(&HistoryType::tickerSecond, mHistory), "hist"); } #if defined(ETHERNET) diff --git a/src/app.h b/src/app.h index 029f0f1e..4708ee5a 100644 --- a/src/app.h +++ b/src/app.h @@ -24,6 +24,7 @@ #include "utils/scheduler.h" #include "utils/syslog.h" #include "web/RestApi.h" +#include "plugins/history.h" #include "web/web.h" #include "hm/Communication.h" #if defined(ETHERNET) @@ -35,6 +36,7 @@ #include // position is relevant since version 1.4.7 of this library + // convert degrees and radians for sun calculation #define SIN(x) (sin(radians(x))) #define COS(x) (cos(radians(x))) @@ -42,12 +44,11 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -#ifdef ESP32 -#endif typedef Web WebType; typedef RestApi RestApiType; typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; +typedef HistoryData HistoryType; // PLUGINS #if defined(PLUGIN_DISPLAY) @@ -55,7 +56,6 @@ typedef PubSerial PubSerialType; #include "plugins/Display/Display_data.h" typedef Display DisplayType; #endif -#include "plugins/history.h" class app : public IApp, public ah::Scheduler { public: @@ -244,8 +244,13 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } - TotalPowerHistory *getTotalPowerHistoryPtr() { return mTotalPowerHistory; }; - YieldDayHistory *getYieldDayHistoryPtr() { return mYieldDayHistory; }; + uint16_t getHistoryValue(HistoryType type, uint16_t i) { + return mHistory.valueAt(type, i); + } + + uint16_t getHistoryMaxDay() { + return mHistory.getMaximumDay(); + } private: #define CHECK_AVAIL true @@ -354,8 +359,7 @@ class app : public IApp, public ah::Scheduler { DisplayType mDisplay; DisplayData mDispData; #endif - TotalPowerHistory *mTotalPowerHistory; - YieldDayHistory *mYieldDayHistory; + HistoryType mHistory; }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 89d52d9e..a9c67147 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -8,19 +8,13 @@ #include "defines.h" #include "hm/hmSystem.h" +#include "plugins/history.h" #if defined(ETHERNET) #include "AsyncWebServer_ESP32_W5500.h" #else #include "ESPAsyncWebServer.h" #endif -class TotalPowerHistory; -class YieldDayHistory; -//#include "hms/hmsRadio.h" -#if defined(ESP32) -//typedef CmtRadio> CmtRadioType; -#endif - // abstract interface to App. Make members of App accessible from child class // like web or API without forward declaration class IApp { @@ -65,10 +59,10 @@ class IApp { virtual bool getProtection(AsyncWebServerRequest *request) = 0; - virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0; - virtual YieldDayHistory *getYieldDayHistoryPtr() = 0; - virtual void* getRadioObj(bool nrf) = 0; + virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; + virtual uint16_t getHistoryMaxDay() = 0; + virtual void* getRadioObj(bool nrf) = 0; }; #endif /*__IAPP_H__*/ diff --git a/src/config/config.h b/src/config/config.h index 393c78e3..2cb9bcd3 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -36,9 +36,6 @@ // CONFIGURATION - COMPILE TIME //------------------------------------- -// Draw power chart in MONO-Display -#define DISPLAY_CHART 1 - // ethernet #if defined(ETHERNET) diff --git a/src/config/settings.h b/src/config/settings.h index ec61d733..f4a083f8 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -53,6 +53,7 @@ #define DEF_PROT_MQTT 0x0000 #define DEF_PROT_HISTORY 0x0000 + typedef struct { uint8_t ip[4]; // ip address uint8_t mask[4]; // sub mask diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 2f0a94bf..0cfbd710 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -99,8 +99,7 @@ class Display { uint8_t nrprod = 0; uint8_t nrsleep = 0; - uint8_t nrAvailable = 0; - int8_t minQAllInv = 4; + int8_t minQAllInv = 4; Inverter<> *iv; record_t<> *rec; @@ -115,8 +114,6 @@ class Display { nrprod++; else nrsleep++; - if (iv->isAvailable()) - nrAvailable++; rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -144,7 +141,6 @@ class Display { // prepare display data mDisplayData.nrProducing = nrprod; mDisplayData.nrSleeping = nrsleep; - mDisplayData.nrAvailable = nrAvailable; mDisplayData.totalPower = totalPower; mDisplayData.totalYieldDay = totalYieldDay; mDisplayData.totalYieldTotal = totalYieldTotal; @@ -169,17 +165,7 @@ class Display { else mDisplayData.utcTs = 0; - const uint32_t sunriseTime = mApp->getSunrise(); - if (mDisplayData.utcTs == 0) - mDisplayData.sunIsShining = true; // Start with sunshine :-) - else { - mDisplayData.sunIsShining = false; - // new sunrise is calculated after sunset + user-offset - if (utc > sunriseTime) - mDisplayData.sunIsShining = true; - } - - if (mMono) { + if (mMono ) { mMono->disp(); } #if defined(ESP32) && !defined(ETHERNET) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index f8c81914..3e998b6d 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -4,7 +4,6 @@ //----------------------------------------------------------------------------- #pragma once -#include "config/config.h" #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds #define DISP_FMT_TEXT_LEN 32 @@ -102,13 +101,6 @@ class DisplayMono { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } - -#ifdef DISPLAY_CHART -#define DISP_WATT_ARR_LENGTH 128 // Number of WATT history values - float m_wattArr[DISP_WATT_ARR_LENGTH + 1]; // ring buffer for watt history - uint16_t m_wattListIdx; // index for next Element to write into WattArr - void drawPowerChart(); -#endif }; /* adapted 5x8 Font for low-res displays with symbols diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 88c72979..afb581dd 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -9,12 +9,6 @@ class DisplayMono128X64 : public DisplayMono { public: DisplayMono128X64() : DisplayMono() { -#ifdef DISPLAY_CHART - for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) - m_wattArr[i] = 0.0; - m_wattListIdx = 0; - mDrawChart = false; -#endif mExtra = 0; } @@ -47,6 +41,7 @@ class DisplayMono128X64 : public DisplayMono { } void disp(void) { + uint8_t pos, sun_pos, moon_pos; mDisplay->clearBuffer(); @@ -66,149 +61,109 @@ class DisplayMono128X64 : public DisplayMono { // calculate current pixelshift for pixelshift screensaver calcPixelShift(pixelShiftRange); -#ifdef DISPLAY_CHART - static uint32_t dataUpdateTime = mDisplayData->utcTs + 60; // update chart every minute - if (mDisplayData->utcTs >= dataUpdateTime) - { - dataUpdateTime = mDisplayData->utcTs + 60; // next minute - m_wattArr[m_wattListIdx] = mDisplayData->totalPower; - m_wattListIdx = (m_wattListIdx + 1) % (DISP_WATT_ARR_LENGTH); - } - - if (mDrawChart && mDisplayData->sunIsShining && (mDisplayData->nrAvailable > 0)) - { - // print total power - if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - printText(mFmtText, l_Time, 10); - } else { - printText("offline", l_Time, 0xff); - } - - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_Status, 10); - - drawPowerChart(); + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); + printText(mFmtText, l_TotalPower, 0xff); + } else { + printText("offline", l_TotalPower, 0xff); } - else -#endif - { - // print total power - if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); - } + // print Date and time + if (0 != mDisplayData->utcTs) + printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - // print Date and time - if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - - // dynamic status bar, alternatively: - // print ip address - if (!(mExtra % 5) && (mDisplayData->ipAddress)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); - printText(mFmtText, l_Status, 0xff); - } - // print status of inverters - else { - uint8_t pos, sun_pos, moon_pos; - sun_pos = -1; - moon_pos = -1; - setLineFont(l_Status); - if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); - else if (0 == mDisplayData->nrSleeping) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - sun_pos = 0; + // dynamic status bar, alternatively: + // print ip address + if (!(mExtra % 5) && (mDisplayData->ipAddress)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); + printText(mFmtText, l_Status, 0xff); + } + // print status of inverters + else { + sun_pos = -1; + moon_pos = -1; + setLineFont(l_Status); + if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + else if (0 == mDisplayData->nrSleeping) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + sun_pos = 0; } else if (0 == mDisplayData->nrProducing) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - moon_pos = 0; + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + moon_pos = 0; } else { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); - sun_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText + 2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); - moon_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText + 7, DISP_FMT_TEXT_LEN, " "); - } - printText(mFmtText, l_Status, 0xff); - - pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; - mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); - if (sun_pos != -1) - mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol - if (moon_pos != -1) - mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); + sun_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); + moon_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " "); } + printText(mFmtText, l_Status, 0xff); + + pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; + mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); + if (sun_pos!=-1) + mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol + if (moon_pos!=-1) + mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol + } - // print yields - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol + // print yields + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol - if (mDisplayData->totalYieldDay > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_YieldDay, 0xff); - - if (mDisplayData->totalYieldTotal > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); - printText(mFmtText, l_YieldTotal, 0xff); + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_YieldDay, 0xff); - // draw dynamic RSSI bars - int xoffs; - if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver - xoffs = pixelShiftRange / 2; - else - xoffs = 0; - int rssi_bar_height = 9; - for (int i = 0; i < 4; i++) { - int radio_rssi_threshold = -60 - i * 10; - int wifi_rssi_threshold = -60 - i * 10; - if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - } - // draw dynamic antenna and WiFi symbols - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - char sym[] = " "; - sym[0] = mDisplayData->RadioSymbol ? 'A' : 'E'; // NRF - mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); + printText(mFmtText, l_YieldTotal, 0xff); - if (mDisplayData->MQTTSymbol) - sym[0] = 'J'; // MQTT - else - sym[0] = mDisplayData->WifiSymbol ? 'B' : 'F'; // Wifi - mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); - mDisplay->sendBuffer(); + // draw dynamic RSSI bars + int xoffs; + if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver + xoffs = pixelShiftRange/2; + else + xoffs = 0; + int rssi_bar_height = 9; + for (int i = 0; i < 4; i++) { + int radio_rssi_threshold = -60 - i * 10; + int wifi_rssi_threshold = -60 - i * 10; + if (mDisplayData->RadioRSSI > radio_rssi_threshold) + mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + if (mDisplayData->WifiRSSI > wifi_rssi_threshold) + mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); } + // draw dynamic antenna and WiFi symbols + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + char sym[]=" "; + sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF + mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + + if (mDisplayData->MQTTSymbol) + sym[0] = 'J'; // MQTT + else + sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi + mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->sendBuffer(); + mDisplay->sendBuffer(); - - mExtra++; -#ifdef DISPLAY_CHART - static uint32_t switchDisplayTime = mDisplayData->utcTs + 20; - if (mDisplayData->utcTs >= switchDisplayTime) { - switchDisplayTime = mDisplayData->utcTs + 20; - mDrawChart = !mDrawChart; - } -#endif + mExtra++; } private: @@ -271,46 +226,4 @@ class DisplayMono128X64 : public DisplayMono { dispX += mPixelshift; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } - -#ifdef DISPLAY_CHART - bool mDrawChart ; - - void drawPowerChart() { - const int hight = 40; // chart hight - - // Clear area - // mDisplay->draw_rectangle(0, 63 - hight, DISP_WATT_ARR_LENGTH, 63, OLED::SOLID, OLED::BLACK); - mDisplay->setDrawColor(0); - mDisplay->drawBox(0, 63 - hight, DISP_WATT_ARR_LENGTH, hight); - mDisplay->setDrawColor(1); - - // Get max value for scaling - float maxValue = 0.0; - for (int i = 0; i < DISP_WATT_ARR_LENGTH; i++) { - float fValue = m_wattArr[i]; - if (fValue > maxValue) - maxValue = fValue; - } - // calc divider to fit into chart hight - int divider = round(maxValue / (float)hight); - if (divider < 1) - divider = 1; - - // draw chart bars - // Start display of data right behind last written data - uint16_t idx = m_wattListIdx; - for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) { - float fValue = m_wattArr[idx]; - int iValue = roundf(fValue); - iValue /= divider; - if (iValue > hight) - iValue = hight; - // mDisplay->draw_line(i, 63 - iValue, i, 63); - // mDisplay->drawVLine(i, 63 - iValue, iValue); - if (iValue>0) - mDisplay->drawLine(i, 63 - iValue, i, 63); - idx = (idx + 1) % (DISP_WATT_ARR_LENGTH); - } - } -#endif }; diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h index 1ccc5f7d..a400377d 100644 --- a/src/plugins/Display/Display_data.h +++ b/src/plugins/Display/Display_data.h @@ -11,14 +11,12 @@ struct DisplayData { uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized uint8_t nrProducing=0; // indicate number of producing inverters uint8_t nrSleeping=0; // indicate number of sleeping inverters - uint8_t nrAvailable=0; // number of available (comunicating) inverters bool WifiSymbol = false; // indicate if WiFi is connected bool RadioSymbol = false; // indicate if radio module is connecting and working bool MQTTSymbol = false; // indicate if MQTT is connected int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio IPAddress ipAddress; // indicate ip adress of ahoy - bool sunIsShining; // indicate if time is between sunrise and sunset }; #endif /*__DISPLAY_DATA__*/ diff --git a/src/plugins/history.cpp b/src/plugins/history.cpp deleted file mode 100644 index 451d3e1e..00000000 --- a/src/plugins/history.cpp +++ /dev/null @@ -1,94 +0,0 @@ - -#include "plugins/history.h" - -#include "appInterface.h" -#include "config/config.h" -#include "utils/dbg.h" - -void TotalPowerHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { - mApp = app; - mSys = sys; - mConfig = config; - mRefreshCycle = mConfig->inst.sendInterval; - mMaximumDay = 0; - - // Debug - //for (uint16_t i = 0; i < HISTORY_DATA_ARR_LENGTH *1.5; i++) { - // addValue(i); - //} -} - -void TotalPowerHistory::tickerSecond() { - ++mLoopCnt; - if ((mLoopCnt % mRefreshCycle) == 0) { - //DPRINTLN(DBG_DEBUG,F("TotalPowerHistory::tickerSecond > refreshCycle" + String(mRefreshCycle) + "|" + String(mLoopCnt) + "|" + String(mRefreshCycle % mLoopCnt)); - mLoopCnt = 0; - float totalPower = 0; - float totalPowerDay = 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; - totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - totalPowerDay += iv->getChannelFieldValue(CH0, FLD_MP, rec); - } - if (totalPower > 0) { - uint16_t iTotalPower = roundf(totalPower); - DPRINTLN(DBG_DEBUG, F("[TotalPowerHistory]: addValue(iTotalPower)=") + String(iTotalPower)); - addValue(iTotalPower); - } - if (totalPowerDay > 0) { - mMaximumDay = roundf(totalPowerDay); - } - } -} - -void YieldDayHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { - mApp = app; - mSys = sys; - mConfig = config; - mRefreshCycle = 60; // every minute - mDayStored = false; -}; - -void YieldDayHistory::tickerSecond() { - ++mLoopCnt; - if ((mLoopCnt % mRefreshCycle) == 0) { - mLoopCnt = 0; - // check for sunset. if so store yield of day once - uint32_t sunsetTime = mApp->getSunset(); - uint32_t sunriseTime = mApp->getSunrise(); - uint32_t currentTime = mApp->getTimestamp(); - DPRINTLN(DBG_DEBUG,F("[YieldDayHistory] current | rise | set -> ") + String(currentTime) + " | " + String(sunriseTime) + " | " + String(sunsetTime)); - - if (currentTime > sunsetTime) { - if (!mDayStored) { - DPRINTLN(DBG_DEBUG,F("currentTime > sunsetTime ") + String(currentTime) + " > " + String(sunsetTime)); - float totalYieldDay = -0.1; - 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; - totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); - } - if (totalYieldDay > 0) { - uint16_t iTotalYieldDay = roundf(totalYieldDay); - DPRINTLN(DBG_DEBUG,F("addValue(iTotalYieldDay)=") + String(iTotalYieldDay)); - addValue(iTotalYieldDay); - mDayStored = true; - } - } - } else { - if (currentTime > sunriseTime) { - DPRINTLN(DBG_DEBUG,F("currentTime > sunriseTime ") + String(currentTime) + " > " + String(sunriseTime)); - mDayStored = false; - } - } - } -} \ No newline at end of file diff --git a/src/plugins/history.h b/src/plugins/history.h index f6485b17..f068a127 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -1,86 +1,123 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + #ifndef __HISTORY_DATA_H__ #define __HISTORY_DATA_H__ -#include "utils/helper.h" -#include "defines.h" -#include "hm/hmSystem.h" - -typedef HmSystem HmSystemType; -class IApp; +#include +#include "../appInterface.h" +#include "../hm/hmSystem.h" +#include "../utils/helper.h" #define HISTORY_DATA_ARR_LENGTH 256 -class HistoryData { - public: - HistoryData() { - for (int i = 0; i < HISTORY_DATA_ARR_LENGTH; i++) - m_dataArr[i] = 0; - m_listIdx = 0; - m_dispIdx = 0; - m_wrapped = false; - }; - void addValue(uint16_t value) - { - if (m_wrapped) // after 1st time array wrap we have to increas the display index - m_dispIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); - m_dataArr[m_listIdx] = value; - m_listIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); - if (m_listIdx == 0) - m_wrapped = true; - }; - - uint16_t valueAt(int i){ - uint16_t idx = m_dispIdx + i; - idx = idx % HISTORY_DATA_ARR_LENGTH; - uint16_t value = m_dataArr[idx]; - return value; - }; - - private: - uint16_t m_dataArr[HISTORY_DATA_ARR_LENGTH + 1]; // ring buffer for watt history - uint16_t m_listIdx; // index for next Element to write into WattArr - uint16_t m_dispIdx; // index for 1st Element to display from WattArr - bool m_wrapped; +enum class HistoryStorageType : uint8_t { + POWER, + YIELD }; -class TotalPowerHistory : public HistoryData { - public: - TotalPowerHistory() : HistoryData() { - mLoopCnt = 0; - }; - - void setup(IApp *app, HmSystemType *sys, settings_t *config); - void tickerSecond(); - uint16_t getMaximumDay() { return mMaximumDay; } - - private: - IApp *mApp; - HmSystemType *mSys; - settings *mSettings; - settings_t *mConfig; - uint16_t mRefreshCycle; - uint16_t mLoopCnt; - - uint16_t mMaximumDay; -}; +template +class HistoryData { + private: + struct storage_t { + uint16_t refreshCycle; + uint16_t loopCnt; + uint16_t listIdx; // index for next Element to write into WattArr + uint16_t dispIdx; // index for 1st Element to display from WattArr + bool wrapped; + // ring buffer for watt history + std::array data; + + void reset() { + loopCnt = 0; + listIdx = 0; + dispIdx = 0; + wrapped = false; + for(uint16_t i = 0; i < (HISTORY_DATA_ARR_LENGTH + 1); i++) { + data[i] = 0; + } + } + }; + + public: + void setup(IApp *app, HMSYSTEM *sys, settings_t *config, uint32_t *ts) { + mApp = app; + mSys = sys; + mConfig = config; + mTs = ts; + + mCurPwr.reset(); + mCurPwr.refreshCycle = mConfig->inst.sendInterval; + mYieldDay.reset(); + mYieldDay.refreshCycle = 60; + } + + void tickerSecond() { + Inverter<> *iv; + record_t<> *rec; + float curPwr = 0; + float maxPwr = 0; + float yldDay = -0.1; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); + yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + } + + if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { + mCurPwr.loopCnt = 0; + if (curPwr > 0) + addValue(&mCurPwr, roundf(curPwr)); + if (maxPwr > 0) + mMaximumDay = roundf(maxPwr); + } + + if (*mTs > mApp->getSunset()) { + if ((!mDayStored) && (yldDay > 0)) { + addValue(&mYieldDay, roundf(yldDay)); + mDayStored = true; + } + } else if (*mTs > mApp->getSunrise()) + mDayStored = false; + } + + uint16_t valueAt(HistoryStorageType type, uint16_t i) { + settings_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; + uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; + return s->data[idx]; + } + + uint16_t getMaximumDay() { + return mMaximumDay; + } + + private: + void addValue(storage_t *s, uint16_t value) { + if (s->wrapped) // after 1st time array wrap we have to increase the display index + s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + s->data[s->listIdx] = value; + s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + if (s->listIdx == 0) + s->wrapped = true; + } + + private: + IApp *mApp; + HMSYSTEM *mSys; + settings *mSettings; + settings_t *mConfig; + uint32_t *mTs; -class YieldDayHistory : public HistoryData { - public: - YieldDayHistory() : HistoryData(){ - mLoopCnt = 0; - }; - - void setup(IApp *app, HmSystemType *sys, settings_t *config); - void tickerSecond(); - - private: - IApp *mApp; - HmSystemType *mSys; - settings *mSettings; - settings_t *mConfig; - uint16_t mRefreshCycle; - uint16_t mLoopCnt; - bool mDayStored; + storage_t mCurPwr; + storage_t mYieldDay; + bool mDayStored = false; + uint16_t mMaximumDay = 0; }; -#endif \ No newline at end of file +#endif diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 45c2670d..254b0ac4 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -52,10 +52,8 @@ class RestApi { mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; - mSrv->on("/api/insertYieldDayHistory", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), - std::bind(&RestApi::onApiPostYieldDHistory, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); - 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("/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("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); @@ -142,83 +140,6 @@ class RestApi { #endif } - void onApiPostYieldDHistory(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) - { - uint32_t total = request->contentLength(); - DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final); - - if (0 == index) { - if (NULL != mTmpBuf) - delete[] mTmpBuf; - mTmpBuf = new uint8_t[total + 1]; - mTmpSize = total; - } - if (mTmpSize >= (len + index)) - memcpy(&mTmpBuf[index], data, len); - - if (!final) - return; // not last frame - nothing to do - - mTmpSize = len + index ; // correct the total size - mTmpBuf[mTmpSize] = 0; - -#ifndef ESP32 - DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap -#else - DynamicJsonDocument json(12000)); // does this work? I have no ESP32 :-( -#endif - DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize); - json.shrinkToFit(); - JsonObject obj = json.as(); - - - // Debugging - // mTmpBuf[mTmpSize] = 0; - // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf); - - if (!err && obj) - { - // insert data into yieldDayHistory object - HistoryData *p; - if (obj["maximumDay"]>0) // this is power history data - p = mApp->getTotalPowerHistoryPtr(); - else - p = mApp->getYieldDayHistoryPtr(); - - size_t cnt = obj[F("value")].size(); - DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt)); - - for (uint16_t i = 0; i < cnt; i++) { - uint16_t val = obj[F("value")][i]; - p->addValue(val); - // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); - } - } - else - { - switch (err.code()) { - case DeserializationError::Ok: - break; - case DeserializationError::IncompleteInput: - DPRINTLN(DBG_DEBUG, F("Incomplete input")); - break; - case DeserializationError::InvalidInput: - DPRINTLN(DBG_DEBUG, F("Invalid input")); - break; - case DeserializationError::NoMemory: - DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes"); - break; - default: - DPRINTLN(DBG_DEBUG, F("Deserialization failed")); - break; - } - } - - request->send(204); // Success with no page load - delete[] mTmpBuf; - mTmpBuf = NULL; - } - void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); @@ -858,31 +779,29 @@ class RestApi { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = mConfig->inst.sendInterval; obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; - uint16_t maximum = 0; - TotalPowerHistory *p = mApp->getTotalPowerHistoryPtr(); + uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { - uint16_t value = p->valueAt(fld); + uint16_t value = mApp->getHistoryValue(HistoryStorageType::POWER, fld); obj[F("value")][fld] = value; - if (value > maximum) - maximum = value; + if (value > max) + max = value; } - obj[F("maximum")] = maximum; - obj[F("maximumDay")] = p->getMaximumDay(); + obj[F("max")] = max; + obj[F("maxDay")] = mApp->getHistoryMaxDay(); } void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = 86400; // 1 day obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; - uint16_t maximum = 0; - YieldDayHistory *p = mApp->getYieldDayHistoryPtr(); + uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { - uint16_t value = p->valueAt(fld); + uint16_t value = mApp->getHistoryValue(HistoryStorageType::YIELD, fld); obj[F("value")][fld] = value; - if (value > maximum) - maximum = value; - } - obj[F("maximum")] = maximum; + if (value > max) + max = value; + } + obj[F("max")] = max; } From e23daad910cfa69d90797cff23312ab39a1413ff Mon Sep 17 00:00:00 2001 From: rejoe2 Date: Tue, 9 Jan 2024 10:10:39 +0100 Subject: [PATCH 058/115] MI - add "get loss logic" * as we don't have info from inverter side, this is just dull tx/rx statistics from the recent cycle... --- src/hm/Communication.h | 39 ++++++++++++++++++++++++++++----------- src/hm/hmInverter.h | 25 ++++++++++++++++--------- src/hm/radio.h | 6 ++++-- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/hm/Communication.h b/src/hm/Communication.h index d0878dd5..5c1b1f21 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -141,6 +141,8 @@ class Communication : public CommQueue<> { q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); mWaitTime.startTimeMonitor(1000); } + if(IV_MI == q->iv->ivGen) + q->iv->mIvTxCnt++; } closeRequest(q, false); break; @@ -733,17 +735,16 @@ class Communication : public CommQueue<> { miNextRequest((p->packet[0] - ALL_FRAMES + 1), q); } else { q->iv->miMultiParts = 7; // indicate we are ready - //miComplete(q->iv); } } else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) { //addImportant(q->iv, MI_REQ_CH2); miNextRequest(MI_REQ_CH2, q); mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); - //use also miMultiParts here for better statistics? - //mHeu.setGotFragment(q->iv); - } else { // first data msg for 1ch, 2nd for 2ch + q->iv->mIvRxCnt++; // statistics workaround... + + } else { // first data msg for 1ch, 2nd for 2ch q->iv->miMultiParts += 6; // indicate we are ready - //miComplete(q->iv); + } } @@ -757,13 +758,9 @@ class Communication : public CommQueue<> { DBGHEXLN(cmd); } - if(q->iv->miMultiParts == 7) { - //mHeu.setGotAll(q->iv); + if(q->iv->miMultiParts == 7) q->iv->radioStatistics.rxSuccess++; - } else - //mHeu.setGotFragment(q->iv); - /*iv->radioStatistics.rxFail++; // got no complete payload*/ - //q->iv->radioStatistics.retransmits++; + q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true); mWaitTime.startTimeMonitor(MI_TIMEOUT); @@ -871,6 +868,26 @@ class Communication : public CommQueue<> { DPRINT_IVID(DBG_INFO, iv->id); DBGPRINTLN(F("got all data msgs")); } + + if (iv->mGetLossInterval >= AHOY_GET_LOSS_INTERVAL) { // initially mIvRxCnt = mIvTxCnt = 0 + iv->mGetLossInterval = 1; + iv->radioStatistics.ivSent = iv->mIvRxCnt + iv->mDtuTxCnt; // iv->mIvRxCnt is the nr. of additional answer frames, default we expect one frame per request + iv->radioStatistics.ivLoss = iv->radioStatistics.ivSent - iv->mDtuRxCnt; // this is what we didn't receive + iv->radioStatistics.dtuLoss = iv->mIvTxCnt; // this is somehow the requests w/o answers in that periode + iv->radioStatistics.dtuSent = iv->mDtuTxCnt; + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN("DTU loss: " + + String (iv->radioStatistics.ivLoss) + "/" + + String (iv->radioStatistics.ivSent) + " frames for " + + String (iv->radioStatistics.dtuSent) + " requests"); + } + iv->mIvRxCnt = 0; // start new interval, iVRxCnt is abused to collect additional possible frames + iv->mIvTxCnt = 0; // start new interval, iVTxCnt is abused to collect nr. of unanswered requests + iv->mDtuRxCnt = 0; // start new interval + iv->mDtuTxCnt = 0; // start new interval + } + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 4e714cfd..aeb40c19 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -212,16 +212,22 @@ class Inverter { cb(RealTimeRunData_Debug, false); // get live data } } else { // MI - if(0 == getFwVersion()) + if(0 == getFwVersion()) { + mIvRxCnt +=2; cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number - else { + } else { record_t<> *rec = getRecordStruct(InverterDevInform_Simple); - if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) + if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) { cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1 - else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile + mIvRxCnt +=2; + } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile cb(0x10, false); // legacy GPF command - else + else { cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); + mGetLossInterval++; + if (type != INV_TYPE_4CH) + mIvRxCnt++; // statistics workaround... + } } } } @@ -619,7 +625,7 @@ class Inverter { radioStatistics.dtuSent = txCnt + ((uint16_t)65535 - mIvTxCnt) + 1; else radioStatistics.dtuSent = txCnt - mIvTxCnt; - + radioStatistics.dtuLoss = radioStatistics.dtuSent - mDtuRxCnt; DPRINT_IVID(DBG_INFO, id); @@ -831,15 +837,16 @@ class Inverter { bool mDevControlRequest; // true if change needed uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; - uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug - uint16_t mIvRxCnt = 0; - uint16_t mIvTxCnt = 0; uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer) bool mNextLive = true; // first read live data after booting up then version etc. public: uint16_t mDtuRxCnt = 0; uint16_t mDtuTxCnt = 0; + uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug + uint16_t mIvRxCnt = 0; + uint16_t mIvTxCnt = 0; + }; template diff --git a/src/hm/radio.h b/src/hm/radio.h index 1ef32e05..e5eda128 100644 --- a/src/hm/radio.h +++ b/src/hm/radio.h @@ -42,8 +42,10 @@ class Radio { void prepareDevInformCmd(Inverter<> *iv, 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. if(IV_MI == getIvGen(iv)) { - DPRINT(DBG_DEBUG, F("legacy cmd 0x")); - DPRINTLN(DBG_DEBUG,String(cmd, HEX)); + if(*mSerialDebug) { + DPRINT(DBG_DEBUG, F("legacy cmd 0x")); + DPRINTLN(DBG_DEBUG,String(cmd, HEX)); + } sendCmdPacket(iv, cmd, cmd, false, false); return; } From 3a8f37fd8eeb0c360949a476a7a3f998d7b85dee Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 20:37:10 +0100 Subject: [PATCH 059/115] fix compile errors --- src/app.h | 4 ++-- src/appInterface.h | 1 - src/plugins/history.h | 2 +- src/web/RestApi.h | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app.h b/src/app.h index 4708ee5a..237b4034 100644 --- a/src/app.h +++ b/src/app.h @@ -244,8 +244,8 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } - uint16_t getHistoryValue(HistoryType type, uint16_t i) { - return mHistory.valueAt(type, i); + uint16_t getHistoryValue(uint8_t type, uint16_t i) { + return mHistory.valueAt((HistoryStorageType)type, i); } uint16_t getHistoryMaxDay() { diff --git a/src/appInterface.h b/src/appInterface.h index a9c67147..a2831c9b 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -8,7 +8,6 @@ #include "defines.h" #include "hm/hmSystem.h" -#include "plugins/history.h" #if defined(ETHERNET) #include "AsyncWebServer_ESP32_W5500.h" #else diff --git a/src/plugins/history.h b/src/plugins/history.h index f068a127..9ed7860d 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -88,7 +88,7 @@ class HistoryData { } uint16_t valueAt(HistoryStorageType type, uint16_t i) { - settings_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; + storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; return s->data[idx]; } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 254b0ac4..6161fd90 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -781,7 +781,7 @@ class RestApi { obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { - uint16_t value = mApp->getHistoryValue(HistoryStorageType::POWER, fld); + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); obj[F("value")][fld] = value; if (value > max) max = value; @@ -796,7 +796,7 @@ class RestApi { obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { - uint16_t value = mApp->getHistoryValue(HistoryStorageType::YIELD, fld); + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); obj[F("value")][fld] = value; if (value > max) max = value; From fa2028e4798897e0fc859200a9814730332bc16b Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 22:12:06 +0100 Subject: [PATCH 060/115] 0.8.50 * merge PR: added history charts to web #1336 --- src/CHANGES.md | 3 + src/defines.h | 2 +- src/web/RestApi.h | 2 - src/web/html/history.html | 222 ++++++++++++++------------------ src/web/html/save.html | 2 +- src/web/html/serial.html | 2 +- src/web/html/style.css | 12 +- src/web/html/visualization.html | 2 +- src/web/html/wizard.html | 2 +- src/web/lang.json | 52 +++++++- 10 files changed, 163 insertions(+), 138 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index a11a4e2b..fd02a030 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.50 - 2024-01-09 +* merge PR: added history charts to web #1336 + ## 0.8.49 - 2024-01-08 * fix send total values if inverter state is different from `OFF` #1331 * fix german language issues #1335 diff --git a/src/defines.h b/src/defines.h index b205297f..9d0e97c9 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 49 +#define VERSION_PATCH 50 //------------------------------------- typedef struct { diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 26ca571f..96a4c5e4 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -794,7 +794,6 @@ class RestApi { void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = mConfig->inst.sendInterval; - obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); @@ -809,7 +808,6 @@ class RestApi { void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = 86400; // 1 day - obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); diff --git a/src/web/html/history.html b/src/web/html/history.html index 5b6c1fb5..d0f33755 100644 --- a/src/web/html/history.html +++ b/src/web/html/history.html @@ -1,135 +1,113 @@ + + {#NAV_HISTORY} + {#HTML_HEADER} + + - - History - {#HTML_HEADER} - - + - - - - {#HTML_NAV} -
-
-

Total Power history

-
-
-

- Maximum day: W. Last value: W.
- Maximum graphics: W. Updated every seconds

-
-

Yield per day history

-
-
-

- Maximum value: Wh
- Updated every seconds

+ + {#HTML_NAV} +
+
+

{#TOTAL_POWER}

+
+
+

+ {#MAX_DAY}: W. {#LAST_VALUE}: W.
+ {#MAXIMUM}: W. {#UPDATED} {#SECONDS} +

+
+

{#TOTAL_YIELD_PER_DAY}

+
+
+

+ {#MAXIMUM}: Wh
+ {#UPDATED} {#SECONDS} +

+
+
+ {#HTML_FOOTER} -

Insert data into Yield per day history

-
- Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call -
- - -
-
-

-
-
- {#HTML_FOOTER} + + function parsePowerHistory(obj){ + if (null != obj) { + parseHistory(obj,"pwr", pwrExeOnce) + document.getElementById("pwrLast").innerHTML = mLastValue + document.getElementById("pwrMaxDay").innerHTML = obj.maxDay + } + if (pwrExeOnce) { + pwrExeOnce = false; + window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); - + setTimeout(() => { + getAjax("/api/yieldDayHistory", parseYieldDayHistory); + } , 20); + } + } + function parseYieldDayHistory(obj) { + if (null != obj) { + parseNav(obj.generic); + parseHistory(obj, "yd", ydExeOnce) + } + if (ydExeOnce) { + ydExeOnce = false; + window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", mRefresh * 500); + } + } - \ No newline at end of file + getAjax("/api/powerHistory", parsePowerHistory); + + + diff --git a/src/web/html/save.html b/src/web/html/save.html index e5d5a67d..bfc9a12a 100644 --- a/src/web/html/save.html +++ b/src/web/html/save.html @@ -1,7 +1,7 @@ - Save + {#NAV_SAVE} {#HTML_HEADER} diff --git a/src/web/html/serial.html b/src/web/html/serial.html index ef7aa4c3..25940eb0 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -1,7 +1,7 @@ - Serial Console + {#NAV_WEBSERIAL} {#HTML_HEADER} diff --git a/src/web/html/style.css b/src/web/html/style.css index 3ad91a8b..ef581ffa 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -36,21 +36,17 @@ textarea { svg rect {fill: #0000AA;} svg.chart { background: #f2f2f2; - border: 2px solid gray; - padding: 1px; + border: 2px solid gray; + padding: 1px; } div.chartDivContainer { - padding: 1px; - margin: 1px; + padding: 1px; + margin: 1px; } div.chartdivContainer span { color: var(--fg2); } -div.chartDiv { - padding: 0px; - margin: 0px; -} .topnav { diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 8099539c..15b0c7b5 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -1,7 +1,7 @@ - Live + {#NAV_LIVE} {#HTML_HEADER} diff --git a/src/web/html/wizard.html b/src/web/html/wizard.html index 674823b2..3df44dc4 100644 --- a/src/web/html/wizard.html +++ b/src/web/html/wizard.html @@ -1,7 +1,7 @@ - Setup Wizard + {#NAV_WIZARD} {#HTML_HEADER} diff --git a/src/web/lang.json b/src/web/lang.json index 9f1b9e5e..cb8ab8d6 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -3,13 +3,18 @@ { "name": "general", "list": [ + { + "token": "NAV_WIZARD", + "en": "Setup Wizard", + "de": "Daten" + }, { "token": "NAV_LIVE", "en": "Live", "de": "Daten" }, { - "token": "{#NAV_HISTORY}", + "token": "NAV_HISTORY", "en": "History", "de": "Verlauf" }, @@ -28,6 +33,11 @@ "en": "Documentation", "de": "Dokumentation" }, + { + "token": "NAV_SAVE", + "en": "save", + "de": "speichern" + }, { "token": "NAV_ABOUT", "en": "About", @@ -1334,6 +1344,46 @@ "de": "Fehler beim Speichern" } ] + }, + { + "name": "history.html", + "list": [ + { + "token": "TOTAL_POWER", + "en": "Total Power", + "de": "Gesamtleistung" + }, + { + "token": "TOTAL_YIELD_PER_DAY", + "en": "Total Yield per day", + "de": "Gesamtertrag pro Tag" + }, + { + "token": "MAX_DAY", + "en": "maximum day", + "de": "Tagesmaximum" + }, + { + "token": "LAST_VALUE", + "en": "last value", + "de": "letzter Wert" + }, + { + "token": "MAXIMUM", + "en": "maximum value", + "de": "Maximalwert" + }, + { + "token": "UPDATED", + "en": "Updated every", + "de": "aktualisiert alle" + }, + { + "token": "SECONDS", + "en": "seconds", + "de": "Sekunden" + } + ] } ] } From 2a01238bff8fb569ca3130fe7a3b7b50b8e49155 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 22:23:16 +0100 Subject: [PATCH 061/115] small fix --- src/hm/Communication.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 5c1b1f21..cf0d0054 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -140,8 +140,7 @@ class Communication : public CommQueue<> { if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) { q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); mWaitTime.startTimeMonitor(1000); - } - if(IV_MI == q->iv->ivGen) + } else if(IV_MI == q->iv->ivGen) q->iv->mIvTxCnt++; } closeRequest(q, false); From 281da4f5762793000b6b22289dbf4f2bce104b1d Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 22:31:03 +0100 Subject: [PATCH 062/115] 0.8.50 * merge PR: small display changes #1339 * merge PR: MI - add "get loss logic" #1341 --- src/CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CHANGES.md b/src/CHANGES.md index fd02a030..a15ba6ca 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,10 @@ ## 0.8.50 - 2024-01-09 * merge PR: added history charts to web #1336 +* merge PR: small display changes #1339 +* merge PR: MI - add "get loss logic" #1341 +* translated `/history` +* fix translations in title of documents ## 0.8.49 - 2024-01-08 * fix send total values if inverter state is different from `OFF` #1331 From ce6013d6a0d2cab8aa0a3a69c18a8ec3fb178406 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 22:40:02 +0100 Subject: [PATCH 063/115] 0.8.50 * small changes --- src/plugins/Display/Display_Mono.h | 83 +++++++++++++++--------------- src/web/lang.json | 4 +- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 785f1334..62d5cc3a 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -155,16 +155,17 @@ class DisplayMono { } void resetPowerGraph() { - if (mPgData != nullptr) { - mPgMaxPwr = 0.0; - mPgLastPos = 0; - for (uint8_t i = 0; i < mPgWidth; i++) - mPgData[i] = 0.0; - } + if (mPgData != nullptr) { + mPgMaxPwr = 0.0; + mPgLastPos = 0; + for (uint8_t i = 0; i < mPgWidth; i++) { + mPgData[i] = 0.0; + } + } } uint8_t sss2pgpos(uint seconds_since_start) { - return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); + return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); } void calcPowerGraphValues() { @@ -186,60 +187,60 @@ class DisplayMono { } uint8_t getPowerGraphXpos(uint8_t p) { - if ((p <= mPgLastPos) && (mPgLastPos > 0)) + if ((p <= mPgLastPos) && (mPgLastPos > 0)) return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - else + else return(0); } uint8_t getPowerGraphYpos(uint8_t p) { - if (p < mPgWidth) - //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height + if (p < mPgWidth) return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - else + else return(0); } void plotPowerGraph(uint8_t xoff, uint8_t yoff) { - // draw axes - mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis - mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis - - // draw X scale - tmElements_t tm; - breakTime(mDisplayData->pGraphEndTime, tm); - uint8_t endHourPg = tm.Hour; - breakTime(mDisplayData->utcTs, tm); - uint8_t endHour = std::min(endHourPg, tm.Hour); - breakTime(mDisplayData->pGraphStartTime, tm); - tm.Hour += 1; - tm.Minute = 0; - tm.Second = 0; - for (; tm.Hour <= endHour; tm.Hour++) { + // draw axes + mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis + mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis + + // draw X scale + tmElements_t tm; + breakTime(mDisplayData->pGraphEndTime, tm); + uint8_t endHourPg = tm.Hour; + breakTime(mDisplayData->utcTs, tm); + uint8_t endHour = std::min(endHourPg, tm.Hour); + breakTime(mDisplayData->pGraphStartTime, tm); + tm.Hour += 1; + tm.Minute = 0; + tm.Second = 0; + for (; tm.Hour <= endHour; tm.Hour++) { uint8_t x_pos_screen = getPowerGraphXpos(sss2pgpos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); - } + } - // draw Y scale - uint16_t scale_y = 10; - uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr)); - if (maxpwr_int > 100) + // draw Y scale + uint16_t scale_y = 10; + uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr)); + if (maxpwr_int > 100) scale_y = 100; - for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) { + + for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) { uint8_t ypos = yoff - static_cast(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis mDisplay->drawPixel(xoff + 1, ypos); - } + } - // draw curve - for (uint8_t i = 1; i <= mPgLastPos; i++) { + // draw curve + for (uint8_t i = 1; i <= mPgLastPos; i++) { mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); - } + } - // print max power value - mDisplay->setFont(u8g2_font_4x6_tr); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); - mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); + // print max power value + mDisplay->setFont(u8g2_font_4x6_tr); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); + mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); } // pixelshift screensaver with wipe effect diff --git a/src/web/lang.json b/src/web/lang.json index 8868148d..35e99b43 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -1261,12 +1261,12 @@ { "token": "INV_LOSS_RATE", "en": "Inverter loss rate", - "de": "Wechselrichter Empfangsqualität" + "de": "Wechselrichter Verlustrate" }, { "token": "DTU_LOSS_RATE", "en": "DTU loss rate", - "de": "DTU Empfangsqualität" + "de": "DTU Verlustrate" }, { "token": "RADIO_STAT_MODAL", From d2bf74ed3911bdae0839b4a7282da8d26278ed9b Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 23:09:08 +0100 Subject: [PATCH 064/115] 0.8.50 * added translations for error messages #1343 --- src/CHANGES.md | 1 + src/web/RestApi.h | 37 +++++++++++------------ src/web/lang.h | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 src/web/lang.h diff --git a/src/CHANGES.md b/src/CHANGES.md index a15ba6ca..fd8b2ec7 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -6,6 +6,7 @@ * merge PR: MI - add "get loss logic" #1341 * translated `/history` * fix translations in title of documents +* added translations for error messages #1343 ## 0.8.49 - 2024-01-08 * fix send total values if inverter state is different from `OFF` #1331 diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 96a4c5e4..1ed55a93 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -15,6 +15,7 @@ #include "../appInterface.h" #include "../hm/hmSystem.h" #include "../utils/helper.h" +#include "lang.h" #include "AsyncJson.h" #if defined(ETHERNET) #include "AsyncWebServer_ESP32_W5500.h" @@ -172,15 +173,15 @@ class RestApi { root[F("success")] = setSetup(obj, root); else { root[F("success")] = false; - root[F("error")] = "Path not found: " + path; + root[F("error")] = F(PATH_NOT_FOUND) + path; } } else { switch (err.code()) { case DeserializationError::Ok: break; - case DeserializationError::IncompleteInput: root[F("error")] = F("Incomplete input"); 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; + case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break; + case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break; + case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break; + default: root[F("error")] = F(DESER_FAILED); break; } } @@ -402,7 +403,7 @@ class RestApi { void getIvStatistis(JsonObject obj, uint8_t id) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { - obj[F("error")] = F("inverter not found!"); + obj[F("error")] = F(INV_NOT_FOUND); return; } obj[F("name")] = String(iv->config->name); @@ -421,7 +422,7 @@ class RestApi { void getIvPowerLimitAck(JsonObject obj, uint8_t id) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { - obj[F("error")] = F("inverter not found!"); + obj[F("error")] = F(INV_NOT_FOUND); return; } obj["ack"] = (bool)iv->powerLimitAck; @@ -474,7 +475,7 @@ class RestApi { void getInverter(JsonObject obj, uint8_t id) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { - obj[F("error")] = F("inverter not found!"); + obj[F("error")] = F(INV_NOT_FOUND); return; } @@ -537,7 +538,7 @@ class RestApi { void getIvAlarms(JsonObject obj, uint8_t id) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { - obj[F("error")] = F("inverter not found!"); + obj[F("error")] = F(INV_NOT_FOUND); return; } @@ -560,7 +561,7 @@ class RestApi { void getIvVersion(JsonObject obj, uint8_t id) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { - obj[F("error")] = F("inverter not found!"); + obj[F("error")] = F(INV_NOT_FOUND); return; } @@ -732,14 +733,10 @@ class RestApi { obj[F("disNightComm")] = disNightCom; JsonArray warn = obj.createNestedArray(F("warnings")); - if(!mRadioNrf->isChipConnected() && mConfig->nrf.enabled) - warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable")); - 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")); + warn.add(F(REBOOT_ESP_APPLY_CHANGES)); if(0 == mApp->getTimestamp()) - warn.add(F("time not set. No communication to inverter possible")); + warn.add(F(TIME_NOT_SET)); } void getSetup(AsyncWebServerRequest *request, JsonObject obj) { @@ -823,7 +820,7 @@ class RestApi { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; if(NULL == iv) { - jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); + jsonOut[F("error")] = F(INV_INDEX_INVALID) + jsonIn[F("id")].as(); return false; } jsonOut[F("id")] = jsonIn[F("id")]; @@ -848,12 +845,12 @@ class RestApi { DPRINTLN(DBG_INFO, F("dev cmd")); iv->setDevCommand(jsonIn[F("val")].as()); } else { - jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; + 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"); + jsonOut[F("error")] = F(INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT); return false; } @@ -902,7 +899,7 @@ class RestApi { iv->config->add2Total = jsonIn[F("add2total")]; mApp->saveSettings(false); // without reboot } else { - jsonOut[F("error")] = F("unknown cmd"); + jsonOut[F("error")] = F(UNKNOWN_CMD); return false; } diff --git a/src/web/lang.h b/src/web/lang.h new file mode 100644 index 00000000..cb955110 --- /dev/null +++ b/src/web/lang.h @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __LANG_H__ +#define __LANG_H__ + +#ifdef LANG_DE + #define REBOOT_ESP_APPLY_CHANGES "starte AhoyDTU neu, um die Änderungen zu speichern" +#else /*LANG_EN*/ + #define REBOOT_ESP_APPLY_CHANGES "reboot AhoyDTU to apply all your configuration changes"; +#endif + +#ifdef LANG_DE + #define TIME_NOT_SET "keine gültige Zeit, daher keine Kommunikation zum Wechselrichter möglich" +#else /*LANG_EN*/ + #define TIME_NOT_SET "time not set. No communication to inverter possible"; +#endif + +#ifdef LANG_DE + #define INV_INDEX_INVALID "Wechselrichterindex ungültig; " +#else /*LANG_EN*/ + #define INV_INDEX_INVALID "inverter index invalid: " +#endif + +#ifdef LANG_DE + #define UNKNOWN_CMD "unbekanntes Kommando: '" +#else /*LANG_EN*/ + #define UNKNOWN_CMD "unknown cmd: '" +#endif + +#ifdef LANG_DE + #define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "Leistungsbegrenzung / Ansteuerung aktuell nicht möglich" +#else /*LANG_EN*/ + #define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "inverter does not accept dev control request at this moment" +#endif + +#ifdef LANG_DE + #define PATH_NOT_FOUND "Pfad nicht gefunden: " +#else /*LANG_EN*/ + #define PATH_NOT_FOUND "Path not found: " +#endif + +#ifdef LANG_DE + #define INCOMPLETE_INPUT "Unvollständige Eingabe" +#else /*LANG_EN*/ + #define INCOMPLETE_INPUT "Incomplete input" +#endif + +#ifdef LANG_DE + #define INVALID_INPUT "Ungültige Eingabe" +#else /*LANG_EN*/ + #define INVALID_INPUT "Invalid input" +#endif + +#ifdef LANG_DE + #define NOT_ENOUGH_MEM "nicht genügend Speicher" +#else /*LANG_EN*/ + #define NOT_ENOUGH_MEM "Not enough memory" +#endif + +#ifdef LANG_DE + #define DESER_FAILED "Deserialisierung fehlgeschlagen" +#else /*LANG_EN*/ + #define DESER_FAILED "Deserialization failed" +#endif + +#ifdef LANG_DE + #define INV_NOT_FOUND "Wechselrichter nicht gefunden!" +#else /*LANG_EN*/ + #define INV_NOT_FOUND "inverter not found!" +#endif + +#endif /*__LANG_H__*/ From e4cb948be48a6b40b9bbe37865fc43ba0a4fd903 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 9 Jan 2024 23:15:28 +0100 Subject: [PATCH 065/115] 0.8.50 fix English versions --- src/web/lang.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/lang.h b/src/web/lang.h index cb955110..a82c7888 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -9,13 +9,13 @@ #ifdef LANG_DE #define REBOOT_ESP_APPLY_CHANGES "starte AhoyDTU neu, um die Änderungen zu speichern" #else /*LANG_EN*/ - #define REBOOT_ESP_APPLY_CHANGES "reboot AhoyDTU to apply all your configuration changes"; + #define REBOOT_ESP_APPLY_CHANGES "reboot AhoyDTU to apply all your configuration changes" #endif #ifdef LANG_DE #define TIME_NOT_SET "keine gültige Zeit, daher keine Kommunikation zum Wechselrichter möglich" #else /*LANG_EN*/ - #define TIME_NOT_SET "time not set. No communication to inverter possible"; + #define TIME_NOT_SET "time not set. No communication to inverter possible" #endif #ifdef LANG_DE From 76fa30503d4a3a5096a01b42ee6fa5d26173a396 Mon Sep 17 00:00:00 2001 From: lumapu Date: Wed, 10 Jan 2024 23:53:24 +0100 Subject: [PATCH 066/115] 0.8.51 * fix translation #1346 * further improve sending active power control command faster #1332 * added history protection mask --- ahoy.code-workspace | 45 -------------------------------- scripts/auto_firmware_version.py | 3 ++- src/CHANGES.md | 5 ++++ src/app.cpp | 2 +- src/app.h | 4 +++ src/appInterface.h | 5 ++-- src/config/settings.h | 8 +++--- src/defines.h | 2 +- src/hm/hmInverter.h | 7 +++-- src/hm/hmSystem.h | 10 ++++--- src/utils/scheduler.h | 2 +- src/web/RestApi.h | 3 ++- src/web/html/index.html | 2 +- src/web/html/setup.html | 6 ++--- src/web/lang.json | 6 ++--- 15 files changed, 41 insertions(+), 69 deletions(-) delete mode 100644 ahoy.code-workspace diff --git a/ahoy.code-workspace b/ahoy.code-workspace deleted file mode 100644 index 20d5909e..00000000 --- a/ahoy.code-workspace +++ /dev/null @@ -1,45 +0,0 @@ -{ - "folders": [ - { - "path": "." - }, - { - "path": "src" - } - ], - "settings": { - "files.associations": { - "algorithm": "cpp", - "array": "cpp", - "chrono": "cpp", - "deque": "cpp", - "format": "cpp", - "forward_list": "cpp", - "functional": "cpp", - "initializer_list": "cpp", - "iterator": "cpp", - "list": "cpp", - "memory": "cpp", - "queue": "cpp", - "random": "cpp", - "regex": "cpp", - "vector": "cpp", - "xhash": "cpp", - "xlocmon": "cpp", - "xlocnum": "cpp", - "xmemory": "cpp", - "xstring": "cpp", - "xtree": "cpp", - "xutility": "cpp", - "*.tcc": "cpp", - "string": "cpp", - "unordered_map": "cpp", - "unordered_set": "cpp", - "string_view": "cpp", - "sstream": "cpp", - "istream": "cpp", - "ostream": "cpp" - }, - "editor.formatOnSave": false - } -} \ No newline at end of file diff --git a/scripts/auto_firmware_version.py b/scripts/auto_firmware_version.py index c4ab270d..75bf7379 100644 --- a/scripts/auto_firmware_version.py +++ b/scripts/auto_firmware_version.py @@ -21,7 +21,8 @@ def get_firmware_specifier_build_flag(): except: build_version = "g0000000" - build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\"" + build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\" " + build_flag += "-DENV_NAME=\\\"" + env["PIOENV"] + "\\\" "; print ("Firmware Revision: " + build_version) return (build_flag) diff --git a/src/CHANGES.md b/src/CHANGES.md index fd8b2ec7..89442499 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,10 @@ # Development Changes +## 0.8.51 - 2024-01-10 +* fix translation #1346 +* further improve sending active power control command faster #1332 +* added history protection mask + ## 0.8.50 - 2024-01-09 * merge PR: added history charts to web #1336 * merge PR: small display changes #1339 diff --git a/src/app.cpp b/src/app.cpp index cae48149..88135099 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -53,7 +53,7 @@ void app::setup() { mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); - mSys.setup(&mTimestamp, &mConfig->inst); + mSys.setup(&mTimestamp, &mConfig->inst, this); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { initInverter(i); } diff --git a/src/app.h b/src/app.h index a7a88609..a4e51113 100644 --- a/src/app.h +++ b/src/app.h @@ -307,6 +307,10 @@ class app : public IApp, public ah::Scheduler { #endif /* defined(ETHERNET) */ void updateNtp(void); + void triggerTickSend() { + once(std::bind(&app::tickSend, this), 0, "tSend"); + } + void tickCalcSunrise(void); void tickIVCommunication(void); void tickSun(void); diff --git a/src/appInterface.h b/src/appInterface.h index b812f5fc..5feccd3c 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -7,7 +7,6 @@ #define __IAPP_H__ #include "defines.h" -#include "hm/hmSystem.h" #if defined(ETHERNET) #include "AsyncWebServer_ESP32_W5500.h" #else @@ -47,6 +46,8 @@ class IApp { virtual void getSchedulerInfo(uint8_t *max) = 0; virtual void getSchedulerNames() = 0; + virtual void triggerTickSend() = 0; + virtual bool getRebootRequestState() = 0; virtual bool getSettingsValid() = 0; virtual void setMqttDiscoveryFlag() = 0; diff --git a/src/config/settings.h b/src/config/settings.h index b8d0d1e9..0e690ede 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -39,9 +39,9 @@ #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 PROT_MASK_HISTORY 0x0100 +#define PROT_MASK_HISTORY 0x0040 +#define PROT_MASK_API 0x0080 +#define PROT_MASK_MQTT 0x0100 #define DEF_PROT_INDEX 0x0001 #define DEF_PROT_LIVE 0x0000 @@ -49,9 +49,9 @@ #define DEF_PROT_SETUP 0x0008 #define DEF_PROT_UPDATE 0x0010 #define DEF_PROT_SYSTEM 0x0020 +#define DEF_PROT_HISTORY 0x0000 #define DEF_PROT_API 0x0000 #define DEF_PROT_MQTT 0x0000 -#define DEF_PROT_HISTORY 0x0000 typedef struct { diff --git a/src/defines.h b/src/defines.h index 9d0e97c9..91fee041 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 50 +#define VERSION_PATCH 51 //------------------------------------- typedef struct { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index aeb40c19..4b2cc40d 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -14,6 +14,7 @@ #define MAX_GRID_LENGTH 150 #include "hmDefines.h" +#include "../appInterface.h" #include "HeuristicInv.h" #include "../hms/hmsDefines.h" #include @@ -148,8 +149,9 @@ class Inverter { bool commEnabled; // 'pause night communication' sets this field to false uint32_t tsMaxAcPower; // holds the timestamp when the MaxAC power was seen - static uint32_t *timestamp; // system timestamp + static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup + static IApp *app; // pointer to app interface public: @@ -286,6 +288,7 @@ class Inverter { if(isConnected) { mDevControlRequest = true; devControlCmd = cmd; + app->triggerTickSend(); } return isConnected; } diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index b86f8d08..6ff741bf 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -1,11 +1,12 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __HM_SYSTEM_H__ #define __HM_SYSTEM_H__ +#include "../appInterface.h" #include "hmInverter.h" #include @@ -14,9 +15,10 @@ class HmSystem { public: HmSystem() {} - void setup(uint32_t *timestamp, cfgInst_t *config) { - mInverter[0].timestamp = timestamp; + void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) { + mInverter[0].timestamp = timestamp; mInverter[0].generalConfig = config; + mInverter[0].app = app; } void addInverter(uint8_t id, std::function *iv)> cb) { diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 23af29f8..9a01cda8 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -121,7 +121,7 @@ namespace ah { uint16_t mTsMillis; private: - inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { + 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; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 1ed55a93..73216ed5 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 2024 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __WEB_API_H__ @@ -255,6 +255,7 @@ class RestApi { obj[F("ts_now")] = mApp->getTimestamp(); obj[F("version")] = String(mApp->getVersion()); obj[F("build")] = String(AUTO_GIT_HASH); + obj[F("env")] = String(ENV_NAME); obj[F("menu_prot")] = mApp->getProtection(request); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); diff --git a/src/web/html/index.html b/src/web/html/index.html index 6ad2c787..99a42089 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -26,7 +26,7 @@
  • {#DISCUSS} Discord
  • {#REPORT} {#ISSUES}
  • {#CONTRIBUTE} {#DOCUMENTATION}
  • -
  • Download & Test {#DEV_FIRMWARE}, {#DEV_CHANGELOG}
  • +
  • Download & Test {#DEV_FIRMWARE}, {#DEV_CHANGELOG}
  • {#DON_MAKE} {#DONATION}
  • diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 00b0477c..62c52b3a 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -254,7 +254,7 @@
    -
    {MQTT_PASSWORD}
    +
    {#MQTT_PASSWORD}
    @@ -630,9 +630,9 @@ if(!obj["pwd_set"]) e.value = ""; var d = document.getElementById("prot_mask"); - var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System"]; + var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"]; var el = []; - for(var i = 0; i < 6; i++) { + for(var i = 0; i < 7; i++) { var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); el.push(mlCb("protMask" + i, a[i], chk)) } diff --git a/src/web/lang.json b/src/web/lang.json index 35e99b43..42c5e98a 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -136,7 +136,7 @@ { "token": "REBOOT_AT_MIDNIGHT", "en": "Reboot Ahoy at midnight", - "de": "um Mitternacht neu starten" + "de": "mitternachts neu starten" }, { "token": "DARK_MODE", @@ -291,7 +291,7 @@ { "token": "INV_RESET_MAX_MIDNIGHT", "en": "Reset 'max' values at midnight", - "de": "Maximalwerte bei Sonnenuntergang zurücksetzen" + "de": "Maximalwerte mitternachts zurücksetzen" }, { "token": "INV_START_WITHOUT_TIME", @@ -1096,7 +1096,7 @@ { "token": "APC", "en": "APC", - "de": "Begr." + "de": "Limit" }, { "token": "ALARMS", From 9a8d8560db5b31087735caaadd3be6c13e0759e0 Mon Sep 17 00:00:00 2001 From: you69man Date: Wed, 10 Jan 2024 22:57:47 +0100 Subject: [PATCH 067/115] possibly fix power graph problems --- src/plugins/Display/Display_Mono.h | 9 +++++++-- src/plugins/Display/Display_Mono_128X64.h | 5 +++-- src/plugins/Display/Display_Mono_84X48.h | 5 +++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 62d5cc3a..bd3ee30e 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -119,7 +119,7 @@ class DisplayMono { case DispSwitchState::TEXT: if (mDispSwitchTime.isTimeout()) { mDispSwitchState = DispSwitchState::GRAPH; - mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms + mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms change = true; } break; @@ -135,6 +135,7 @@ class DisplayMono { } void initPowerGraph(uint8_t width, uint8_t height) { + DBGPRINTLN("---- Init Power Graph ----"); mPgWidth = width; mPgHeight = height; mPgData = new float[mPgWidth]; @@ -205,6 +206,10 @@ class DisplayMono { mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis + // do not draw as long as time is not set correctly and no data was received + if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs < 1) || (mPgMaxPwr < 1) || (mPgLastPos < 1)) + return; + // draw X scale tmElements_t tm; breakTime(mDisplayData->pGraphEndTime, tm); @@ -234,7 +239,7 @@ class DisplayMono { // draw curve for (uint8_t i = 1; i <= mPgLastPos; i++) { mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), - xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); } // print max power value diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index b4c1cfe4..2d5c13c2 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -58,7 +58,8 @@ class DisplayMono128X64 : public DisplayMono { widthShrink = (mCfg->screenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver - initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); + if (mCfg->graph_ratio > 0) + initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); @@ -172,7 +173,7 @@ class DisplayMono128X64 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == DispSwitchState::GRAPH) { + if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) { // plot power graph plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1); } diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index 51c5eafe..d9f1d98d 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -46,7 +46,8 @@ class DisplayMono84X48 : public DisplayMono { break; } - initPowerGraph(mDispWidth - 16, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); + if (mCfg->graph_ratio > 0) + initPowerGraph(mDispWidth - 16, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2); printText("Ahoy!", l_Ahoy, 0xff); printText("ahoydtu.de", l_Website, 0xff); @@ -134,7 +135,7 @@ class DisplayMono84X48 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == DispSwitchState::GRAPH) { + if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) { // plot power graph plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1); } From ed3e93274e4854bec49b0d1da4430d1e009f4df9 Mon Sep 17 00:00:00 2001 From: lumapu Date: Thu, 11 Jan 2024 00:20:55 +0100 Subject: [PATCH 068/115] 0.8.51 * added history protection mask * merge PR: display graph improvements #1347 --- src/CHANGES.md | 1 + src/hm/hmInverter.h | 4 ++-- src/hm/hmSystem.h | 2 +- src/plugins/Display/Display_Mono.h | 4 ++-- src/web/RestApi.h | 2 ++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 89442499..ca123e3b 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -4,6 +4,7 @@ * fix translation #1346 * further improve sending active power control command faster #1332 * added history protection mask +* merge PR: display graph improvements #1347 ## 0.8.50 - 2024-01-09 * merge PR: added history charts to web #1336 diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 4b2cc40d..1f623d85 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -151,7 +151,7 @@ class Inverter { static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup - static IApp *app; // pointer to app interface + //static IApp *app; // pointer to app interface public: @@ -288,7 +288,7 @@ class Inverter { if(isConnected) { mDevControlRequest = true; devControlCmd = cmd; - app->triggerTickSend(); + //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)" } return isConnected; } diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index 6ff741bf..8f997104 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -18,7 +18,7 @@ class HmSystem { void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) { mInverter[0].timestamp = timestamp; mInverter[0].generalConfig = config; - mInverter[0].app = app; + //mInverter[0].app = app; } void addInverter(uint8_t id, std::function *iv)> cb) { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index bd3ee30e..185e066e 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -135,7 +135,7 @@ class DisplayMono { } void initPowerGraph(uint8_t width, uint8_t height) { - DBGPRINTLN("---- Init Power Graph ----"); + DBGPRINTLN(F("---- Init Power Graph ----")); mPgWidth = width; mPgHeight = height; mPgData = new float[mPgWidth]; @@ -207,7 +207,7 @@ class DisplayMono { mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis // do not draw as long as time is not set correctly and no data was received - if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs < 1) || (mPgMaxPwr < 1) || (mPgLastPos < 1)) + if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs != 0) || (mPgMaxPwr != 0) || (mPgLastPos != 0)) return; // draw X scale diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 73216ed5..8148eb20 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -842,6 +842,8 @@ class RestApi { iv->powerLimit[1] = AbsolutNonPersistent; accepted = iv->setDevControlRequest(ActivePowerContr); + if(accepted) + mApp->triggerTickSend(); } else if(F("dev") == jsonIn[F("cmd")]) { DPRINTLN(DBG_INFO, F("dev cmd")); iv->setDevCommand(jsonIn[F("val")].as()); From 9f39e5c1509c2ef264fb1d2ca68b9aa1c3b11bf4 Mon Sep 17 00:00:00 2001 From: lumapu Date: Thu, 11 Jan 2024 00:42:09 +0100 Subject: [PATCH 069/115] 0.8.51 fix display (wrong correction) fix dependency (GxEPD2) --- src/platformio.ini | 10 +++++----- src/plugins/Display/Display_Mono.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/platformio.ini b/src/platformio.ini index a2397596..89040215 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -32,7 +32,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2 @ ^1.5.3 + https://github.com/zinggjm/GxEPD2#1.5.3 build_flags = -std=c++17 -std=gnu++17 @@ -151,7 +151,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2 @ ^1.5.3 + https://github.com/zinggjm/GxEPD2#1.5.3 build_flags = ${env.build_flags} -D ETHERNET -DRELEASE @@ -173,7 +173,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2 @ ^1.5.3 + https://github.com/zinggjm/GxEPD2#1.5.3 build_flags = ${env.build_flags} -D ETHERNET -DRELEASE @@ -321,7 +321,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2 @ ^1.5.3 + https://github.com/zinggjm/GxEPD2#1.5.3 upload_protocol = esp-builtin build_flags = ${env.build_flags} -DETHERNET @@ -363,7 +363,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.3 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2 @ ^1.5.3 + https://github.com/zinggjm/GxEPD2#1.5.3 upload_protocol = esp-builtin build_flags = ${env.build_flags} -DETHERNET diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 185e066e..008f7378 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -207,7 +207,7 @@ class DisplayMono { mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis // do not draw as long as time is not set correctly and no data was received - if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs != 0) || (mPgMaxPwr != 0) || (mPgLastPos != 0)) + if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs < 1) || (mPgMaxPwr < 1) || (mPgLastPos < 1)) return; // draw X scale From 455d29a6fa807ac1f2abda8b35fcd36259b41d89 Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 12 Jan 2024 00:00:52 +0100 Subject: [PATCH 070/115] 0.8.52 * possible fix of 'division by zero' #1345 * fix lang #1348 #1346 * fix timestamp `max AC power` #1324 * fix stylesheet overlay `max AC power` #1324 * fix download link #1340 * fix history graph * try to fix #1331 --- src/CHANGES.md | 9 +++++++++ src/defines.h | 2 +- src/hm/hmInverter.h | 8 ++++---- src/plugins/Display/Display_Mono.h | 15 +++++++++------ src/plugins/Display/Display_Mono_128X32.h | 4 ++-- src/plugins/Display/Display_Mono_128X64.h | 8 ++++---- src/plugins/Display/Display_Mono_64X48.h | 2 +- src/plugins/Display/Display_Mono_84X48.h | 2 +- src/plugins/history.h | 18 ++++++++++-------- src/publisher/pubMqttIvData.h | 2 +- src/web/html/history.html | 12 ++++++++---- src/web/html/index.html | 2 +- src/web/html/setup.html | 4 ++-- src/web/html/style.css | 2 +- src/web/html/visualization.html | 4 ++-- src/web/lang.json | 15 +++++++++++++++ 16 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index ca123e3b..34ea7b72 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,14 @@ # Development Changes +## 0.8.52 - 2024-01-11 +* possible fix of 'division by zero' #1345 +* fix lang #1348 #1346 +* fix timestamp `max AC power` #1324 +* fix stylesheet overlay `max AC power` #1324 +* fix download link #1340 +* fix history graph +* try to fix #1331 + ## 0.8.51 - 2024-01-10 * fix translation #1346 * further improve sending active power control command faster #1332 diff --git a/src/defines.h b/src/defines.h index 91fee041..e6a1d147 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 51 +#define VERSION_PATCH 52 //------------------------------------- typedef struct { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 1f623d85..7bbf1019 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -961,8 +961,10 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { acMaxPower = iv->getValue(i, rec); } } - if(acPower > acMaxPower) + if(acPower > acMaxPower) { + iv->tsMaxAcPower = *iv->timestamp; return acPower; + } } return acMaxPower; } @@ -981,10 +983,8 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { dcMaxPower = iv->getValue(i, rec); } } - if(dcPower > dcMaxPower) { - iv->tsMaxAcPower = *iv->timestamp; + if(dcPower > dcMaxPower) return dcPower; - } } return dcMaxPower; } diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 008f7378..a0d46d11 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -166,7 +166,10 @@ class DisplayMono { } uint8_t sss2pgpos(uint seconds_since_start) { - return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); + uint32_t diff = (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime); + if(diff) + return (seconds_since_start * (mPgWidth - 1) / diff); + return 0; } void calcPowerGraphValues() { @@ -175,6 +178,8 @@ class DisplayMono { mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data resetPowerGraph(); + if(0 == mPgPeriod) + mPgPeriod = 1; mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day } @@ -190,15 +195,13 @@ class DisplayMono { uint8_t getPowerGraphXpos(uint8_t p) { if ((p <= mPgLastPos) && (mPgLastPos > 0)) return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - else - return(0); + return 0; } uint8_t getPowerGraphYpos(uint8_t p) { - if (p < mPgWidth) + if ((p < mPgWidth) && (mPgMaxPwr > 0)) return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - else - return(0); + return 0; } void plotPowerGraph(uint8_t xoff, uint8_t yoff) { diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index 6ab21a6b..e904769f 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -107,7 +107,7 @@ class DisplayMono128X32 : public DisplayMono { void printText(const char *text, uint8_t line) { setFont(line); - uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift; + uint8_t dispX = mLineXOffsets[line] + (pixelShiftRange / 2) + mPixelshift; if (isTwoRowLine(line)) { String stringText = String(text); diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 2d5c13c2..34f35834 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -193,13 +193,13 @@ class DisplayMono128X64 : public DisplayMono { mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); char sym[]=" "; sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF - mDisplay->drawStr(widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->drawStr((widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym); if (mDisplayData->MQTTSymbol) sym[0] = 'J'; // MQTT else sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi - mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - (widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym); mDisplay->sendBuffer(); mExtra++; @@ -241,8 +241,8 @@ class DisplayMono128X64 : public DisplayMono { mLineYOffsets[i] = yOff; dsc = mDisplay->getDescent(); yOff -= dsc; - if (l_Time == i) // prevent time and status line to touch - yOff++; // -> one pixels space + if (l_Time == i) // prevent time and status line to touch + yOff++; // -> one pixels space i++; } while(l_MAX_LINES>i); } diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 68aa3cc4..a4ddc6ad 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -96,7 +96,7 @@ class DisplayMono64X48 : public DisplayMono { } void printText(const char *text, uint8_t line) { - uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift; + uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift; setFont(line); mDisplay->drawStr(dispX, mLineYOffsets[line], text); diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index d9f1d98d..175fa17e 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- diff --git a/src/plugins/history.h b/src/plugins/history.h index 9ed7860d..c4d3ab35 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -28,7 +28,7 @@ class HistoryData { uint16_t dispIdx; // index for 1st Element to display from WattArr bool wrapped; // ring buffer for watt history - std::array data; + std::array data; void reset() { loopCnt = 0; @@ -78,13 +78,15 @@ class HistoryData { mMaximumDay = roundf(maxPwr); } - if (*mTs > mApp->getSunset()) { - if ((!mDayStored) && (yldDay > 0)) { - addValue(&mYieldDay, roundf(yldDay)); - mDayStored = true; - } - } else if (*mTs > mApp->getSunrise()) - mDayStored = false; + if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + if (*mTs > mApp->getSunset()) { + if ((!mDayStored) && (yldDay > 0)) { + addValue(&mYieldDay, roundf(yldDay)); + mDayStored = true; + } + } else if (*mTs > mApp->getSunrise()) + mDayStored = false; + } } uint16_t valueAt(HistoryStorageType type, uint16_t i) { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 0a59fdd6..9a76d646 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -141,7 +141,7 @@ class PubMqttIvData { // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { - if(mIv->getStatus() != InverterStatus::OFF) { + if(mIv->getStatus() > InverterStatus::OFF) { if(mIv->config->add2Total) { mTotalFound = true; switch (rec->assign[mPos].fieldId) { diff --git a/src/web/html/history.html b/src/web/html/history.html index d0f33755..975a02ed 100644 --- a/src/web/html/history.html +++ b/src/web/html/history.html @@ -44,19 +44,23 @@ function parseHistory(obj, namePrefix, execOnce) { mRefresh = obj.refresh var data = Object.assign({}, obj.value) - var numDataPts = data.length + numDataPts = Object.keys(data).length if (true == execOnce) { - let s = svg(null, (numDataPts + 2) * 2, mChartHeight, "chart"); + let s = document.createElementNS(svgns, "svg"); + s.setAttribute("class", "chart"); + s.setAttribute("width", (numDataPts + 2) * 2); + s.setAttribute("height", mChartHeight); s.setAttribute("role", "img"); + let g = document.createElementNS(svgns, "g"); s.appendChild(g); for (var i = 0; i < numDataPts; i++) { val = data[i]; let rect = document.createElementNS(svgns, "rect"); rect.setAttribute("id", namePrefix+"Rect" + i); - rect.setAttribute("x", String(i * 2) + ""); - rect.setAttribute("width", String(2) + ""); + rect.setAttribute("x", i * 2); + rect.setAttribute("width", 2); g.appendChild(rect); } document.getElementById(namePrefix+"Chart").appendChild(s); diff --git a/src/web/html/index.html b/src/web/html/index.html index 99a42089..e7b7afc4 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -26,7 +26,7 @@
  • {#DISCUSS} Discord
  • {#REPORT} {#ISSUES}
  • {#CONTRIBUTE} {#DOCUMENTATION}
  • -
  • Download & Test {#DEV_FIRMWARE}, {#DEV_CHANGELOG}
  • +
  • Download & Test {#DEV_FIRMWARE}, {#DEV_CHANGELOG}
  • {#DON_MAKE} {#DONATION}
  • diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 62c52b3a..e75faf5b 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -269,7 +269,7 @@
    Discovery Config (homeassistant)
    - +
    @@ -309,7 +309,7 @@
    {#BTN_REBOOT_SUCCESSFUL_SAVE}
    - +
    diff --git a/src/web/html/style.css b/src/web/html/style.css index ef581ffa..395fcb99 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -666,7 +666,7 @@ div.hr { } -.tooltip{ +.tooltip:hover { position: relative; } .tooltip:hover:after { diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 15b0c7b5..b99501f3 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -138,7 +138,7 @@ ml("div", {class: "row mt-2"},[ numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}), numMid(obj.ch[0][8], "W", "{#DC_POWER}"), - numMid(obj.ch[0][0], "V", "{#DC_VOLTAGE}"), + numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"), numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"), numMid(obj.ch[0][3], "Hz", "{#FREQUENCY}"), numMid(obj.ch[0][9], "%", "{#EFFICIENCY}"), @@ -362,7 +362,7 @@ var v = getGridValue(glob); if(null === g) { if(0 == obj.grid.length) { - content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}?")))) + content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}")))) } else { content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}")))) content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}.")))) diff --git a/src/web/lang.json b/src/web/lang.json index 42c5e98a..6717306d 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -433,6 +433,16 @@ "en": "Line 1-4", "de": "Zeile 1-4" }, + { + "token": "BTN_SAVE", + "en": "save", + "de": "speichern" + }, + { + "token": "BTN_SEND", + "en": "send", + "de": "senden" + }, { "token": "BTN_REBOOT_SUCCESSFUL_SAVE", "en": "Reboot device after successful save", @@ -1118,6 +1128,11 @@ "en": "DC Voltage", "de": "DC Spannung" }, + { + "token": "AC_VOLTAGE", + "en": "AC Voltage", + "de": "Netzspannung" + }, { "token": "AC_CURRENT", "en": "AC Current", From ca6ebfe0fe95d8cfd95956243e4f94f43070de90 Mon Sep 17 00:00:00 2001 From: lumapu Date: Fri, 12 Jan 2024 21:42:22 +0100 Subject: [PATCH 071/115] 0.8.53 * fix history graph * fix MqTT yield day #1331 --- src/CHANGES.md | 4 ++++ src/app.cpp | 16 ++++++++++------ src/app.h | 1 + src/defines.h | 2 +- src/publisher/pubMqtt.h | 15 +++++---------- src/publisher/pubMqttIvData.h | 35 ++++++++++++++++++----------------- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 34ea7b72..3fb87fce 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.53 - 2024-01-12 +* fix history graph +* fix MqTT yield day #1331 + ## 0.8.52 - 2024-01-11 * possible fix of 'division by zero' #1345 * fix lang #1348 #1346 diff --git a/src/app.cpp b/src/app.cpp index 88135099..97afe16f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -152,7 +152,7 @@ void app::regularTickers(void) { //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif - everySec(std::bind(&HistoryType::tickerSecond, mHistory), "hist"); + everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist"); } #if defined(ETHERNET) @@ -241,7 +241,7 @@ void app::tickCalcSunrise(void) { if (mMqttEnabled) { tickSun(); nxtTrig = mSunrise + mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly - onceAt(std::bind(&app::tickSun, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm' + onceAt(std::bind(&app::tickSunrise, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm' } } @@ -291,6 +291,13 @@ void app::tickSun(void) { once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry } +//----------------------------------------------------------------------------- +void app::tickSunrise(void) { + // only used and enabled by MQTT (see setup()) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening, true)) + once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry +} + //----------------------------------------------------------------------------- void app::tickZeroValues(void) { zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY); @@ -423,11 +430,8 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { changed = true; } - if(changed) { - if(mMqttEnabled && !skipYieldDay) - mMqtt.setZeroValuesEnable(); + if(changed) payloadEventListener(RealTimeRunData_Debug, NULL); - } } //----------------------------------------------------------------------------- diff --git a/src/app.h b/src/app.h index a4e51113..04a7f1cc 100644 --- a/src/app.h +++ b/src/app.h @@ -314,6 +314,7 @@ class app : public IApp, public ah::Scheduler { void tickCalcSunrise(void); void tickIVCommunication(void); void tickSun(void); + void tickSunrise(void); void tickComm(void); void tickSend(void); void tickMinute(void); diff --git a/src/defines.h b/src/defines.h index e6a1d147..0fb35f2f 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 52 +#define VERSION_PATCH 53 //------------------------------------- typedef struct { diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index a1efa5fb..e36338c9 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -43,7 +43,6 @@ class PubMqtt { memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS); memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); mLastAnyAvail = false; - mZeroValues = false; } ~PubMqtt() { } @@ -134,7 +133,7 @@ class PubMqtt { #endif } - bool tickerSun(uint32_t sunrise, uint32_t sunset, int16_t offsM, int16_t offsE) { + bool tickerSun(uint32_t sunrise, uint32_t sunset, int16_t offsM, int16_t offsE, bool isSunrise = false) { if (!mClient.connected()) return false; @@ -153,10 +152,12 @@ class PubMqtt { publish(mSubTopic, ((iv->commEnabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); } - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise + offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + if(isSunrise) + mSendIvData.resetYieldDay(); + return true; } @@ -239,10 +240,6 @@ class PubMqtt { } } - void setZeroValuesEnable(void) { - mZeroValues = true; - } - private: void onConnect(bool sessionPreset) { DPRINTLN(DBG_INFO, F("MQTT connected")); @@ -592,8 +589,7 @@ class PubMqtt { if(mSendList.empty()) return; - mSendIvData.start(mZeroValues); - mZeroValues = false; + mSendIvData.start(); mLastAnyAvail = anyAvail; } @@ -612,7 +608,6 @@ class PubMqtt { std::array mSendAlarm{}; subscriptionCb mSubscriptionCb; bool mLastAnyAvail; - bool mZeroValues; InverterStatus mLastIvState[MAX_NUM_INVERTERS]; uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; uint16_t mIntervalTimeout; diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 9a76d646..c6fc5d10 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -22,11 +22,11 @@ template class PubMqttIvData { public: void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue *sendList) { - mSys = sys; - mUtcTimestamp = utcTs; - mSendList = sendList; - mState = IDLE; - mZeroValues = false; + mSys = sys; + mUtcTimestamp = utcTs; + mSendList = sendList; + mState = IDLE; + mYldTotalStore = 0; mRTRDataHasBeenSent = false; @@ -42,11 +42,14 @@ class PubMqttIvData { yield(); } - bool start(bool zeroValues = false) { + void resetYieldDay() { + mYldTotalStore = 0; + } + + bool start() { if(IDLE != mState) return false; - mZeroValues = zeroValues; mRTRDataHasBeenSent = false; mState = START; return true; @@ -117,10 +120,14 @@ class PubMqttIvData { mIv->isProducing(); // recalculate status mState = SEND_DATA; } else if(mSendTotals && mTotalFound) { + if(mYldTotalStore > mTotal[2]) + mSendTotalYd = false; // don't send yield total if last value was greater + else + mYldTotalStore = mTotal[2]; + mState = SEND_TOTALS; } else { mSendList->pop(); - mZeroValues = false; mState = START; } } @@ -141,7 +148,7 @@ class PubMqttIvData { // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { - if(mIv->getStatus() > InverterStatus::OFF) { + if(mIv->getStatus() != InverterStatus::OFF) { if(mIv->config->add2Total) { mTotalFound = true; switch (rec->assign[mPos].fieldId) { @@ -152,11 +159,7 @@ class PubMqttIvData { mTotal[1] += mIv->getValue(mPos, rec); break; case FLD_YD: { - float val = mIv->getValue(mPos, rec); - if(0 == val) // inverter restarted during day - mSendTotalYd = false; - else - mTotal[2] += val; + mTotal[2] += mIv->getValue(mPos, rec); break; } case FLD_PDC: @@ -236,7 +239,6 @@ class PubMqttIvData { mPos++; } else { mSendList->pop(); - mZeroValues = false; mPos = 0; mState = IDLE; } @@ -251,7 +253,7 @@ class PubMqttIvData { uint8_t mCmd; uint8_t mLastIvId; bool mSendTotals, mTotalFound, mAllTotalFound, mSendTotalYd; - float mTotal[4]; + float mTotal[4], mYldTotalStore; Inverter<> *mIv, *mIvSend; uint8_t mPos; @@ -259,7 +261,6 @@ class PubMqttIvData { char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mVal[140]; - bool mZeroValues; // makes sure that yield day is sent even if no inverter is online std::queue *mSendList; }; From 873f5a71419d018962162084927e55f97ca5e812 Mon Sep 17 00:00:00 2001 From: geronet1 Date: Sat, 13 Jan 2024 20:37:15 +0100 Subject: [PATCH 072/115] ETH support for CMT2300A - HMS/HMT --- src/config/config.h | 10 ++++++++++ src/hms/cmtHal.h | 2 +- src/hms/esp32_3wSpi.h | 10 +++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/config/config.h b/src/config/config.h index 40cb5b76..058caf3f 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -93,6 +93,16 @@ #define DEF_NRF_SCLK_PIN 18 #endif +#if defined(ETHERNET) + #ifndef DEF_CMT_SPI_HOST + #define DEF_CMT_SPI_HOST SPI3_HOST + #endif +#else + #ifndef DEF_CMT_SPI_HOST + #define DEF_CMT_SPI_HOST SPI2_HOST + #endif +#endif /* defined(ETHERNET) */ + #ifndef DEF_CMT_SCLK #define DEF_CMT_SCLK 12 #endif diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h index d5e31ea0..768b8da5 100644 --- a/src/hms/cmtHal.h +++ b/src/hms/cmtHal.h @@ -17,7 +17,7 @@ class cmtHal : public SpiPatcherHandle { public: cmtHal() { - mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST); + mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST); } void patch() override { diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h index 20c42632..c562671a 100644 --- a/src/hms/esp32_3wSpi.h +++ b/src/hms/esp32_3wSpi.h @@ -21,7 +21,7 @@ // for ESP32 this is the so-called HSPI // for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, // it is simply the first externally usable hardware SPI master controller -#define SPI_CMT SPI2_HOST +//#define SPI_CMT SPI2_HOST class esp32_3wSpi { public: @@ -54,8 +54,8 @@ class esp32_3wSpi { .post_cb = NULL, }; - ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED)); - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg)); + ESP_ERROR_CHECK(spi_bus_initialize(DEF_CMT_SPI_HOST, &buscfg, SPI_DMA_DISABLED)); + ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg, &spi_reg)); // FiFo spi_device_interface_config_t devcfg2 = { @@ -72,9 +72,9 @@ class esp32_3wSpi { .pre_cb = NULL, .post_cb = NULL, }; - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); + ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg2, &spi_fifo)); - esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[SPI_CMT].spid_out, true, false); + esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[DEF_CMT_SPI_HOST].spid_out, true, false); delay(100); //pinMode(pinGpio3, INPUT); From 60111d0696671370889c6dea360b171217af114c Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 01:52:56 +0100 Subject: [PATCH 073/115] 0.8.54 * added minimal version (without: MqTT, Display, History), WebUI is not changed! * added simulator (must be activated before compile, standard: off) * changed communication attempts back to 5 --- .github/workflows/compile_development.yml | 2 + src/CHANGES.md | 5 + src/app.cpp | 29 ++++ src/app.h | 61 +++++++- src/config/config.h | 34 ++++- src/config/config_override_example.h | 8 - src/defines.h | 3 +- src/hm/CommQueue.h | 7 +- src/hm/Communication.h | 6 +- src/hm/simulator.h | 175 ++++++++++++++++++++++ src/platformio.ini | 106 +++++++++++-- src/plugins/Display/Display.h | 4 + src/plugins/history.h | 5 +- src/publisher/pubMqtt.h | 2 + src/web/RestApi.h | 6 +- src/web/html/setup.html | 4 +- src/web/lang.json | 4 +- 17 files changed, 420 insertions(+), 41 deletions(-) create mode 100644 src/hm/simulator.h diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 7eaf9b34..90a6293f 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -25,11 +25,13 @@ jobs: - esp8266-prometheus - esp8285 - esp32-wroom32 + - esp32-wroom32-minimal - esp32-wroom32-prometheus - esp32-wroom32-ethernet - esp32-s2-mini - esp32-c3-mini - opendtufusion + - opendtufusion-minimal - opendtufusion-ethernet steps: - uses: actions/checkout@v3 diff --git a/src/CHANGES.md b/src/CHANGES.md index 3fb87fce..6b3d0f80 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,10 @@ # Development Changes +## 0.8.54 - 2024-01-13 +* added minimal version (without: MqTT, Display, History), WebUI is not changed! +* added simulator (must be activated before compile, standard: off) +* changed communication attempts back to 5 + ## 0.8.53 - 2024-01-12 * fix history graph * fix MqTT yield day #1331 diff --git a/src/app.cpp b/src/app.cpp index 97afe16f..8cbe775d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -52,7 +52,9 @@ void app::setup() { mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + #if defined(ENABLE_MQTT) mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); + #endif mSys.setup(&mTimestamp, &mConfig->inst, this); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { initInverter(i); @@ -65,6 +67,7 @@ void app::setup() { // when WiFi is in client mode, then enable mqtt broker #if !defined(AP_ONLY) + #if defined(ENABLE_MQTT) mMqttEnabled = (mConfig->mqtt.broker[0] > 0); if (mMqttEnabled) { mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); @@ -72,6 +75,7 @@ void app::setup() { mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); } #endif + #endif setupLed(); mWeb.setup(this, &mSys, mConfig); @@ -92,7 +96,9 @@ void app::setup() { #endif #endif + #if defined(ENABLE_HISTORY) mHistory.setup(this, &mSys, mConfig, &mTimestamp); + #endif /*ENABLE_HISTORY*/ mPubSerial.setup(mConfig, &mSys, &mTimestamp); @@ -100,6 +106,13 @@ void app::setup() { //mImprov.setup(this, mConfig->sys.deviceName, mVersion); #endif + #if defined(ENABLE_SIMULATOR) + mSimulator.setup(&mSys, &mTimestamp, 0); + mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { + payloadEventListener(cmd, iv); + }); + #endif /*ENABLE_SIMULATOR*/ + regularTickers(); } @@ -115,8 +128,10 @@ void app::loop(void) { ah::Scheduler::loop(); mCommunication.loop(); + #if defined(ENABLE_MQTT) if (mMqttEnabled && mNetworkConnected) mMqtt.loop(); + #endif } //----------------------------------------------------------------------------- @@ -152,7 +167,13 @@ void app::regularTickers(void) { //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif + #if defined(ENABLE_HISTORY) everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist"); + #endif /*ENABLE_HISTORY*/ + + #if defined(ENABLE_SIMULATOR) + every(std::bind(&SimulatorType::tick, &mSimulator), 5, "sim"); + #endif /*ENABLE_SIMULATOR*/ } #if defined(ETHERNET) @@ -168,11 +189,13 @@ void app::onNtpUpdate(bool gotTime) { //----------------------------------------------------------------------------- void app::updateNtp(void) { + #if defined(ENABLE_MQTT) if (mMqttReconnect && mMqttEnabled) { mMqtt.tickerSecond(); everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); } + #endif /*ENABLE_MQTT*/ // only install schedulers once even if NTP wasn't successful in first loop if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed @@ -287,15 +310,19 @@ void app::tickIVCommunication(void) { //----------------------------------------------------------------------------- void app::tickSun(void) { // only used and enabled by MQTT (see setup()) + #if defined(ENABLE_MQTT) if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening)) once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry + #endif } //----------------------------------------------------------------------------- void app::tickSunrise(void) { // only used and enabled by MQTT (see setup()) + #if defined(ENABLE_MQTT) if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening, true)) once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry + #endif } //----------------------------------------------------------------------------- @@ -340,8 +367,10 @@ void app::tickMidnight(void) { if (mConfig->inst.rstYieldMidNight) { zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); + #if defined(ENABLE_MQTT) if (mMqttEnabled) mMqtt.tickerMidnight(); + #endif } } diff --git a/src/app.h b/src/app.h index 04a7f1cc..194f291e 100644 --- a/src/app.h +++ b/src/app.h @@ -17,14 +17,18 @@ #if defined(ESP32) #include "hms/hmsRadio.h" #endif +#if defined(ENABLE_MQTT) #include "publisher/pubMqtt.h" +#endif /*ENABLE_MQTT*/ #include "publisher/pubSerial.h" #include "utils/crc.h" #include "utils/dbg.h" #include "utils/scheduler.h" #include "utils/syslog.h" #include "web/RestApi.h" +#if defined(ENABLE_HISTORY) #include "plugins/history.h" +#endif /*ENABLE_HISTORY*/ #include "web/web.h" #include "hm/Communication.h" #if defined(ETHERNET) @@ -34,6 +38,10 @@ #include "utils/improv.h" #endif /* defined(ETHERNET) */ +#if defined(ENABLE_SIMULATOR) + #include "hm/simulator.h" +#endif /*ENABLE_SIMULATOR*/ + #include // position is relevant since version 1.4.7 of this library @@ -46,9 +54,16 @@ typedef HmSystem HmSystemType; typedef Web WebType; typedef RestApi RestApiType; +#if defined(ENABLE_MQTT) typedef PubMqtt PubMqttType; +#endif /*ENABLE_MQTT*/ typedef PubSerial PubSerialType; +#if defined(ENABLE_HISTORY) typedef HistoryData HistoryType; +#endif /*ENABLE_HISTORY*/ +#if defined (ENABLE_SIMULATOR) +typedef Simulator SimulatorType; +#endif /*ENABLE_SIMULATOR*/ // PLUGINS #if defined(PLUGIN_DISPLAY) @@ -190,19 +205,33 @@ class app : public IApp, public ah::Scheduler { } void setMqttDiscoveryFlag() { + #if defined(ENABLE_MQTT) once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); + #endif } bool getMqttIsConnected() { - return mMqtt.isConnected(); + #if defined(ENABLE_MQTT) + return mMqtt.isConnected(); + #else + return false; + #endif } uint32_t getMqttTxCnt() { - return mMqtt.getTxCnt(); + #if defined(ENABLE_MQTT) + return mMqtt.getTxCnt(); + #else + return 0; + #endif } uint32_t getMqttRxCnt() { - return mMqtt.getRxCnt(); + #if defined(ENABLE_MQTT) + return mMqtt.getRxCnt(); + #else + return 0; + #endif } bool getProtection(AsyncWebServerRequest *request) { @@ -253,11 +282,19 @@ class app : public IApp, public ah::Scheduler { } uint16_t getHistoryValue(uint8_t type, uint16_t i) { - return mHistory.valueAt((HistoryStorageType)type, i); + #if defined(ENABLE_HISTORY) + return mHistory.valueAt((HistoryStorageType)type, i); + #else + return 0; + #endif } uint16_t getHistoryMaxDay() { - return mHistory.getMaximumDay(); + #if defined(ENABLE_HISTORY) + return mHistory.getMaximumDay(); + #else + return 0; + #endif } private: @@ -269,8 +306,10 @@ class app : public IApp, public ah::Scheduler { void payloadEventListener(uint8_t cmd, Inverter<> *iv) { #if !defined(AP_ONLY) - if (mMqttEnabled) - mMqtt.payloadEventListener(cmd, iv); + #if defined(ENABLE_MQTT) + if (mMqttEnabled) + mMqtt.payloadEventListener(cmd, iv); + #endif /*ENABLE_MQTT*/ #endif #if defined(PLUGIN_DISPLAY) if(mConfig->plugin.display.type != 0) @@ -359,7 +398,9 @@ class app : public IApp, public ah::Scheduler { bool mNetworkConnected; // mqtt + #if defined(ENABLE_MQTT) PubMqttType mMqtt; + #endif /*ENABLE_MQTT*/ bool mMqttReconnect; bool mMqttEnabled; @@ -372,7 +413,13 @@ class app : public IApp, public ah::Scheduler { DisplayType mDisplay; DisplayData mDispData; #endif + #if defined(ENABLE_HISTORY) HistoryType mHistory; + #endif /*ENABLE_HISTORY*/ + + #if defined(ENABLE_SIMULATOR) + SimulatorType mSimulator; + #endif /*ENABLE_SIMULATOR*/ }; #endif /*__APP_H__*/ diff --git a/src/config/config.h b/src/config/config.h index 40cb5b76..5e6ae42c 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -32,12 +32,33 @@ // timeout for automatic logoff (20 minutes) #define LOGOUT_TIMEOUT (20 * 60) + +//------------------------------------- +// MODULE SELECTOR - done by platform.ini +//------------------------------------- + +// MqTT connection +//#define ENABLE_MQTT + +// display plugin +//#define PLUGIN_DISPLAY + +// history graph (WebUI) +//#define ENABLE_HISTORY + +// inverter simulation +//#define ENABLE_SIMULATOR + +// to enable the syslog logging (will disable web-serial) +//#define ENABLE_SYSLOG + + + //------------------------------------- // CONFIGURATION - COMPILE TIME //------------------------------------- // ethernet - #if defined(ETHERNET) #define ETH_SPI_HOST SPI2_HOST #define ETH_SPI_CLOCK_MHZ 25 @@ -184,7 +205,7 @@ #define INVERTER_OFF_THRES_SEC 15*60 // threshold of minimum power on which the inverter is marked as inactive -#define INACT_PWR_THRESH 3 +#define INACT_PWR_THRESH 1 // Timezone #define TIMEZONE 1 @@ -222,6 +243,15 @@ // reconnect delay #define MQTT_RECONNECT_DELAY 5000 + +// syslog settings +#ifdef ENABLE_SYSLOG +#define SYSLOG_HOST "" +#define SYSLOG_APP "ahoy" +#define SYSLOG_FACILITY FAC_USER +#define SYSLOG_PORT 514 +#endif + #if __has_include("config_override.h") #include "config_override.h" #endif diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h index b90bbdbd..44623c1f 100644 --- a/src/config/config_override_example.h +++ b/src/config/config_override_example.h @@ -35,13 +35,5 @@ // #define ENABLE_PROMETHEUS_EP -// to enable the syslog logging (will disable web-serial) -//#define ENABLE_SYSLOG -#ifdef ENABLE_SYSLOG -#define SYSLOG_HOST "" -#define SYSLOG_APP "ahoy" -#define SYSLOG_FACILITY FAC_USER -#define SYSLOG_PORT 514 -#endif #endif /*__CONFIG_OVERRIDE_H__*/ diff --git a/src/defines.h b/src/defines.h index 0fb35f2f..4dcead16 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 53 +#define VERSION_PATCH 54 //------------------------------------- typedef struct { @@ -94,7 +94,6 @@ enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; #define MQTT_MAX_PACKET_SIZE 384 -#define PLUGIN_DISPLAY typedef struct { uint32_t rxFail; diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h index eb770bb0..4e73d53a 100644 --- a/src/hm/CommQueue.h +++ b/src/hm/CommQueue.h @@ -11,9 +11,10 @@ #include "hmInverter.h" #include "../utils/dbg.h" -#define DEFAULT_ATTEMPS 10 -#define MORE_ATTEMPS_ALARMDATA 15 -#define MORE_ATTEMPS_GRIDONPROFILEPARA 15 +// needs a '+1' because the comparison does not send if attempts is equal 0 +#define DEFAULT_ATTEMPS 5 + 1 +#define MORE_ATTEMPS_ALARMDATA 15 + 1 +#define MORE_ATTEMPS_GRIDONPROFILEPARA 15 + 1 template class CommQueue { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index cf0d0054..a8a46e1a 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -152,9 +152,9 @@ class Communication : public CommQueue<> { while(!q->iv->radio->mBufCtrl.empty()) { packet_t *p = &q->iv->radio->mBufCtrl.front(); - printRxInfo(q, p); if(validateIvSerial(&p->packet[1], q->iv)) { + printRxInfo(q, p); q->iv->radioStatistics.frmCnt++; q->iv->mDtuRxCnt++; @@ -302,11 +302,11 @@ class Communication : public CommQueue<> { CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8); for(uint8_t i = 0; i < 4; i++) { if(tmp[i] != buf[i]) { - DPRINT(DBG_WARN, F("Inverter serial does not match, got: 0x")); + /*DPRINT(DBG_WARN, F("Inverter serial does not match, got: 0x")); DHEX(buf[0]);DHEX(buf[1]);DHEX(buf[2]);DHEX(buf[3]); DBGPRINT(F(", expected: 0x")); DHEX(tmp[0]);DHEX(tmp[1]);DHEX(tmp[2]);DHEX(tmp[3]); - DBGPRINTLN(""); + DBGPRINTLN("");*/ return false; } } diff --git a/src/hm/simulator.h b/src/hm/simulator.h new file mode 100644 index 00000000..4e06062e --- /dev/null +++ b/src/hm/simulator.h @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __SIMULATOR_H__ +#define __SIMULATOR_H__ + +#if defined(ENABLE_SIMULATOR) + +#include "../defines.h" +#include "../utils/dbg.h" +#include "../utils/helper.h" +#include "hmSystem.h" +#include "hmInverter.h" +#include "Communication.h" + +template +class Simulator { + public: + void setup(HMSYSTEM *sys, uint32_t *ts, uint8_t ivId = 0) { + mTimestamp = ts; + mSys = sys; + mIvId = ivId; + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void tick() { + uint8_t cmd, len; + uint8_t *payload; + getPayload(&cmd, &payload, &len); + + Inverter<> *iv = mSys->getInverterByPos(mIvId); + if (NULL == iv) + return; + + DPRINT(DBG_INFO, F("add payload with cmd: 0x")); + DBGHEXLN(cmd); + + if(GridOnProFilePara == cmd) { + iv->addGridProfile(payload, len); + return; + } + + record_t<> *rec = iv->getRecordStruct(cmd); + rec->ts = *mTimestamp; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + + if((nullptr != mCbPayload) && (GridOnProFilePara != cmd)) + (mCbPayload)(cmd, iv); + } + + private: + inline void getPayload(uint8_t *cmd, uint8_t *payload[], uint8_t *len) { + switch(payloadCtrl) { + default: *cmd = RealTimeRunData_Debug; break; + case 1: *cmd = SystemConfigPara; break; + case 3: *cmd = InverterDevInform_All; break; + case 5: *cmd = InverterDevInform_Simple; break; + case 7: *cmd = GridOnProFilePara; break; + } + + if(payloadCtrl < 8) + payloadCtrl++; + + switch(*cmd) { + default: + case RealTimeRunData_Debug: + *payload = plRealtime; + modifyAcPwr(); + *len = 62; + break; + case InverterDevInform_All: + *payload = plFirmware; + *len = 14; + break; + case InverterDevInform_Simple: + *payload = plPart; + *len = 14; + break; + case SystemConfigPara: + *payload = plLimit; + *len = 14; + break; + case AlarmData: + *payload = plAlarm; + *len = 26; + break; + case GridOnProFilePara: + *payload = plGrid; + *len = 70; + break; + } + } + + inline void modifyAcPwr() { + uint16_t cur = (plRealtime[50] << 8) | plRealtime[51]; + uint16_t change = cur ^ 0xa332; + if(0 == change) + change = 140; + else if(change > 200) + change = (change % 200) + 1; + + if(cur > 7000) + cur -= change; + else + cur += change; + + plRealtime[50] = (cur >> 8) & 0xff; + plRealtime[51] = (cur ) & 0xff; + } + + private: + HMSYSTEM *mSys; + uint8_t mIvId; + uint32_t *mTimestamp; + payloadListenerType mCbPayload = nullptr; + uint8_t payloadCtrl = 0; + + private: + uint8_t plRealtime[62] = { + 0x00, 0x01, 0x01, 0x24, 0x00, 0x22, 0x00, 0x23, + 0x00, 0x63, 0x00, 0x65, 0x00, 0x08, 0x5c, 0xbb, + 0x00, 0x09, 0x6f, 0x08, 0x00, 0x0c, 0x00, 0x0c, + 0x01, 0x1e, 0x00, 0x22, 0x00, 0x21, 0x00, 0x60, + 0x00, 0x5f, 0x00, 0x08, 0xdd, 0x84, 0x00, 0x09, + 0x13, 0x6f, 0x00, 0x0b, 0x00, 0x0b, 0x09, 0x27, + 0x13, 0x8c, 0x01, 0x75, 0x00, 0xc2, 0x00, 0x10, + 0x03, 0x77, 0x00, 0x61, 0x00, 0x02 + }; + + uint8_t plPart[14] = { + 0x27, 0x1c, 0x10, 0x12, 0x10, 0x01, 0x01, 0x00, + 0x0a, 0x00, 0x20, 0x01, 0x00, 0x00 + }; + + uint8_t plFirmware[14] = { + 0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x60, 0x42, + 0x60, 0x42, 0x00, 0x00, 0x00, 0x00 + }; + + uint8_t plLimit[14] = { + 0x00, 0x01, 0x03, 0xe8, 0x00, 0x00, 0x03, 0xe8, + 0xff, 0xff, 0xff, 0xff, 0x01, 0x68 + }; + + uint8_t plGrid[70] = { + 0x0D, 0x00, 0x20, 0x00, 0x00, 0x08, 0x08, 0xFC, + 0x07, 0x30, 0x00, 0x01, 0x0A, 0x55, 0x00, 0x01, + 0x09, 0xE2, 0x10, 0x00, 0x13, 0x88, 0x12, 0x8E, + 0x00, 0x01, 0x14, 0x1E, 0x00, 0x01, 0x20, 0x00, + 0x00, 0x01, 0x30, 0x07, 0x01, 0x2C, 0x0A, 0x55, + 0x07, 0x30, 0x14, 0x1E, 0x12, 0x8E, 0x00, 0x32, + 0x00, 0x1E, 0x40, 0x00, 0x07, 0xD0, 0x00, 0x10, + 0x50, 0x00, 0x00, 0x01, 0x13, 0x9C, 0x01, 0x90, + 0x00, 0x10, 0x70, 0x00, 0x00, 0x01 + }; + + uint8_t plAlarm[26] = { + 0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x51, 0xc7, + 0x51, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x80, 0x02, + 0x00, 0x02, 0xa6, 0xc9, 0xa6, 0xc9, 0x65, 0x3e, + 0x47, 0x21 + }; +}; + +#endif /*ENABLE_SIMULATOR*/ +#endif /*__SIMULATOR_H__*/ diff --git a/src/platformio.ini b/src/platformio.ini index 89040215..719ae300 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -46,6 +46,9 @@ board = esp12e board_build.f_cpu = 80000000L build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY ;-Wl,-Map,output.map monitor_filters = esp8266_exception_decoder @@ -57,6 +60,9 @@ board_build.f_cpu = 80000000L build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 -DLANG_DE + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY ;-Wl,-Map,output.map monitor_filters = esp8266_exception_decoder @@ -66,8 +72,11 @@ platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L build_flags = ${env.build_flags} - -DENABLE_PROMETHEUS_EP -DEMC_MIN_FREE_MEMORY=4096 + -DENABLE_PROMETHEUS_EP + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder @@ -76,9 +85,12 @@ platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L build_flags = ${env.build_flags} - -DENABLE_PROMETHEUS_EP -DEMC_MIN_FREE_MEMORY=4096 + -DENABLE_PROMETHEUS_EP -DLANG_DE + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder @@ -89,6 +101,9 @@ board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder @@ -100,12 +115,26 @@ board_build.f_cpu = 80000000L build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 -DLANG_DE + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder [env:esp32-wroom32] platform = espressif32@6.5.0 board = lolin_d32 +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY +monitor_filters = + esp32_exception_decoder + +[env:esp32-wroom32-minimal] +platform = espressif32@6.5.0 +board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD monitor_filters = @@ -116,6 +145,9 @@ platform = espressif32@6.5.0 board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DLANG_DE monitor_filters = esp32_exception_decoder @@ -126,6 +158,9 @@ board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD -DENABLE_PROMETHEUS_EP + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp32_exception_decoder @@ -134,8 +169,11 @@ platform = espressif32@6.5.0 board = lolin_d32 build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD - -DENABLE_PROMETHEUS_EP -DLANG_DE + -DENABLE_PROMETHEUS_EP + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp32_exception_decoder @@ -156,8 +194,9 @@ build_flags = ${env.build_flags} -D ETHERNET -DRELEASE -DUSE_HSPI_FOR_EPD - -DLOG_LOCAL_LEVEL=ESP_LOG_INFO - -DDEBUG_LEVEL=DBG_INFO + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp32_exception_decoder @@ -179,8 +218,9 @@ build_flags = ${env.build_flags} -DRELEASE -DUSE_HSPI_FOR_EPD -DLANG_DE - -DLOG_LOCAL_LEVEL=ESP_LOG_INFO - -DDEBUG_LEVEL=DBG_INFO + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY monitor_filters = esp32_exception_decoder @@ -189,6 +229,9 @@ platform = espressif32@6.5.0 board = lolin_s2_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_NRF_CS_PIN=12 -DDEF_NRF_CE_PIN=3 -DDEF_NRF_IRQ_PIN=5 @@ -208,6 +251,9 @@ platform = espressif32@6.5.0 board = lolin_s2_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_NRF_CS_PIN=12 -DDEF_NRF_CE_PIN=3 -DDEF_NRF_IRQ_PIN=5 @@ -228,6 +274,9 @@ platform = espressif32@6.5.0 board = lolin_c3_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_NRF_CS_PIN=5 -DDEF_NRF_CE_PIN=0 -DDEF_NRF_IRQ_PIN=1 @@ -247,6 +296,9 @@ platform = espressif32@6.5.0 board = lolin_c3_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_NRF_CS_PIN=5 -DDEF_NRF_CE_PIN=0 -DDEF_NRF_IRQ_PIN=1 @@ -267,6 +319,9 @@ platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin build_flags = ${env.build_flags} + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_NRF_CS_PIN=37 -DDEF_NRF_CE_PIN=38 -DDEF_NRF_IRQ_PIN=47 @@ -282,6 +337,7 @@ build_flags = ${env.build_flags} -DDEF_LED1=17 -DLED_ACTIVE_HIGH -DARDUINO_USB_MODE=1 + #-DARDUINO_USB_CDC_ON_BOOT=1 monitor_filters = esp32_exception_decoder, colorize @@ -289,6 +345,33 @@ monitor_filters = platform = espressif32@6.5.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin +build_flags = ${env.build_flags} + -DLANG_DE + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY + -DDEF_NRF_CS_PIN=37 + -DDEF_NRF_CE_PIN=38 + -DDEF_NRF_IRQ_PIN=47 + -DDEF_NRF_MISO_PIN=48 + -DDEF_NRF_MOSI_PIN=35 + -DDEF_NRF_SCLK_PIN=36 + -DDEF_CMT_CSB=4 + -DDEF_CMT_FCSB=21 + -DDEF_CMT_IRQ=8 + -DDEF_CMT_SDIO=5 + -DDEF_CMT_SCLK=6 + -DDEF_LED0=18 + -DDEF_LED1=17 + -DLED_ACTIVE_HIGH + -DARDUINO_USB_MODE=1 +monitor_filters = + esp32_exception_decoder, colorize + +[env:opendtufusion-minimal] +platform = espressif32@6.5.0 +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin build_flags = ${env.build_flags} -DDEF_NRF_CS_PIN=37 -DDEF_NRF_CE_PIN=38 @@ -305,7 +388,6 @@ build_flags = ${env.build_flags} -DDEF_LED1=17 -DLED_ACTIVE_HIGH -DARDUINO_USB_MODE=1 - -DLANG_DE monitor_filters = esp32_exception_decoder, colorize @@ -326,6 +408,9 @@ upload_protocol = esp-builtin build_flags = ${env.build_flags} -DETHERNET -DSPI_HAL + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_ETH_CS_PIN=42 -DDEF_ETH_SCK_PIN=39 -DDEF_ETH_MISO_PIN=41 @@ -368,6 +453,10 @@ upload_protocol = esp-builtin build_flags = ${env.build_flags} -DETHERNET -DSPI_HAL + -DLANG_DE + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY -DDEF_ETH_CS_PIN=42 -DDEF_ETH_SCK_PIN=39 -DDEF_ETH_MISO_PIN=41 @@ -390,6 +479,5 @@ build_flags = ${env.build_flags} -DLED_ACTIVE_HIGH -DARDUINO_USB_MODE=1 #-DARDUINO_USB_CDC_ON_BOOT=1 - -DLANG_DE monitor_filters = esp32_exception_decoder, colorize diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 0ce49c33..7780afef 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -1,6 +1,8 @@ #ifndef __DISPLAY__ #define __DISPLAY__ +#if defined(PLUGIN_DISPLAY) + #include #include @@ -238,4 +240,6 @@ class Display { DisplayMono *mMono; }; +#endif /*PLUGIN_DISPLAY*/ + #endif /*__DISPLAY__*/ diff --git a/src/plugins/history.h b/src/plugins/history.h index c4d3ab35..da57800f 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -6,6 +6,8 @@ #ifndef __HISTORY_DATA_H__ #define __HISTORY_DATA_H__ +#if defined(ENABLE_HISTORY) + #include #include "../appInterface.h" #include "../hm/hmSystem.h" @@ -122,4 +124,5 @@ class HistoryData { uint16_t mMaximumDay = 0; }; -#endif +#endif /*ENABLE_HISTORY*/ +#endif /*__HISTORY_DATA_H__*/ diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index e36338c9..48300b30 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -8,6 +8,7 @@ #ifndef __PUB_MQTT_H__ #define __PUB_MQTT_H__ +#if defined(ENABLE_MQTT) #ifdef ESP8266 #include #elif defined(ESP32) @@ -623,4 +624,5 @@ class PubMqtt { discovery_t mDiscovery; }; +#endif /*ENABLE_MQTT*/ #endif /*__PUB_MQTT_H__*/ diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8148eb20..afaa3455 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -202,8 +202,6 @@ class RestApi { ep[F("setup")] = url + F("setup"); ep[F("system")] = url + F("system"); ep[F("live")] = url + F("live"); - ep[F("powerHistory")] = url + F("powerHistory"); - ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); } @@ -791,6 +789,7 @@ class RestApi { void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) obj[F("refresh")] = mConfig->inst.sendInterval; uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { @@ -801,10 +800,12 @@ class RestApi { } obj[F("max")] = max; obj[F("maxDay")] = mApp->getHistoryMaxDay(); + #endif /*ENABLE_HISTORY*/ } void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) obj[F("refresh")] = 86400; // 1 day uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { @@ -814,6 +815,7 @@ class RestApi { max = value; } obj[F("max")] = max; + #endif /*ENABLE_HISTORY*/ } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index e75faf5b..961e8805 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -141,11 +141,11 @@ {#INVERTER}
    -
    {#INTERVAL}
    +
    {#INTERVAL} [s]
    -
    {#INV_GAP}
    +
    {#INV_GAP} [ms]
    diff --git a/src/web/lang.json b/src/web/lang.json index 6717306d..c495ac9f 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -265,8 +265,8 @@ }, { "token": "INTERVAL", - "en": "Interval [s]", - "de": "Intervall [s]" + "en": "Interval", + "de": "Intervall" }, { "token": "INV_GAP", From f5158d47254a093e9ba100f1bc22962e83febb92 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 14 Jan 2024 10:26:16 +0100 Subject: [PATCH 074/115] Enhancement: Add info about compiled modules to version string --- src/app.cpp | 23 ++++++++++++++++++++++- src/app.h | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 8cbe775d..80f2083e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -465,7 +465,28 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { //----------------------------------------------------------------------------- void app::resetSystem(void) { - snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + snprintf(mVersion, sizeof(mVersion), "%d.%d.%d%s", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, + "-" +#ifdef ENABLE_PROMETHEUS_EP + "P" +#endif + +#ifdef ENABLE_MQTT + "M" +#endif + +#ifdef PLUGIN_DISPLAY + "D" +#endif + +#ifdef ENABLE_HISTORY + "H" +#endif + +#ifdef AP_ONLY + "A" +#endif + ); #ifdef AP_ONLY mTimestamp = 1; diff --git a/src/app.h b/src/app.h index 194f291e..8da8f3f7 100644 --- a/src/app.h +++ b/src/app.h @@ -386,7 +386,7 @@ class app : public IApp, public ah::Scheduler { CmtRadio<> mCmtRadio; #endif - char mVersion[12]; + char mVersion[18]; settings mSettings; settings_t *mConfig; bool mSavePending; From 7c1ddf875f6dc08b60223e20d268a28c64ff804b Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 14 Jan 2024 13:47:22 +0100 Subject: [PATCH 075/115] add language, syslog and simulator identifier to version string --- src/app.cpp | 15 +++++++++++++++ src/app.h | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index 80f2083e..ddbc2771 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -486,6 +486,21 @@ void app::resetSystem(void) { #ifdef AP_ONLY "A" #endif + +#ifdef ENABLE_SYSLOG + "Y" +#endif + +#ifdef ENABLE_SIMULATOR + "S" +#endif + + "-" +#ifdef LANG_DE + "de" +#else + "en" +#endif ); #ifdef AP_ONLY diff --git a/src/app.h b/src/app.h index 8da8f3f7..0015ede4 100644 --- a/src/app.h +++ b/src/app.h @@ -386,7 +386,7 @@ class app : public IApp, public ah::Scheduler { CmtRadio<> mCmtRadio; #endif - char mVersion[18]; + char mVersion[23]; settings mSettings; settings_t *mConfig; bool mSavePending; From 28418998fc3abe36558a4a5dc912aac775cbfaae Mon Sep 17 00:00:00 2001 From: you69man Date: Sun, 14 Jan 2024 15:26:27 +0100 Subject: [PATCH 076/115] fix reboot problem with deactivated power graph --- src/plugins/Display/Display_Mono.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index a0d46d11..e480baf0 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -18,6 +18,7 @@ #endif #include "../../utils/helper.h" #include "Display_data.h" +#include "config/settings.h" #include "../../utils/dbg.h" #include "../../utils/timemonitor.h" @@ -184,7 +185,7 @@ class DisplayMono { } void addPowerGraphEntry(float val) { - if (mDisplayData->utcTs > 0) { // precondition: utc time available + if ((mPgData != nullptr) && (mDisplayData->utcTs > 0)) { // precondition: power graph initialized and utc time available calcPowerGraphValues(); //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); @@ -205,12 +206,15 @@ class DisplayMono { } void plotPowerGraph(uint8_t xoff, uint8_t yoff) { + if (mPgData == nullptr) // power graph not initialized + return; + // draw axes mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis // do not draw as long as time is not set correctly and no data was received - if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs < 1) || (mPgMaxPwr < 1) || (mPgLastPos < 1)) + if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs == 0) || (mPgMaxPwr < 1) || (mPgLastPos == 0)) return; // draw X scale From 0fb98518ead7d664e352bf6c90a4db86489de5ff Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 16:30:01 +0100 Subject: [PATCH 077/115] 0.8.55 * merge PR: fix reboot problem with deactivated power graph #1360 --- src/CHANGES.md | 3 +++ src/defines.h | 2 +- src/plugins/Display/Display_Mono.h | 7 +++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 6b3d0f80..e65fca23 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.55 - 2024-01-14 +* merge PR: fix reboot problem with deactivated power graph #1360 + ## 0.8.54 - 2024-01-13 * added minimal version (without: MqTT, Display, History), WebUI is not changed! * added simulator (must be activated before compile, standard: off) diff --git a/src/defines.h b/src/defines.h index 4dcead16..975abb58 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 54 +#define VERSION_PATCH 55 //------------------------------------- typedef struct { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index e480baf0..1e34be87 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -18,7 +18,6 @@ #endif #include "../../utils/helper.h" #include "Display_data.h" -#include "config/settings.h" #include "../../utils/dbg.h" #include "../../utils/timemonitor.h" @@ -185,7 +184,7 @@ class DisplayMono { } void addPowerGraphEntry(float val) { - if ((mPgData != nullptr) && (mDisplayData->utcTs > 0)) { // precondition: power graph initialized and utc time available + if ((nullptr != mPgData) && (mDisplayData->utcTs > 0)) { // precondition: power graph initialized and utc time available calcPowerGraphValues(); //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); @@ -206,7 +205,7 @@ class DisplayMono { } void plotPowerGraph(uint8_t xoff, uint8_t yoff) { - if (mPgData == nullptr) // power graph not initialized + if (nullptr == mPgData) // power graph not initialized return; // draw axes @@ -214,7 +213,7 @@ class DisplayMono { mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis // do not draw as long as time is not set correctly and no data was received - if ((mDisplayData->pGraphStartTime == 0) || (mDisplayData->pGraphEndTime == 0) || (mDisplayData->utcTs == 0) || (mPgMaxPwr < 1) || (mPgLastPos == 0)) + if ((0 == mDisplayData->pGraphStartTime) || (0 == mDisplayData->pGraphEndTime) || (0 == mDisplayData->utcTs) || (mPgMaxPwr < 1) || (0 == mPgLastPos)) return; // draw X scale From ba6c38ede0c546e0b33fcc6ffc527cd8734a865a Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 16:42:33 +0100 Subject: [PATCH 078/115] 0.8.55 * changed scope of variables and member functions inside display classes --- src/CHANGES.md | 1 + src/plugins/Display/Display_Mono.h | 184 +++++++++++++++-------------- 2 files changed, 95 insertions(+), 90 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index e65fca23..3b917c9f 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,7 @@ ## 0.8.55 - 2024-01-14 * merge PR: fix reboot problem with deactivated power graph #1360 +* changed scope of variables and member functions inside display classes ## 0.8.54 - 2024-01-13 * added minimal version (without: MqTT, Display, History), WebUI is not changed! diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 1e34be87..712059ea 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -69,33 +69,6 @@ class DisplayMono { }; protected: - display_t *mCfg; - U8G2 *mDisplay; - DisplayData *mDisplayData; - - float *mPgData = nullptr; - uint8_t mPgWidth = 0; - uint8_t mPgHeight = 0; - float mPgMaxPwr = 0.0; - uint32_t mPgPeriod = 0; // seconds - uint32_t mPgTimeOfDay = 0; - uint8_t mPgLastPos = 0; - - uint16_t mDispWidth; - uint16_t mDispHeight; - uint8_t mLuminance; - - uint8_t mLineXOffsets[5] = {}; - uint8_t mLineYOffsets[5] = {}; - - uint8_t mExtra; - int8_t mPixelshift=0; - TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); - TimeMonitor mDispSwitchTime = TimeMonitor(); - DispSwitchState mDispSwitchState = DispSwitchState::TEXT; - bool mDisplayActive = true; // always start with display on - char mFmtText[DISP_FMT_TEXT_LEN]; - // Common initialization function to be called by subclasses void monoInit(U8G2* display, DisplayData *displayData) { mDisplay = display; @@ -113,25 +86,10 @@ class DisplayMono { mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100 } - bool monoMaintainDispSwitchState(void) { - bool change = false; - switch(mDispSwitchState) { - case DispSwitchState::TEXT: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = DispSwitchState::GRAPH; - mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms - change = true; - } - break; - case DispSwitchState::GRAPH: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = DispSwitchState::TEXT; - mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); - change = true; - } - break; - } - return change; + // pixelshift screensaver with wipe effect + void calcPixelShift(int range) { + int8_t mod = (millis() / 10000) % ((range >> 1) << 2); + mPixelshift = mCfg->screenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } void initPowerGraph(uint8_t width, uint8_t height) { @@ -155,34 +113,6 @@ class DisplayMono { DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ } - void resetPowerGraph() { - if (mPgData != nullptr) { - mPgMaxPwr = 0.0; - mPgLastPos = 0; - for (uint8_t i = 0; i < mPgWidth; i++) { - mPgData[i] = 0.0; - } - } - } - - uint8_t sss2pgpos(uint seconds_since_start) { - uint32_t diff = (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime); - if(diff) - return (seconds_since_start * (mPgWidth - 1) / diff); - return 0; - } - - void calcPowerGraphValues() { - mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis - uint32_t oldTimeOfDay = mPgTimeOfDay; - mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time - if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data - resetPowerGraph(); - if(0 == mPgPeriod) - mPgPeriod = 1; - mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day - } - void addPowerGraphEntry(float val) { if ((nullptr != mPgData) && (mDisplayData->utcTs > 0)) { // precondition: power graph initialized and utc time available calcPowerGraphValues(); @@ -192,18 +122,6 @@ class DisplayMono { } } - uint8_t getPowerGraphXpos(uint8_t p) { - if ((p <= mPgLastPos) && (mPgLastPos > 0)) - return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - return 0; - } - - uint8_t getPowerGraphYpos(uint8_t p) { - if ((p < mPgWidth) && (mPgMaxPwr > 0)) - return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - return 0; - } - void plotPowerGraph(uint8_t xoff, uint8_t yoff) { if (nullptr == mPgData) // power graph not initialized return; @@ -254,11 +172,97 @@ class DisplayMono { mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); } - // pixelshift screensaver with wipe effect - void calcPixelShift(int range) { - int8_t mod = (millis() / 10000) % ((range >> 1) << 2); - mPixelshift = mCfg->screenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; + private: + bool monoMaintainDispSwitchState(void) { + bool change = false; + switch(mDispSwitchState) { + case DispSwitchState::TEXT: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::GRAPH; + mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms + change = true; + } + break; + case DispSwitchState::GRAPH: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::TEXT; + mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); + change = true; + } + break; + } + return change; + } + + void resetPowerGraph() { + if (mPgData != nullptr) { + mPgMaxPwr = 0.0; + mPgLastPos = 0; + for (uint8_t i = 0; i < mPgWidth; i++) { + mPgData[i] = 0.0; + } + } } + + uint8_t sss2pgpos(uint seconds_since_start) { + uint32_t diff = (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime); + if(diff) + return (seconds_since_start * (mPgWidth - 1) / diff); + return 0; + } + + void calcPowerGraphValues() { + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis + uint32_t oldTimeOfDay = mPgTimeOfDay; + mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time + if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data + resetPowerGraph(); + if(0 == mPgPeriod) + mPgPeriod = 1; + mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day + } + + uint8_t getPowerGraphXpos(uint8_t p) { + if ((p <= mPgLastPos) && (mPgLastPos > 0)) + return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis + return 0; + } + + uint8_t getPowerGraphYpos(uint8_t p) { + if ((p < mPgWidth) && (mPgMaxPwr > 0)) + return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height + return 0; + } + + protected: + display_t *mCfg; + U8G2 *mDisplay; + DisplayData *mDisplayData; + DispSwitchState mDispSwitchState = DispSwitchState::TEXT; + + uint16_t mDispWidth; + uint8_t mExtra; + int8_t mPixelshift=0; + char mFmtText[DISP_FMT_TEXT_LEN]; + uint8_t mLineXOffsets[5] = {}; + uint8_t mLineYOffsets[5] = {}; + + uint8_t mPgWidth = 0; + + private: + float *mPgData = nullptr; + uint8_t mPgHeight = 0; + float mPgMaxPwr = 0.0; + uint32_t mPgPeriod = 0; // seconds + uint32_t mPgTimeOfDay = 0; + uint8_t mPgLastPos = 0; + + uint16_t mDispHeight; + uint8_t mLuminance; + + TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); + TimeMonitor mDispSwitchTime = TimeMonitor(); + bool mDisplayActive = true; // always start with display on }; /* adapted 5x8 Font for low-res displays with symbols From 8500c9e37d820e97b0bc69e4d8a071f0687ea673 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 16:43:14 +0100 Subject: [PATCH 079/115] 0.8.55 * removed automatically "minimal" builds --- .github/workflows/compile_development.yml | 2 -- src/CHANGES.md | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 90a6293f..7eaf9b34 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -25,13 +25,11 @@ jobs: - esp8266-prometheus - esp8285 - esp32-wroom32 - - esp32-wroom32-minimal - esp32-wroom32-prometheus - esp32-wroom32-ethernet - esp32-s2-mini - esp32-c3-mini - opendtufusion - - opendtufusion-minimal - opendtufusion-ethernet steps: - uses: actions/checkout@v3 diff --git a/src/CHANGES.md b/src/CHANGES.md index 3b917c9f..13521857 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -3,6 +3,7 @@ ## 0.8.55 - 2024-01-14 * merge PR: fix reboot problem with deactivated power graph #1360 * changed scope of variables and member functions inside display classes +* removed automatically "minimal" builds ## 0.8.54 - 2024-01-13 * added minimal version (without: MqTT, Display, History), WebUI is not changed! From de81296b28a72f51992c68ca49905e7d528ee5ea Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 16:45:36 +0100 Subject: [PATCH 080/115] 0.8.55 * fix include of "settings.h" (was already done in #1360) --- src/CHANGES.md | 1 + src/plugins/Display/Display_Mono.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/CHANGES.md b/src/CHANGES.md index 13521857..62616096 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -4,6 +4,7 @@ * merge PR: fix reboot problem with deactivated power graph #1360 * changed scope of variables and member functions inside display classes * removed automatically "minimal" builds +* fix include of "settings.h" (was already done in #1360) ## 0.8.54 - 2024-01-13 * added minimal version (without: MqTT, Display, History), WebUI is not changed! diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 712059ea..c8d25f8b 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -20,6 +20,7 @@ #include "Display_data.h" #include "../../utils/dbg.h" #include "../../utils/timemonitor.h" +#include "../../config/settings.h" class DisplayMono { public: From 6c4e6f9d901bd846192dea50269cffc9f9bb251b Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 14 Jan 2024 17:38:37 +0100 Subject: [PATCH 081/115] 0.8.55 * merge PR: fix reboot problem with deactivated power graph #1360 * changed scope of variables and member functions inside display classes * removed automatically "minimal" builds * fix include of "settings.h" (was already done in #1360) * merge PR: Enhancement: Add info about compiled modules to version string #1357 * add info about installed binary to `/update` #1353 * fix lang in `/system` #1346 --- scripts/convertHtml.py | 30 ++++++++++++++++----- src/CHANGES.md | 3 +++ src/app.cpp | 58 ++++++++++++++++++++-------------------- src/app.h | 7 ++++- src/appInterface.h | 1 + src/web/RestApi.h | 5 +++- src/web/html/system.html | 2 +- src/web/html/update.html | 2 ++ src/web/lang.h | 12 +++++++++ src/web/lang.json | 7 ++++- 10 files changed, 87 insertions(+), 40 deletions(-) diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py index 4e01a875..c39e95ac 100644 --- a/scripts/convertHtml.py +++ b/scripts/convertHtml.py @@ -23,18 +23,33 @@ def readVersion(path): 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, lang): +def readVersionFull(path): + f = open(path, "r") + lines = f.readlines() + f.close() + + today = date.today() + search = ["_MAJOR", "_MINOR", "_PATCH"] + version = today.strftime("%y%m%d") + "_ahoy_" + 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() + "." + version = version[:-1] + "_" + get_git_sha() + return version + +def htmlParts(file, header, nav, footer, versionPath, lang): p = ""; f = open(file, "r") lines = f.readlines() @@ -59,8 +74,10 @@ def htmlParts(file, header, nav, footer, version, lang): p += line #placeholders + version = readVersion(versionPath); link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + '' p = p.replace("{#VERSION}", version) + p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath)) p = p.replace("{#VERSION_GIT}", link) # remove if - endif ESP32 @@ -120,7 +137,7 @@ def translate(file, data, lang="de"): return data -def convert2Header(inFile, version, lang): +def convert2Header(inFile, versionPath, lang): fileType = inFile.split(".")[1] define = inFile.split(".")[0].upper() define2 = inFile.split(".")[1].upper() @@ -140,7 +157,7 @@ def convert2Header(inFile, version, lang): f.close() else: if fileType == "html": - data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version, lang) + data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", versionPath, lang) else: f = open(inFile, "r") data = f.read() @@ -193,7 +210,6 @@ for files in types: 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") # get language from environment lang = "en" @@ -202,4 +218,4 @@ if env['PIOENV'][-3:] == "-de": # go throw the array for val in files_grabbed: - convert2Header(val, version, lang) + convert2Header(val, "../../defines.h", lang) diff --git a/src/CHANGES.md b/src/CHANGES.md index 62616096..db4618d5 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -5,6 +5,9 @@ * changed scope of variables and member functions inside display classes * removed automatically "minimal" builds * fix include of "settings.h" (was already done in #1360) +* merge PR: Enhancement: Add info about compiled modules to version string #1357 +* add info about installed binary to `/update` #1353 +* fix lang in `/system` #1346 ## 0.8.54 - 2024-01-13 * added minimal version (without: MqTT, Display, History), WebUI is not changed! diff --git a/src/app.cpp b/src/app.cpp index ddbc2771..dd84ec61 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -465,42 +465,42 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { //----------------------------------------------------------------------------- void app::resetSystem(void) { - snprintf(mVersion, sizeof(mVersion), "%d.%d.%d%s", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, - "-" -#ifdef ENABLE_PROMETHEUS_EP - "P" -#endif + snprintf(mVersion, sizeof(mVersion), "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + snprintf(mVersionModules, sizeof(mVersionModules), "%s", + #ifdef ENABLE_PROMETHEUS_EP + "P" + #endif -#ifdef ENABLE_MQTT - "M" -#endif + #ifdef ENABLE_MQTT + "M" + #endif -#ifdef PLUGIN_DISPLAY - "D" -#endif + #ifdef PLUGIN_DISPLAY + "D" + #endif -#ifdef ENABLE_HISTORY - "H" -#endif + #ifdef ENABLE_HISTORY + "H" + #endif -#ifdef AP_ONLY - "A" -#endif + #ifdef AP_ONLY + "A" + #endif -#ifdef ENABLE_SYSLOG - "Y" -#endif + #ifdef ENABLE_SYSLOG + "Y" + #endif -#ifdef ENABLE_SIMULATOR - "S" -#endif + #ifdef ENABLE_SIMULATOR + "S" + #endif - "-" -#ifdef LANG_DE - "de" -#else - "en" -#endif + "-" + #ifdef LANG_DE + "de" + #else + "en" + #endif ); #ifdef AP_ONLY diff --git a/src/app.h b/src/app.h index 0015ede4..5b39405c 100644 --- a/src/app.h +++ b/src/app.h @@ -188,6 +188,10 @@ class app : public IApp, public ah::Scheduler { return mVersion; } + const char *getVersionModules() { + return mVersionModules; + } + uint32_t getSunrise() { return mSunrise; } @@ -386,7 +390,8 @@ class app : public IApp, public ah::Scheduler { CmtRadio<> mCmtRadio; #endif - char mVersion[23]; + char mVersion[12]; + char mVersionModules[12]; settings mSettings; settings_t *mConfig; bool mSavePending; diff --git a/src/appInterface.h b/src/appInterface.h index 5feccd3c..ad38f756 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -27,6 +27,7 @@ class IApp { virtual bool getShouldReboot() = 0; virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; + virtual const char *getVersionModules() = 0; #if !defined(ETHERNET) virtual void scanAvailNetworks() = 0; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index afaa3455..edf92a97 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -252,6 +252,7 @@ class RestApi { obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_now")] = mApp->getTimestamp(); obj[F("version")] = String(mApp->getVersion()); + obj[F("modules")] = String(mApp->getVersionModules()); obj[F("build")] = String(AUTO_GIT_HASH); obj[F("env")] = String(ENV_NAME); obj[F("menu_prot")] = mApp->getProtection(request); @@ -325,7 +326,9 @@ class RestApi { void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { getSysInfo(request, obj.createNestedObject(F("system"))); getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("html")] = F("AhoyFactory Reset

    Reboot"); + char tmp[100]; + snprintf(tmp, 100, "%s

    %s", FACTORY_RESET, BTN_REBOOT); + obj[F("html")] = String(tmp); } void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) { diff --git a/src/web/html/system.html b/src/web/html/system.html index ab0b5289..a646e8b8 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -24,7 +24,7 @@ const data = ["sdk", "cpu_freq", "chip_revision", "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", - "max_free_blk", "version", "core_version", "reboot_reason"]; + "max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"]; lines = []; for (const [key, value] of Object.entries(obj)) { diff --git a/src/web/html/update.html b/src/web/html/update.html index f0e39fe7..3fed75a5 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -10,6 +10,7 @@
    {#SELECT_FILE} (*.bin) +

    {#INSTALLED_VERSION}:

    @@ -26,6 +27,7 @@ parseNav(obj); parseESP(obj); parseRssi(obj); + document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin" } function hide() { diff --git a/src/web/lang.h b/src/web/lang.h index a82c7888..3c3cd631 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -72,4 +72,16 @@ #define INV_NOT_FOUND "inverter not found!" #endif +#ifdef LANG_DE + #define FACTORY_RESET "Ahoy Factory Reset" +#else /*LANG_EN*/ + #define FACTORY_RESET "Ahoy auf Werkseinstellungen zurücksetzen" +#endif + +#ifdef LANG_DE + #define BTN_REBOOT "Reboot" +#else /*LANG_EN*/ + #define BTN_REBOOT "Ahoy neustarten" +#endif + #endif /*__LANG_H__*/ diff --git a/src/web/lang.json b/src/web/lang.json index c495ac9f..c910650e 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -51,7 +51,7 @@ { "token": "WELCOME", "en": "Welcome to AhoyDTU", - "de": "Willkommen zu AhoyDTU" + "de": "Willkommen bei AhoyDTU" }, { "token": "NETWORK_SETUP", @@ -1043,6 +1043,11 @@ "en": "Download latest Release and Development versions (without login)", "de": "Lade die letzte Releaseversion oder Entwicklerversion herunter (ohne Login)" }, + { + "token": "INSTALLED_VERSION", + "en": "installed version (original filename)", + "de": "aktuell installierte Version" + }, { "token": "UPDATE_STARTED", "en": "update started", From f4a82242dfa77933a369c7c363be0604703a600f Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 15 Jan 2024 07:53:41 +0100 Subject: [PATCH 082/115] 0.8.56 * potential fix of update problems and random reboots #1359 #1354 --- src/CHANGES.md | 3 +++ src/app.cpp | 1 + src/defines.h | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index db4618d5..d566216a 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.56 - 2024-01-15 +* potential fix of update problems and random reboots #1359 #1354 + ## 0.8.55 - 2024-01-14 * merge PR: fix reboot problem with deactivated power graph #1360 * changed scope of variables and member functions inside display classes diff --git a/src/app.cpp b/src/app.cpp index dd84ec61..6350e990 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -132,6 +132,7 @@ void app::loop(void) { if (mMqttEnabled && mNetworkConnected) mMqtt.loop(); #endif + yield(); } //----------------------------------------------------------------------------- diff --git a/src/defines.h b/src/defines.h index 975abb58..46c9a249 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 55 +#define VERSION_PATCH 56 //------------------------------------- typedef struct { From e44e72231530a818f1c949ca3b90fa7c581ebfeb Mon Sep 17 00:00:00 2001 From: rejoe2 Date: Mon, 15 Jan 2024 13:09:23 +0100 Subject: [PATCH 083/115] MI-MQTT and last retransmit - fix MI not sending data over MQTT (discord https://discord.com/channels/984173303147155506/1144045244166443159/1196092306873397351) - change logic for last retransmit (discord https://discord.com/channels/984173303147155506/1144045244166443159/1196098984167608360) --- src/hm/CommQueue.h | 6 +++--- src/hm/Communication.h | 31 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h index 4e73d53a..d62efe07 100644 --- a/src/hm/CommQueue.h +++ b/src/hm/CommQueue.h @@ -12,9 +12,9 @@ #include "../utils/dbg.h" // needs a '+1' because the comparison does not send if attempts is equal 0 -#define DEFAULT_ATTEMPS 5 + 1 -#define MORE_ATTEMPS_ALARMDATA 15 + 1 -#define MORE_ATTEMPS_GRIDONPROFILEPARA 15 + 1 +#define DEFAULT_ATTEMPS 5 +#define MORE_ATTEMPS_ALARMDATA 15 +#define MORE_ATTEMPS_GRIDONPROFILEPARA 15 template class CommQueue { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index a8a46e1a..5f14a5af 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -178,11 +178,11 @@ class Communication : public CommQueue<> { yield(); } - if(0 == q->attempts) { + /*if(0 == q->attempts) { DPRINT_IVID(DBG_INFO, q->iv->id); DBGPRINT(F("no attempts left")); closeRequest(q, false); - } else { + } else {*/ if(q->iv->ivGen != IV_MI) { mState = States::CHECK_PACKAGE; } else { @@ -207,7 +207,7 @@ class Communication : public CommQueue<> { closeRequest(q, true); } } - } + //} } break; @@ -235,6 +235,12 @@ class Communication : public CommQueue<> { } if(framnr) { + if(0 == q->attempts) { + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINT(F("no attempts left")); + closeRequest(q, false); + return; + } setAttempt(); if(*mSerialDebug) { @@ -516,15 +522,15 @@ class Communication : public CommQueue<> { } void sendRetransmit(const queue_s *q, uint8_t i) { - if(q->attempts) { + //if(q->attempts) { q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true); q->iv->radioStatistics.retransmits++; mWaitTime.startTimeMonitor(SINGLEFR_TIMEOUT); // timeout mState = States::WAIT; - } else { + /*} else { //add(q, true); closeRequest(q, false); - } + }*/ } private: @@ -604,6 +610,8 @@ class Communication : public CommQueue<> { rec->ts = q->ts; q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1); q->iv->miMultiParts +=4; + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; + } else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10 DPRINT_IVID(DBG_INFO, q->iv->id); if ( p->packet[9] == 0x01 ) { @@ -620,6 +628,7 @@ class Communication : public CommQueue<> { record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure rec->ts = q->ts; q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1); + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; if(*mSerialDebug) { DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); @@ -667,6 +676,7 @@ class Communication : public CommQueue<> { inline void miGPFDecode(packet_t *p, const queue_s *q) { record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure rec->ts = q->ts; + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION @@ -849,6 +859,8 @@ class Communication : public CommQueue<> { if (!stsok) { q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0); + rec->ts = q->ts; + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; } if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) { @@ -907,17 +919,12 @@ class Communication : public CommQueue<> { iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); iv->doCalculations(); + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; // update status state-machine, if (ac_pow) iv->isProducing(); - //closeRequest(iv, iv->miMultiParts > 5); - - //mHeu.setGotAll(iv); - //cmdDone(false); if(NULL != mCbPayload) (mCbPayload)(RealTimeRunData_Debug, iv); - - //mState = States::RESET; // everything ok, next request } private: From ac2f772b74ed8c9fa4439ba01acde517eb5ee748 Mon Sep 17 00:00:00 2001 From: you69man Date: Sun, 14 Jan 2024 17:16:32 +0100 Subject: [PATCH 084/115] fix immediate clearing of display after sunset --- src/plugins/Display/Display_Mono.h | 121 ++++++++++++++-------- src/plugins/Display/Display_Mono_128X64.h | 3 +- 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index c8d25f8b..e0a03c61 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -81,7 +81,7 @@ class DisplayMono { mDispWidth = mDisplay->getDisplayWidth(); mDispHeight = mDisplay->getDisplayHeight(); mDispSwitchTime.stopTimeMonitor(); - if (mCfg->graph_ratio == 100) // if graph ratio is 100% start in graph mode + if (100 == mCfg->graph_ratio) // if graph ratio is 100% start in graph mode mDispSwitchState = DispSwitchState::GRAPH; else if (mCfg->graph_ratio != 0) mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100 @@ -90,39 +90,70 @@ class DisplayMono { // pixelshift screensaver with wipe effect void calcPixelShift(int range) { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); - mPixelshift = mCfg->screenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; + mPixelshift = (1 == mCfg->screenSaver) ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } + protected: + enum class PowerGraphState { + NO_TIME_SYNC, + IN_PERIOD, + WAIT_4_NEW_PERIOD, + WAIT_4_RESTART + }; + + // initialize power graph and allocate data buffer based on pixel width void initPowerGraph(uint8_t width, uint8_t height) { DBGPRINTLN(F("---- Init Power Graph ----")); mPgWidth = width; mPgHeight = height; mPgData = new float[mPgWidth]; + mPgState = PowerGraphState::NO_TIME_SYNC; resetPowerGraph(); -/* - Inverter<> *iv; - mPgMaxAvailPower = 0; - uint8_t nInv = mSys->getNumInverters(); - for (uint8_t i = 0; i < nInv; i++) { - iv = mSys->getInverterByPos(i); - if (iv == NULL) - continue; - for (uint8_t ch = 0; ch < 6; ch++) { - mPgMaxAvailPower += iv->config->chMaxPwr[ch]; - } - } - DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ } + // add new value to power graph and maintain state engine for period times void addPowerGraphEntry(float val) { - if ((nullptr != mPgData) && (mDisplayData->utcTs > 0)) { // precondition: power graph initialized and utc time available - calcPowerGraphValues(); - //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 - mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); - mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis + if (nullptr == mPgData) // power graph not initialized + return; + + bool store_entry = false; + switch(mPgState) { + case PowerGraphState::NO_TIME_SYNC: + if ((mDisplayData->pGraphStartTime > 0) && (mDisplayData->pGraphEndTime > 0) && // wait until period data is available ... + (mDisplayData->utcTs >= mDisplayData->pGraphStartTime) && (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) { // and current time is in period + storeStartEndTimes(); // period was received -> store + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + } + break; + case PowerGraphState::IN_PERIOD: + if (mDisplayData->utcTs > mPgEndTime) // check if end of day is reached ... + mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting + else + store_entry = true; + break; + case PowerGraphState::WAIT_4_NEW_PERIOD: + if ((mPgStartTime != mDisplayData->pGraphStartTime) || (mPgEndTime != mDisplayData->pGraphEndTime)) { // wait until new time period was received ... + storeStartEndTimes(); // and store it for next period + mPgState = PowerGraphState::WAIT_4_RESTART; + } + break; + case PowerGraphState::WAIT_4_RESTART: + if ((mDisplayData->utcTs >= mPgStartTime) && (mDisplayData->utcTs < mPgEndTime)) { // wait until current time is in period again ... + resetPowerGraph(); // then reset power graph data + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + } + break; + } + if (store_entry) { + mPgLastPos = std::min((uint8_t) sss2PgPos(mDisplayData->utcTs - mPgStartTime), (uint8_t) (mPgWidth - 1)); // current datapoint based on seconds since start + mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values + mPgMaxPwr = std::max(mPgMaxPwr, val); // update max value of stored data for scaling of y-axis } } + // plot power graph to given display offset void plotPowerGraph(uint8_t xoff, uint8_t yoff) { if (nullptr == mPgData) // power graph not initialized return; @@ -146,7 +177,7 @@ class DisplayMono { tm.Minute = 0; tm.Second = 0; for (; tm.Hour <= endHour; tm.Hour++) { - uint8_t x_pos_screen = getPowerGraphXpos(sss2pgpos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis + uint8_t x_pos_screen = getPowerGraphXpos(sss2PgPos((uint32_t) makeTime(tm) - mDisplayData->pGraphStartTime)); // scale horizontal axis mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); } @@ -163,8 +194,8 @@ class DisplayMono { // draw curve for (uint8_t i = 1; i <= mPgLastPos; i++) { - mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), - xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphValueYpos(i - 1), + xoff + getPowerGraphXpos(i), yoff - getPowerGraphValueYpos(i)); } // print max power value @@ -195,6 +226,7 @@ class DisplayMono { return change; } + // reset power graph void resetPowerGraph() { if (mPgData != nullptr) { mPgMaxPwr = 0.0; @@ -205,34 +237,35 @@ class DisplayMono { } } - uint8_t sss2pgpos(uint seconds_since_start) { - uint32_t diff = (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime); - if(diff) - return (seconds_since_start * (mPgWidth - 1) / diff); - return 0; + // store start and end times of current time period and calculate period length + void storeStartEndTimes() { + mPgStartTime = mDisplayData->pGraphStartTime; + mPgEndTime = mDisplayData->pGraphEndTime; + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis } - void calcPowerGraphValues() { - mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis - uint32_t oldTimeOfDay = mPgTimeOfDay; - mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time - if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data - resetPowerGraph(); - if(0 == mPgPeriod) - mPgPeriod = 1; - mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day + // get power graph datapoint index, scaled to current time period, by seconds since start + uint8_t sss2PgPos(uint seconds_since_start) { + if(mPgPeriod) + return (seconds_since_start * (mPgWidth - 1) / mPgPeriod); + else + return 0; } - uint8_t getPowerGraphXpos(uint8_t p) { + // get X-position of power graph, scaled to lastpos, by according data point index + uint8_t getPowerGraphXpos(uint8_t p) { if ((p <= mPgLastPos) && (mPgLastPos > 0)) return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - return 0; + else + return 0; } - uint8_t getPowerGraphYpos(uint8_t p) { + // get Y-position of power graph, scaled to maximum value, by according datapoint index + uint8_t getPowerGraphValueYpos(uint8_t p) { if ((p < mPgWidth) && (mPgMaxPwr > 0)) return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - return 0; + else + return 0; } protected: @@ -254,9 +287,11 @@ class DisplayMono { float *mPgData = nullptr; uint8_t mPgHeight = 0; float mPgMaxPwr = 0.0; - uint32_t mPgPeriod = 0; // seconds - uint32_t mPgTimeOfDay = 0; + uint32_t mPgStartTime = 0; + uint32_t mPgEndTime = 0; + uint32_t mPgPeriod = 0; // seconds uint8_t mPgLastPos = 0; + PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC; uint16_t mDispHeight; uint8_t mLuminance; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 34f35834..b4aa08cc 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -89,9 +89,8 @@ class DisplayMono128X64 : public DisplayMono { calcPixelShift(pixelShiftRange); // add new power data to power graph - if (mDisplayData->nrProducing > 0) { + if (mDisplayData->nrProducing > 0) addPowerGraphEntry(mDisplayData->totalPower); - } // print Date and time if (0 != mDisplayData->utcTs) From 9d29276c06de2296bcc4b2d2f46770697ea8e62d Mon Sep 17 00:00:00 2001 From: rejoe2 Date: Mon, 15 Jan 2024 16:12:51 +0100 Subject: [PATCH 085/115] Add "second try" - serial info deactivated - first tx is counted as retransmit --- src/hm/Communication.h | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 5f14a5af..9e383b6a 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -91,6 +91,7 @@ class Communication : public CommQueue<> { mIsRetransmit = false; if(NULL == q->iv->radio) cmdDone(false); // can't communicate while radio is not defined! + mFirstTry = q->iv->isAvailable(); q->iv->mCmd = q->cmd; q->iv->mIsSingleframeReq = false; mState = States::START; @@ -140,8 +141,25 @@ class Communication : public CommQueue<> { if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) { q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); mWaitTime.startTimeMonitor(1000); - } else if(IV_MI == q->iv->ivGen) - q->iv->mIvTxCnt++; + } else { + if(IV_MI == q->iv->ivGen) + q->iv->mIvTxCnt++; + if(mFirstTry){ + mFirstTry = false; + mState = States::START; + setAttempt(); + mHeu.evalTxChQuality(q->iv, false, 0, 0); + //q->iv->radioStatistics.rxFailNoAnser++; + q->iv->radioStatistics.retransmits++; + mWaitTime.stopTimeMonitor(); + + /*if(*mSerialDebug) { + DPRINT_IVID(DBG_INFO, q->iv->id); + DBGPRINTLN(F("second try")); + }*/ + return; + } + } } closeRequest(q, false); break; @@ -553,7 +571,6 @@ class Communication : public CommQueue<> { q->iv->mGotLastMsg = false; q->iv->miMultiParts = 0; mIsRetransmit = false; - mFirstTry = false; // for correct reset mState = States::RESET; DBGPRINTLN(F("-----")); } From 1ac920d52d8b5768f0be26db5b5959543af84e38 Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 15 Jan 2024 21:38:54 +0100 Subject: [PATCH 086/115] 0.8.57 * merge PR: fix immediate clearing of display after sunset #1364 --- src/CHANGES.md | 3 +++ src/defines.h | 2 +- src/plugins/Display/Display_Mono.h | 27 ++++++++++++++++----------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index d566216a..2032f009 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.8.57 - 2024-01-15 +* merge PR: fix immediate clearing of display after sunset #1364 + ## 0.8.56 - 2024-01-15 * potential fix of update problems and random reboots #1359 #1354 diff --git a/src/defines.h b/src/defines.h index 46c9a249..528aa714 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 56 +#define VERSION_PATCH 57 //------------------------------------- typedef struct { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index e0a03c61..58b4224f 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -116,12 +116,16 @@ class DisplayMono { if (nullptr == mPgData) // power graph not initialized return; + bool storeStartEndTimes = false; bool store_entry = false; switch(mPgState) { case PowerGraphState::NO_TIME_SYNC: - if ((mDisplayData->pGraphStartTime > 0) && (mDisplayData->pGraphEndTime > 0) && // wait until period data is available ... - (mDisplayData->utcTs >= mDisplayData->pGraphStartTime) && (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) { // and current time is in period - storeStartEndTimes(); // period was received -> store + if ((mDisplayData->pGraphStartTime > 0) + && (mDisplayData->pGraphEndTime > 0) // wait until period data is available ... + && (mDisplayData->utcTs >= mDisplayData->pGraphStartTime) + && (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) // and current time is in period + { + storeStartEndTimes = true; // period was received -> store store_entry = true; mPgState = PowerGraphState::IN_PERIOD; } @@ -134,7 +138,7 @@ class DisplayMono { break; case PowerGraphState::WAIT_4_NEW_PERIOD: if ((mPgStartTime != mDisplayData->pGraphStartTime) || (mPgEndTime != mDisplayData->pGraphEndTime)) { // wait until new time period was received ... - storeStartEndTimes(); // and store it for next period + storeStartEndTimes = true; // and store it for next period mPgState = PowerGraphState::WAIT_4_RESTART; } break; @@ -146,6 +150,14 @@ class DisplayMono { } break; } + + // store start and end times of current time period and calculate period length + if (storeStartEndTimes) { + mPgStartTime = mDisplayData->pGraphStartTime; + mPgEndTime = mDisplayData->pGraphEndTime; + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis + } + if (store_entry) { mPgLastPos = std::min((uint8_t) sss2PgPos(mDisplayData->utcTs - mPgStartTime), (uint8_t) (mPgWidth - 1)); // current datapoint based on seconds since start mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values @@ -237,13 +249,6 @@ class DisplayMono { } } - // store start and end times of current time period and calculate period length - void storeStartEndTimes() { - mPgStartTime = mDisplayData->pGraphStartTime; - mPgEndTime = mDisplayData->pGraphEndTime; - mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis - } - // get power graph datapoint index, scaled to current time period, by seconds since start uint8_t sss2PgPos(uint seconds_since_start) { if(mPgPeriod) From 358344ad6d0f23f20aa5f52a0c493b473decb182 Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 15 Jan 2024 22:55:56 +0100 Subject: [PATCH 087/115] 0.8.57 * merge PR: MI-MQTT and last retransmit #1363 * fixed DTU-ID, now built from the unique part of the MAC * fix lang in `/system` #1346 --- src/CHANGES.md | 3 +++ src/hm/CommQueue.h | 1 - src/hm/Communication.h | 4 ++-- src/hm/radio.h | 15 +++++++++------ src/web/html/update.html | 2 +- src/web/lang.h | 8 ++++---- src/web/lang.json | 2 +- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 2032f009..15dcfba2 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,9 @@ ## 0.8.57 - 2024-01-15 * merge PR: fix immediate clearing of display after sunset #1364 +* merge PR: MI-MQTT and last retransmit #1363 +* fixed DTU-ID, now built from the unique part of the MAC +* fix lang in `/system` #1346 ## 0.8.56 - 2024-01-15 * potential fix of update problems and random reboots #1359 #1354 diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h index d62efe07..13decc01 100644 --- a/src/hm/CommQueue.h +++ b/src/hm/CommQueue.h @@ -11,7 +11,6 @@ #include "hmInverter.h" #include "../utils/dbg.h" -// needs a '+1' because the comparison does not send if attempts is equal 0 #define DEFAULT_ATTEMPS 5 #define MORE_ATTEMPS_ALARMDATA 15 #define MORE_ATTEMPS_GRIDONPROFILEPARA 15 diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 9e383b6a..7b5c84f9 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -144,14 +144,14 @@ class Communication : public CommQueue<> { } else { if(IV_MI == q->iv->ivGen) q->iv->mIvTxCnt++; - if(mFirstTry){ + if(mFirstTry) { mFirstTry = false; - mState = States::START; setAttempt(); mHeu.evalTxChQuality(q->iv, false, 0, 0); //q->iv->radioStatistics.rxFailNoAnser++; q->iv->radioStatistics.retransmits++; mWaitTime.stopTimeMonitor(); + mState = States::START; /*if(*mSerialDebug) { DPRINT_IVID(DBG_INFO, q->iv->id); diff --git a/src/hm/radio.h b/src/hm/radio.h index e5eda128..1422b285 100644 --- a/src/hm/radio.h +++ b/src/hm/radio.h @@ -103,16 +103,19 @@ class Radio { void generateDtuSn(void) { uint32_t chipID = 0; #ifdef ESP32 - uint64_t MAC = ESP.getEfuseMac(); - chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); + chipID = (ESP.getEfuseMac() & 0xffffffff); #else chipID = ESP.getChipId(); #endif - mDtuSn = 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++) { - mDtuSn |= (chipID % 10) << (i * 4); - chipID /= 10; + + uint8_t t; + for(int i = 0; i < (7 << 2); i += 4) { + t = (chipID >> i) & 0x0f; + if(t > 0x09) + t -= 6; + mDtuSn |= (t << i); } + mDtuSn |= 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal } uint32_t mDtuSn; diff --git a/src/web/html/update.html b/src/web/html/update.html index 3fed75a5..800d9dfe 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -10,7 +10,7 @@
    {#SELECT_FILE} (*.bin) -

    {#INSTALLED_VERSION}:

    +

    {#INSTALLED_VERSION}:

    diff --git a/src/web/lang.h b/src/web/lang.h index 3c3cd631..546399aa 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -73,15 +73,15 @@ #endif #ifdef LANG_DE - #define FACTORY_RESET "Ahoy Factory Reset" -#else /*LANG_EN*/ #define FACTORY_RESET "Ahoy auf Werkseinstellungen zurücksetzen" +#else /*LANG_EN*/ + #define FACTORY_RESET "Ahoy Factory Reset" #endif #ifdef LANG_DE - #define BTN_REBOOT "Reboot" -#else /*LANG_EN*/ #define BTN_REBOOT "Ahoy neustarten" +#else /*LANG_EN*/ + #define BTN_REBOOT "Reboot" #endif #endif /*__LANG_H__*/ diff --git a/src/web/lang.json b/src/web/lang.json index c910650e..ae8c0ca2 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -781,7 +781,7 @@ { "token": "IRQ_WORKING", "en": "Interrupt Pin working", - "de": "Interrupt Pin funktoniert" + "de": "Interrupt Pin funktioniert" }, { "token": "NRF24_DATA_RATE", From f503516c9feba7a7cc6fb427954165ce05cd7746 Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 16 Jan 2024 00:11:34 +0100 Subject: [PATCH 088/115] 0.8.57 * added protection to prevent update to wrong firmware (environment check) --- ahoy.code-workspace | 45 ++++++++++++++++++++++++++++++++++++++++ src/CHANGES.md | 1 + src/app.cpp | 2 +- src/web/RestApi.h | 4 ++-- src/web/html/update.html | 29 ++++++++++++++++++++------ src/web/lang.json | 20 ++++++++++++++++++ 6 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 ahoy.code-workspace diff --git a/ahoy.code-workspace b/ahoy.code-workspace new file mode 100644 index 00000000..20d5909e --- /dev/null +++ b/ahoy.code-workspace @@ -0,0 +1,45 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "src" + } + ], + "settings": { + "files.associations": { + "algorithm": "cpp", + "array": "cpp", + "chrono": "cpp", + "deque": "cpp", + "format": "cpp", + "forward_list": "cpp", + "functional": "cpp", + "initializer_list": "cpp", + "iterator": "cpp", + "list": "cpp", + "memory": "cpp", + "queue": "cpp", + "random": "cpp", + "regex": "cpp", + "vector": "cpp", + "xhash": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xmemory": "cpp", + "xstring": "cpp", + "xtree": "cpp", + "xutility": "cpp", + "*.tcc": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "string_view": "cpp", + "sstream": "cpp", + "istream": "cpp", + "ostream": "cpp" + }, + "editor.formatOnSave": false + } +} \ No newline at end of file diff --git a/src/CHANGES.md b/src/CHANGES.md index 15dcfba2..316224ab 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -5,6 +5,7 @@ * merge PR: MI-MQTT and last retransmit #1363 * fixed DTU-ID, now built from the unique part of the MAC * fix lang in `/system` #1346 +* added protection to prevent update to wrong firmware (environment check) ## 0.8.56 - 2024-01-15 * potential fix of update problems and random reboots #1359 #1354 diff --git a/src/app.cpp b/src/app.cpp index 6350e990..8dfcdb4d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -35,7 +35,7 @@ void app::setup() { } #if defined(ESP32) if(mConfig->cmt.enabled) { - mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false); + mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb); } #endif #ifdef ETHERNET diff --git a/src/web/RestApi.h b/src/web/RestApi.h index edf92a97..097adac3 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -326,8 +326,8 @@ class RestApi { void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { getSysInfo(request, obj.createNestedObject(F("system"))); getGeneric(request, obj.createNestedObject(F("generic"))); - char tmp[100]; - snprintf(tmp, 100, "%s

    %s", FACTORY_RESET, BTN_REBOOT); + char tmp[200]; + snprintf(tmp, 200, "%s

    %s", FACTORY_RESET, BTN_REBOOT); obj[F("html")] = String(tmp); } diff --git a/src/web/html/update.html b/src/web/html/update.html index 800d9dfe..52ace5f1 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -23,17 +23,34 @@
    {#HTML_FOOTER} + From 7e81709eb83d5a13c0b838c94c23a0e97cbce7d3 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 21 Jan 2024 14:00:44 +0100 Subject: [PATCH 113/115] 0.8.61 * add favicon to header * improved NRF communication * merge PR: provide localized times to display mono classes #1376 * merge PR: Bypass OOM-Crash on minimal version & history access #1378 * merge PR: Add some REST Api Endpoints to avail_endpoints #1380 --- src/CHANGES.md | 3 +++ src/web/RestApi.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/CHANGES.md b/src/CHANGES.md index 2c52b991..d37c7eb6 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -3,6 +3,9 @@ ## 0.8.61 - 2024-01-21 * add favicon to header * improved NRF communication +* merge PR: provide localized times to display mono classes #1376 +* merge PR: Bypass OOM-Crash on minimal version & history access #1378 +* merge PR: Add some REST Api Endpoints to avail_endpoints #1380 ## 0.8.60 - 2024-01-20 * merge PR: non blocking nRF loop #1371 diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 003a0592..98e0afd2 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -206,8 +206,10 @@ class RestApi { #endif /* !defined(ETHERNET) */ ep[F("system")] = url + F("system"); ep[F("live")] = url + F("live"); + #if defined(ENABLE_HISTORY) ep[F("powerHistory")] = url + F("powerHistory"); ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); + #endif } From e039820dba7fc7a6f26b0778192cf2b852c32f24 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 21 Jan 2024 17:44:10 +0100 Subject: [PATCH 114/115] 0.8.62 * updated version in footer #1381 * repaired radio statistics #1382 --- src/CHANGES.md | 4 ++++ src/defines.h | 2 +- src/hm/Communication.h | 7 ++++--- src/hm/hmInverter.h | 3 ++- src/hm/hmRadio.h | 27 ++++----------------------- src/web/html/includes/footer.html | 2 +- 6 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index d37c7eb6..23f0e685 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.62 - 2024-01-21 +* updated version in footer #1381 +* repaired radio statistics #1382 + ## 0.8.61 - 2024-01-21 * add favicon to header * improved NRF communication diff --git a/src/defines.h b/src/defines.h index 8d6a3086..c614103e 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 61 +#define VERSION_PATCH 62 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 59f21d2b..80b43ab3 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -143,16 +143,17 @@ class Communication : public CommQueue<> { } else { if(IV_MI == q->iv->ivGen) q->iv->mIvTxCnt++; + if(mFirstTry) { - mFirstTry = false; + mFirstTry = false; setAttempt(); mHeu.evalTxChQuality(q->iv, false, 0, 0); - //q->iv->radioStatistics.rxFailNoAnser++; + q->iv->radioStatistics.rxFailNoAnser++; q->iv->radioStatistics.retransmits++; q->iv->radio->mRadioWaitTime.stopTimeMonitor(); mState = States::START; - return; + return; } } } diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 2c1c6362..b28682e5 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -210,9 +210,10 @@ class Inverter { cb(GridOnProFilePara, false); } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate mGetLossInterval = 1; + cb(RealTimeRunData_Debug, false); // get live data cb(GetLossRate, false); } else - cb(RealTimeRunData_Debug, false); // get live data + cb(RealTimeRunData_Debug, false); // get live data } } else { // MI if(0 == getFwVersion()) { diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index d298afd3..67b1abcd 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -78,13 +78,11 @@ class HmRadio : public Radio { #else mNrf24->begin(mSpi.get(), ce, cs); #endif - mNrf24->setRetries(3, 15); // 3*250us + 250us and 16 loops -> 15.25ms + mNrf24->setRetries(3, 15); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms - mNrf24->setChannel(mRfChLst[mRxChIdx]); - mNrf24->startListening(); mNrf24->setDataRate(RF24_250KBPS); - mNrf24->setAutoAck(true); - mNrf24->enableDynamicAck(); + //mNrf24->setAutoAck(true); // enabled by default + //mNrf24->enableDynamicAck(); mNrf24->enableDynamicPayloads(); mNrf24->setCRCLength(RF24_CRC_16); mNrf24->setAddressWidth(5); @@ -155,21 +153,6 @@ class HmRadio : public Radio { if(tx_ok) mLastIv->mAckCount++; - // start listening - if(!mIsRetransmit) { - if(mTxSetupTime < 30) { - mRxChIdx = (mTxChIdx + 4) % RF_CHANNELS; - mNrf24->setChannel(mRfChLst[mRxChIdx]); - mNrf24->startListening(); - - do { - yield(); - } while((millis() - mMillis) < 37); - } - } - mIsRetransmit = false; - - mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; mNrf24->setChannel(mRfChLst[mRxChIdx]); mNrf24->startListening(); @@ -185,7 +168,7 @@ class HmRadio : public Radio { if (getReceived()) { // check what we got, returns true for last package mNRFisInRX = false; mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions - // add stop listening? + mNrf24->stopListening(); } else { innerLoopTimeout = DURATION_LISTEN_MIN; mTimeslotStart = millis(); @@ -398,7 +381,6 @@ class HmRadio : public Radio { mLastIv = iv; iv->mDtuTxCnt++; mNRFisInRX = false; - mIsRetransmit = isRetransmit; } uint64_t getIvId(Inverter<> *iv) { @@ -433,7 +415,6 @@ class HmRadio : public Radio { bool rxPendular = false; uint32_t innerLoopTimeout = DURATION_LISTEN_MIN; uint8_t mTxSetupTime = 0; - bool mIsRetransmit = false; std::unique_ptr mSpi; std::unique_ptr mNrf24; diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html index bbc4c6ce..6aa89673 100644 --- a/src/web/html/includes/footer.html +++ b/src/web/html/includes/footer.html @@ -1,6 +1,6 @@