Browse Source

Merge branch 'development03' into ethW5500

pull/1512/head
lumapu 1 year ago
parent
commit
7eae81c706
  1. 136
      .github/workflows/compile_development.yml
  2. 302
      Getting_Started.md
  3. 12
      README.md
  4. 25
      doc/prometheus_ep_description.md
  5. BIN
      doc/screenshots/inverterSettings.png
  6. BIN
      doc/screenshots/settings.png
  7. 301
      manual/Getting_Started.md
  8. 19
      manual/User_Manual.md
  9. 71
      manual/ahoy_config.md
  10. 12
      patches/GxEPD2_SW_SPI.patch
  11. 3
      scripts/auto_firmware_version.py
  12. 61
      scripts/convertHtml.py
  13. 133
      scripts/getVersion.py
  14. 41
      scripts/reduceGxEPD2.py
  15. 4
      src/.vscode/settings.json
  16. 154
      src/CHANGES.md
  17. 188
      src/app.cpp
  18. 109
      src/app.h
  19. 19
      src/appInterface.h
  20. 53
      src/config/config.h
  21. 8
      src/config/config_override_example.h
  22. 98
      src/config/settings.h
  23. 21
      src/defines.h
  24. 6
      src/eth/ahoyeth.h
  25. 18
      src/hm/CommQueue.h
  26. 549
      src/hm/Communication.h
  27. 11
      src/hm/hmDefines.h
  28. 235
      src/hm/hmInverter.h
  29. 209
      src/hm/hmRadio.h
  30. 26
      src/hm/hmSystem.h
  31. 15
      src/hm/nrfHal.h
  32. 40
      src/hm/radio.h
  33. 175
      src/hm/simulator.h
  34. 2
      src/hms/cmtHal.h
  35. 10
      src/hms/esp32_3wSpi.h
  36. 26
      src/hms/hmsRadio.h
  37. 286
      src/platformio.ini
  38. 399
      src/plugins/Display/Display.h
  39. 381
      src/plugins/Display/Display_Mono.h
  40. 18
      src/plugins/Display/Display_Mono_128X32.h
  41. 217
      src/plugins/Display/Display_Mono_128X64.h
  42. 16
      src/plugins/Display/Display_Mono_64X48.h
  43. 165
      src/plugins/Display/Display_Mono_84X48.h
  44. 28
      src/plugins/Display/Display_data.h
  45. 23
      src/plugins/Display/Display_ePaper.cpp
  46. 6
      src/plugins/Display/Display_ePaper.h
  47. 128
      src/plugins/history.h
  48. 47
      src/publisher/pubMqtt.h
  49. 152
      src/publisher/pubMqttIvData.h
  50. 6
      src/utils/crc.cpp
  51. 6
      src/utils/helper.cpp
  52. 8
      src/utils/improv.h
  53. 14
      src/utils/scheduler.h
  54. 9
      src/utils/syslog.cpp
  55. 15
      src/utils/timemonitor.h
  56. 162
      src/web/RestApi.h
  57. 28
      src/web/html/api.js
  58. 59
      src/web/html/grid_info.json
  59. 117
      src/web/html/history.html
  60. 2
      src/web/html/includes/footer.html
  61. 3
      src/web/html/includes/header.html
  62. 12
      src/web/html/includes/nav.html
  63. 83
      src/web/html/index.html
  64. 14
      src/web/html/save.html
  65. 2
      src/web/html/serial.html
  66. 679
      src/web/html/setup.html
  67. 68
      src/web/html/style.css
  68. 42
      src/web/html/system.html
  69. 37
      src/web/html/update.html
  70. 160
      src/web/html/visualization.html
  71. 87
      src/web/html/wizard.html
  72. 87
      src/web/lang.h
  73. 1479
      src/web/lang.json
  74. 289
      src/web/web.h
  75. 19
      src/wifi/ahoywifi.cpp
  76. 18
      src/wifi/ahoywifi.h

136
.github/workflows/compile_development.yml

@ -5,14 +5,94 @@ 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:
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 }}
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
ref: development03
name: dev-${{ matrix.variant }}
path: firmware/*
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,43 +123,49 @@ 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 -e ${{ matrix.variant }}
- name: Copy boot_app0.bin
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Rename Binary files
id: rename-binary-files
working-directory: src
run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: dev-${{ matrix.variant }}
path: firmware/*
deploy:
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
- name: Get Artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: firmware
- 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 }}
- 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
VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
src: src/${{ steps.rename-binary-files.outputs.name }}/
src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/dev
port: ${{ secrets.FW_SSH_PORT }}

302
Getting_Started.md

@ -1,302 +0,0 @@
## 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.
You find the full [User_Manual here](User_Manual.md)
## Compatiblity
The following inverters are currently supported out of the box:
Hoymiles Inverters
| Status | Serie | Model | comment |
| ----- | ----- | ------ | ------- |
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Overview](#overview)
- [Compatiblity](#compatiblity)
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [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)
- [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
- [HTTP based Pages](#http-based-pages)
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
- [Used Libraries](#used-libraries)
- [ToDo](#todo)
***
Solenso Inverters:
- SOL-H350
## 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..
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.
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** |
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.
| **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** |
#### There are fake NRF24L01+ Modules out there
Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).<br/>
An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/>
You are welcome to add more examples of faked chips. We will add that information here.<br/>
Some users reported better connection or longer range through more walls when using the
"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector,
which has an eye-catching HF cover. But beware: It comes without the antenna!
In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW".
Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN".
It is not always the bigger the better...
Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator.
The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously.
If you operate additional interfaces like a display, the reserve is again reduced.
## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins:
- SCLK (Signal Clock),
- MISO (Master In Slave Out) and
- MOSI (Master Out Slave In)
*These pins need to be configured in the config.h.*
Additional, there are 3 pins, which can be set individual:
- CS (Chip Select),
- CE (Chip Enable) and
- IRQ (Interrupt)
*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.<br>
##### 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
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example
Example wiring for a 38pin ESP32 module
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp32-38_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp32-38_nrf.png)
##### ESP32 GPIO settings
CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
```
CS D1 (GPIO5)
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.
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.
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.
### 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.<br/>
Visual Studio Code, AtomIDE and other IDE's support the PlatformIO Addon.<br/>
If you do not want to compile your own build, you can use one of our ready-to-flash binaries.
##### Optional Configuration before compilation
- 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.
#### Using a ready-to-flash binary using nodemcu-pyflasher
This information suits you if you just want to use an easy way.
1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher)
2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. open flash-tool and connect the target device to your computer.
4. Set the correct serial port and select the correct *.bin file
5. click on "Flash NodeMCU"
6. flash the ESP with the compiled firmware using the UART pins or
7. repower the ESP
8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
10. configure your WiFi settings, save, repower
11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
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!
#### 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.
2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. `cd ahoy_v<XXX> && cp *esp32.bin esp32.bin`
4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
5. Unplug and replug your device.
6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
## Connect to your Ahoy DTU
When everything is wired up 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.<br/>
This might be useful in case of any troubles that might occur as well as to simply<br/>
obtain information about the converted values which were read out of 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.<br/>
If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address<br/>
from your local DHCP Server (in most cases thats your Router).<br/><br/>
In case it could not connect to your configured Network, it will provide its own WiFi Network that you can<br/>
connect to for furter configuration.<br/>
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`".<br/>
The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).<br/>
If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/>
<br/>
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.<br/>
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/).<br/>
Just open the IP-Address in your browser.<br/>
<br/>
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)
##### 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) ).<br/>
| page | use | output | default availability |
| ---- | ------ | ------ | ------ |
| /uptime | displays the uptime uf your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | yes |
| /reboot | reboots the Ahoy DTU | | yes |
| /erase | erases the EEPROM | | yes |
| /factory | resets to the factory defaults configured in config.h | | yes |
| /setup | opens the setup page | | yes |
| /save | | | yes |
| /cmdstat | show stat from the home page | | yes |
| /visualization | displays the information from your converter | | yes |
| /livedata | displays the live data | | yes |
| /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 |
## MQTT command to set the DTU without webinterface
[Read here](User_Manual.md)
## Used Libraries
| Name | version | License |
| --------------------- | ------- | -------- |
| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
| `DNSServer` | 1.1.1 | LGPL-2.1 |
| `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 |
## ToDo
[See this post](https://github.com/lumapu/ahoy/issues/142)

12
README.md

@ -20,7 +20,7 @@ This work is licensed under a
# 🖐 Ahoy!
![Logo](https://github.com/lumapu/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.
## Changelog
[latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md)
@ -39,9 +39,11 @@ Table of approaches:
⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker**
## 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)
@ -50,11 +52,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!**

25
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,25 @@ 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. 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 |
| `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_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 |
@ -46,9 +60,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 | |

BIN
doc/screenshots/inverterSettings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/screenshots/settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

301
manual/Getting_Started.md

@ -0,0 +1,301 @@
## Overview
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
Currently, the following inverters are supported:
Hoymiles Inverters
| Status | Serie | Model | comment |
| ----- | ----- | ------ | ------- |
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
## Table of Contents
- [Overview](#overview)
- [Compatiblity](#compatiblity)
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [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)
- [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)
- [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
- [HTTP based Pages](#http-based-pages)
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
- [Used Libraries](#used-libraries)
- [ToDo](#todo)
***
Solenso Inverters:
- SOL-H350
## Things needed
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.
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!
| **Parts** | **Price** |
| --- | --- |
| 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 €** |
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 €|
| *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.
#### 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.**
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.
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
The NRF24L01+ radio module is connected to the standard SPI pins:
- SCLK (Signal Clock),
- MISO (Master In Slave Out) and
- MOSI (Master Out Slave In)
*These pins need to be configured in the config.h.*
Additional, there are 3 pins, which can be set individual:
- CS (Chip Select),
- CE (Chip Enable) and
- IRQ (Interrupt)
*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.
##### 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
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example
Example wiring for a 38pin ESP32 module
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp32-38_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp32-38_nrf.png)
##### ESP32 GPIO settings
CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
```
CS D1 (GPIO5)
CE D2 (GPIO4)
IRQ D0 (GPIO16 - no IRQ!)
```
**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 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
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 (expert)
This information is for those who wish to configure and build their own firmware.
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
This information suits you if you just want to use an easy way.
1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher)
2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. open flash-tool and connect the target device to your computer.
4. Set the correct serial port and select the correct *.bin file
5. click on "Flash NodeMCU"
6. flash the ESP with the compiled firmware using the UART pins or
7. repower the ESP
8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
10. configure your WiFi settings, save, repower
11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
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!**
#### 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.
2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. `cd ahoy_v<XXX> && cp *esp32.bin esp32.bin`
4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
5. Unplug and replug your device.
6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
## 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
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 successfully flashed and powered up your Ahoy DTU, you can access it from your browser.<br/>
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) ).
| page | use | output | default availability |
| ---- | ------ | ------ | ------ |
| /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 |
| /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 |
## MQTT command to set the DTU without webinterface
[Read here](User_Manual.md)
## Used Libraries
| Name | version | License |
| --------------------- | ------- | -------- |
| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
| `DNSServer` | 1.1.1 | LGPL-2.1 |
| `SPI` | 1.0 | LGPL-2.1 |
| `Hash` | 1.0 | LGPL-2.1 |
| `EEPROM` | 1.0 | LGPL-2.1 |
| `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
[See this post](https://github.com/lumapu/ahoy/issues/142)

19
User_Manual.md → manual/User_Manual.md

@ -195,8 +195,9 @@ The `<VALUE>` 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 `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
"val": <VALUE>
}
```
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": <VALUE>
}
```
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": <VALUE>
}
```
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": <VALUE>
}
```
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

71
manual/ahoy_config.md

@ -0,0 +1,71 @@
# 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.
## Start
But how do I get my data from the inverter?
The following steps are required:
1. Set the pinning to communicate with the radio module.
2. Check if Ahoy has a current time
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.
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 (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.
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. 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/lumapu/ahoy/doc/screenshots/settings.png)
![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 to be changed)
In the "Advanced" section, you can customize more settings.
Save and reboot.
## ✅ Done - Now check the live site

12
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*);

3
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)

61
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
@ -22,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):
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()
@ -58,12 +74,16 @@ def htmlParts(file, header, nav, footer, version):
p += line
#placeholders
version = readVersion(versionPath);
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link)
# 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 +114,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, versionPath, lang):
fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper()
@ -114,7 +157,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", versionPath, lang)
else:
f = open(inFile, "r")
data = f.read()
@ -167,8 +210,12 @@ 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"
if env['PIOENV'][-3:] == "-de":
lang = "de"
# go throw the array
for val in files_grabbed:
convert2Header(val, version)
convert2Header(val, "../../defines.h", lang)

133
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,106 +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")
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)
# 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("src/.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("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin")
os.rename("src/.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])

41
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")

4
src/.vscode/settings.json

@ -84,5 +84,5 @@
},
"cmake.configureOnOpen": false,
"editor.formatOnSave": false,
"cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO",
}
"cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO"
}

154
src/CHANGES.md

@ -1,5 +1,159 @@
# Development Changes
## 0.8.63 - 2024-01-22
* made code review
* fixed endless loop #1387
## 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
* 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
* merge PR: fixed millis in serial log #1373
* merge PR: fix powergraph scale #1374
* changed inverter gap to `1` as default (old settings will be overridden)
## 0.8.59 - 2024-01-18
* merge PR: solve display settings dependencies #1369
* fix language typos #1346
* full update of ePaper after booting #1107
* fix MqTT yield day reset even if `pause inverter during nighttime` isn't active #1368
## 0.8.58 - 2024-01-17
* fix missing refresh URL #1366
* fix view of grid profile #1365
* fix webUI translation #1346
* fix protection mask #1352
* merge PR: Add Watchdog for ESP32 #1367
* merge PR: ETH support for CMT2300A - HMS/HMT #1356
* full refresh of ePaper after booting #1107
* add optional custom link #1199
* pinout has an own subgroup in `/settings`
* grid profile will be displayed as hex in every case #1199
## 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
* 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
## 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
* 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!
* 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
## 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
* added history protection mask
* merge PR: display graph improvements #1347
## 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
* added translations for error messages #1343
## 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
## 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`
* 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
## 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
* live data is read much earlier / faster and more often #1272
## 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
* 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
* 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
* 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
## 0.8.39 - 2024-01-01
* fix MqTT dis_night_comm in the morning #1309 #1286
* seperated offset for sunrise and sunset #1308
* 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
* add `getLossRate` to radio statistics and to MqTT #1199
## 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

188
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
//-----------------------------------------------------------------------------
@ -7,6 +7,10 @@
#include "app.h"
#include "utils/sun.h"
#if !defined(ESP32)
void esp_task_wdt_reset() {}
#endif
//-----------------------------------------------------------------------------
app::app() : ah::Scheduler {} {}
@ -18,7 +22,13 @@ void app::setup() {
while (!Serial)
yield();
#if defined(ESP32)
esp_task_wdt_init(WDT_TIMEOUT_SECONDS, true);
esp_task_wdt_add(NULL);
#endif
resetSystem();
esp_task_wdt_reset();
mSettings.setup();
mSettings.getPtr(mConfig);
@ -30,12 +40,14 @@ void app::setup() {
else
DBGPRINTLN(F("false"));
esp_task_wdt_reset();
if(mConfig->nrf.enabled) {
mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
}
#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
@ -50,9 +62,14 @@ void app::setup() {
#endif
#endif /* defined(ETHERNET) */
esp_task_wdt_reset();
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));
mSys.setup(&mTimestamp, &mConfig->inst);
#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);
}
@ -62,8 +79,11 @@ void app::setup() {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
esp_task_wdt_reset();
// 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);
@ -71,8 +91,11 @@ void app::setup() {
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
}
#endif
#endif
setupLed();
esp_task_wdt_reset();
mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
@ -83,7 +106,7 @@ void app::setup() {
#endif
// Plugins
#if defined(PLUGIN_DISPLAY)
if (mConfig->plugin.display.type != 0)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
#if defined(ESP32)
mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, &mCmtRadio, &mTimestamp);
#else
@ -91,19 +114,37 @@ void app::setup() {
#endif
#endif
esp_task_wdt_reset();
#if defined(ENABLE_HISTORY)
mHistory.setup(this, &mSys, mConfig, &mTimestamp);
#endif /*ENABLE_HISTORY*/
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
#if !defined(ETHERNET)
//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*/
esp_task_wdt_reset();
regularTickers();
}
//-----------------------------------------------------------------------------
void app::loop(void) {
esp_task_wdt_reset();
if(mConfig->nrf.enabled)
mNrfRadio.loop();
#if defined(ESP32)
if(mConfig->cmt.enabled)
mCmtRadio.loop();
@ -112,8 +153,11 @@ void app::loop(void) {
ah::Scheduler::loop();
mCommunication.loop();
#if defined(ENABLE_MQTT)
if (mMqttEnabled && mNetworkConnected)
mMqtt.loop();
#endif
yield();
}
//-----------------------------------------------------------------------------
@ -141,13 +185,21 @@ void app::regularTickers(void) {
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins
#if defined(PLUGIN_DISPLAY)
if (mConfig->plugin.display.type != 0)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
#endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET)
//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)
@ -163,11 +215,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
@ -226,15 +280,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::tickSunrise, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm'
}
}
//-----------------------------------------------------------------------------
@ -251,14 +308,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,8 +336,27 @@ 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 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
}
//-----------------------------------------------------------------------------
void app::notAvailChanged(void) {
#if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.notAvailChanged(mAllIvNotAvail);
#endif
}
//-----------------------------------------------------------------------------
@ -325,13 +401,16 @@ void app::tickMidnight(void) {
if (mConfig->inst.rstYieldMidNight) {
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
#if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.tickerMidnight();
#endif
}
}
//-----------------------------------------------------------------------------
void app::tickSend(void) {
bool notAvail = true;
uint8_t fill = mCommunication.getFillState();
uint8_t max = mCommunication.getMaxFill();
if((max-MAX_NUM_INVERTERS) <= fill) {
@ -357,6 +436,9 @@ void app::tickSend(void) {
if(!iv->radio->isChipConnected())
continue;
if(InverterStatus::OFF != iv->status)
notAvail = false;
iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if(isDevControl)
mCommunication.addImportant(iv, cmd);
@ -366,6 +448,10 @@ void app::tickSend(void) {
}
}
if(mAllIvNotAvail != notAvail)
once(std::bind(&app::notAvailChanged, this), 1, "avail");
mAllIvNotAvail = notAvail;
updateLed();
}
@ -415,22 +501,56 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
changed = true;
}
if(changed) {
if(mMqttEnabled && !skipYieldDay)
mMqtt.setZeroValuesEnable();
if(changed)
payloadEventListener(RealTimeRunData_Debug, NULL);
}
}
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
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 PLUGIN_DISPLAY
"D"
#endif
#ifdef ENABLE_HISTORY
"H"
#endif
#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
mTimestamp = 1;
#endif
mSendFirst = true;
mAllIvNotAvail = true;
mSunrise = 0;
mSunset = 0;
@ -453,14 +573,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);
}
}
}
@ -469,27 +586,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);
}
}

109
src/app.h

@ -8,6 +8,10 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#if defined(ESP32)
#include <esp_task_wdt.h>
#define WDT_TIMEOUT_SECONDS 8 // Watchdog Timeout 8s
#endif
#include "config/settings.h"
#include "defines.h"
@ -17,13 +21,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)
@ -33,8 +42,13 @@
#include "utils/improv.h"
#endif /* defined(ETHERNET) */
#if defined(ENABLE_SIMULATOR)
#include "hm/simulator.h"
#endif /*ENABLE_SIMULATOR*/
#include <RF24.h> // 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 +56,18 @@
#define ACOS(x) (degrees(acos(x)))
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
#ifdef ESP32
#endif
typedef Web<HmSystemType> WebType;
typedef RestApi<HmSystemType> RestApiType;
#if defined(ENABLE_MQTT)
typedef PubMqtt<HmSystemType> PubMqttType;
#endif /*ENABLE_MQTT*/
typedef PubSerial<HmSystemType> PubSerialType;
#if defined(ENABLE_HISTORY)
typedef HistoryData<HmSystemType> HistoryType;
#endif /*ENABLE_HISTORY*/
#if defined (ENABLE_SIMULATOR)
typedef Simulator<HmSystemType> SimulatorType;
#endif /*ENABLE_SIMULATOR*/
// PLUGINS
#if defined(PLUGIN_DISPLAY)
@ -62,7 +82,7 @@ class app : public IApp, public ah::Scheduler {
~app() {}
void setup(void);
void loop(void);
void loop(void) override;
void onNetwork(bool gotIp);
void regularTickers(void);
@ -96,7 +116,7 @@ class app : public IApp, public ah::Scheduler {
}
uint64_t getTimestampMs() {
return ((uint64_t)Scheduler::mTimestamp * 1000) + (uint64_t)Scheduler::mTsMillis;
return ((uint64_t)Scheduler::mTimestamp * 1000) + ((uint64_t)millis() - (uint64_t)Scheduler::mTsMillis) % 1000;
}
bool saveSettings(bool reboot) {
@ -150,6 +170,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() {
@ -160,6 +192,10 @@ class app : public IApp, public ah::Scheduler {
return mVersion;
}
const char *getVersionModules() {
return mVersionModules;
}
uint32_t getSunrise() {
return mSunrise;
}
@ -177,23 +213,33 @@ class app : public IApp, public ah::Scheduler {
}
void setMqttDiscoveryFlag() {
#if defined(ENABLE_MQTT)
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
}
void setMqttPowerLimitAck(Inverter<> *iv) {
mMqtt.setPowerLimitAck(iv);
#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) {
@ -243,6 +289,22 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime);
}
uint16_t getHistoryValue(uint8_t type, uint16_t i) {
#if defined(ENABLE_HISTORY)
return mHistory.valueAt((HistoryStorageType)type, i);
#else
return 0;
#endif
}
uint16_t getHistoryMaxDay() {
#if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay();
#else
return 0;
#endif
}
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
@ -252,11 +314,13 @@ 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)
if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
mDisplay.payloadEventListener(cmd);
#endif
updateLed();
@ -290,14 +354,20 @@ 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);
void tickSunrise(void);
void tickComm(void);
void tickSend(void);
void tickMinute(void);
void tickZeroValues(void);
void tickMidnight(void);
void notAvailChanged(void);
HmSystemType mSys;
HmRadio<> mNrfRadio;
@ -326,6 +396,7 @@ class app : public IApp, public ah::Scheduler {
#endif
char mVersion[12];
char mVersionModules[12];
settings mSettings;
settings_t *mConfig;
bool mSavePending;
@ -333,11 +404,14 @@ class app : public IApp, public ah::Scheduler {
uint8_t mSendLastIvId;
bool mSendFirst;
bool mAllIvNotAvail;
bool mNetworkConnected;
// mqtt
#if defined(ENABLE_MQTT)
PubMqttType mMqtt;
#endif /*ENABLE_MQTT*/
bool mMqttReconnect;
bool mMqttEnabled;
@ -350,6 +424,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__*/

19
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,14 +7,8 @@
#define __IAPP_H__
#include "defines.h"
#include "hm/hmSystem.h"
#include "ESPAsyncWebServer.h"
//#include "hms/hmsRadio.h"
#if defined(ESP32)
//typedef CmtRadio<esp32_3wSpi<>> CmtRadioType;
#endif
// abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration
class IApp {
@ -29,10 +23,14 @@ 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;
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;
@ -45,10 +43,11 @@ 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;
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0;
virtual bool getNrfEnabled() = 0;
@ -59,8 +58,10 @@ class IApp {
virtual bool getProtection(AsyncWebServerRequest *request) = 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__*/

53
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__
@ -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
@ -93,6 +114,16 @@
#define DEF_NRF_SCLK_PIN 18
#endif
#if defined(ETHERNET) && !defined(SPI_HAL)
#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
@ -142,6 +173,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
@ -181,7 +215,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
@ -219,6 +253,17 @@
// reconnect delay
#define MQTT_RECONNECT_DELAY 5000
// maximum custom link length
#define MAX_CUSTOM_LINK_LEN 100
#define MAX_CUSTOM_LINK_TEXT_LEN 32
// syslog settings
#ifdef ENABLE_SYSLOG
#define SYSLOG_HOST "<hostname-or-ip-address-of-syslog-server>"
#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

8
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 "<hostname-or-ip-address-of-syslog-server>"
#define SYSLOG_APP "ahoy"
#define SYSLOG_FACILITY FAC_USER
#define SYSLOG_PORT 514
#endif
#endif /*__CONFIG_OVERRIDE_H__*/

98
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 9
#define PROT_MASK_INDEX 0x0001
@ -39,8 +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 0x0040
#define PROT_MASK_API 0x0080
#define PROT_MASK_MQTT 0x0100
#define DEF_PROT_INDEX 0x0001
#define DEF_PROT_LIVE 0x0000
@ -48,6 +49,7 @@
#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
@ -106,7 +108,8 @@ typedef struct {
typedef struct {
float lat;
float lon;
uint16_t offsetSec;
int16_t offsetSecMorning;
int16_t offsetSecEvening;
} cfgSun_t;
typedef struct {
@ -117,8 +120,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;
@ -165,6 +167,8 @@ typedef struct {
uint8_t type;
bool pwrSaveAtIvOffline;
uint8_t screenSaver;
uint8_t graph_ratio;
uint8_t graph_size;
uint8_t rot;
//uint16_t wakeUp;
//uint16_t sleepAt;
@ -180,6 +184,8 @@ typedef struct {
typedef struct {
display_t display;
char customLink[MAX_CUSTOM_LINK_LEN];
char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN];
} plugins_t;
typedef struct {
@ -308,18 +314,18 @@ class settings {
DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE);
JsonObject root = json.to<JsonObject>();
json[F("version")] = CONFIG_VERSION;
jsonNetwork(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true);
jsonNetwork(root[F("wifi")].to<JsonObject>(), true);
jsonNrf(root[F("nrf")].to<JsonObject>(), true);
#if defined(ESP32)
jsonCmt(root.createNestedObject(F("cmt")), true);
jsonCmt(root[F("cmt")].to<JsonObject>(), 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<JsonObject>(), true);
jsonSun(root[F("sun")].to<JsonObject>(), true);
jsonSerial(root[F("serial")].to<JsonObject>(), true);
jsonMqtt(root[F("mqtt")].to<JsonObject>(), true);
jsonLed(root[F("led")].to<JsonObject>(), true);
jsonPlugin(root[F("plugin")].to<JsonObject>(), true);
jsonInst(root[F("inst")].to<JsonObject>(), true);
DPRINT(DBG_INFO, F("memory usage: "));
DBGPRINTLN(String(json.memoryUsage()));
@ -373,7 +379,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
@ -420,7 +426,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;
@ -441,7 +448,7 @@ class settings {
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
mCfg.inst.gapMs = 500;
mCfg.inst.gapMs = 1;
mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@ -451,14 +458,17 @@ 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;
mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.contrast = 140;
mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility
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
mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
@ -466,7 +476,7 @@ class settings {
mCfg.plugin.display.disp_reset = DEF_PIN_OFF;
mCfg.plugin.display.disp_busy = DEF_PIN_OFF;
mCfg.plugin.display.disp_dc = DEF_PIN_OFF;
mCfg.plugin.display.pirPin = DEF_MOTION_SENSOR_PIN;
mCfg.plugin.display.pirPin = DEF_PIN_OFF;
}
void loadAddedDefaults() {
@ -496,6 +506,12 @@ class settings {
if(mCfg.configVersion < 7) {
mCfg.led.luminance = 255;
}
if(mCfg.configVersion < 8) {
mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
}
if(mCfg.configVersion < 9) {
mCfg.inst.gapMs = 1;
}
}
}
@ -546,7 +562,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;
}
}
@ -625,11 +641,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<float>(obj, F("lat"), &mCfg.sun.lat);
getVal<float>(obj, F("lon"), &mCfg.sun.lon);
getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec);
getVal<int16_t>(obj, F("offs"), &mCfg.sun.offsetSecMorning);
getVal<int16_t>(obj, F("offsEve"), &mCfg.sun.offsetSecEvening);
}
}
@ -670,13 +688,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<uint8_t>(obj, F("0"), &mCfg.led.led0);
getVal<uint8_t>(obj, F("1"), &mCfg.led.led1);
getVal<uint8_t>(obj, F("0"), &mCfg.led.led[0]);
getVal<uint8_t>(obj, F("1"), &mCfg.led.led[1]);
getVal<uint8_t>(obj, F("2"), &mCfg.led.led[2]);
getVal<bool>(obj, F("act_high"), &mCfg.led.high_active);
getVal<uint8_t>(obj, F("lum"), &mCfg.led.luminance);
}
@ -688,6 +708,8 @@ 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("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;
@ -699,11 +721,15 @@ class settings {
disp[F("busy")] = mCfg.plugin.display.disp_busy;
disp[F("dc")] = mCfg.plugin.display.disp_dc;
disp[F("pirPin")] = mCfg.plugin.display.pirPin;
obj[F("cst_lnk")] = mCfg.plugin.customLink;
obj[F("cst_lnk_txt")] = mCfg.plugin.customLinkText;
} else {
JsonObject disp = obj["disp"];
getVal<uint8_t>(disp, F("type"), &mCfg.plugin.display.type);
getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
getVal<uint8_t>(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver);
getVal<uint8_t>(disp, F("graph_ratio"), &mCfg.plugin.display.graph_ratio);
getVal<uint8_t>(disp, F("graph_size"), &mCfg.plugin.display.graph_size);
getVal<uint8_t>(disp, F("rotation"), &mCfg.plugin.display.rot);
//mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")];
@ -715,6 +741,8 @@ class settings {
getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy);
getVal<uint8_t>(disp, F("dc"), &mCfg.plugin.display.disp_dc);
getVal<uint8_t>(disp, F("pirPin"), &mCfg.plugin.display.pirPin);
getChar(obj, F("cst_lnk"), mCfg.plugin.customLink, MAX_CUSTOM_LINK_LEN);
getChar(obj, F("cst_lnk_txt"), mCfg.plugin.customLinkText, MAX_CUSTOM_LINK_TEXT_LEN);
}
}
@ -796,8 +824,10 @@ class settings {
#if defined(ESP32)
void getChar(JsonObject obj, const char *key, char *dst, int maxLen) {
if(obj.containsKey(key))
if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
dst[maxLen-1] = '\0';
}
}
template<typename T=uint8_t>
@ -807,8 +837,10 @@ class settings {
}
#else
void getChar(JsonObject obj, const __FlashStringHelper *key, char *dst, int maxLen) {
if(obj.containsKey(key))
if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
dst[maxLen-1] = '\0';
}
}
template<typename T=uint8_t>

21
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 37
#define VERSION_PATCH 63
//-------------------------------------
typedef struct {
@ -78,6 +78,18 @@ union serial_u {
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
enum {
DISP_TYPE_T0_NONE = 0,
DISP_TYPE_T1_SSD1306_128X64 = 1,
DISP_TYPE_T2_SH1106_128X64 = 2,
DISP_TYPE_T3_PCD8544_84X48 = 3,
DISP_TYPE_T4_SSD1306_128X32 = 4,
DISP_TYPE_T5_SSD1306_64X48 = 5,
DISP_TYPE_T6_SSD1309_128X64 = 6,
DISP_TYPE_T10_EPAPER = 10
};
//-------------------------------------
// EEPROM
//-------------------------------------
@ -94,7 +106,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;
@ -103,6 +114,10 @@ typedef struct {
uint32_t frmCnt;
uint32_t txCnt;
uint32_t retransmits;
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__*/

6
src/eth/ahoyeth.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://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(ETHERNET)
@ -49,7 +49,7 @@ class ahoyeth {
#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi;
#endif
settings_t *mConfig;
settings_t *mConfig = NULL;
uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server

18
src/hm/CommQueue.h

@ -11,6 +11,10 @@
#include "hmInverter.h"
#include "../utils/dbg.h"
#define DEFAULT_ATTEMPS 5
#define MORE_ATTEMPS_ALARMDATA 8
#define MORE_ATTEMPS_GRIDONPROFILEPARA 5
template <uint8_t N=100>
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) {

549
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
//-----------------------------------------------------------------------------
@ -12,13 +12,10 @@
#include "../utils/timemonitor.h"
#include "Heuristic.h"
#define MI_TIMEOUT 250 // timeout for MI type requests
#define FRSTMSG_TIMEOUT 150 // how long to wait for first msg to be received
#define DEFAULT_TIMEOUT 500 // timeout for regular requests
#define SINGLEFR_TIMEOUT 100 // timeout for single frame requests
#define MAX_BUFFER 250
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> powerLimitAckListenerType;
typedef std::function<void(Inverter<> *)> alarmListenerType;
class Communication : public CommQueue<> {
@ -40,6 +37,10 @@ class Communication : public CommQueue<> {
mCbPayload = cb;
}
void addPowerLimitAckListener(powerLimitAckListenerType cb) {
mCbPwrAck = cb;
}
void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb;
}
@ -60,205 +61,221 @@ class Communication : public CommQueue<> {
mLastEmptyQueueMillis = millis();
mPrintSequenceDuration = true;
uint16_t timeout = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : (((q->iv->mGotFragment && q->iv->mGotLastMsg) || mIsRetransmit) ? SINGLEFR_TIMEOUT : ((q->cmd != AlarmData) && (q->cmd != GridOnProFilePara) ? DEFAULT_TIMEOUT : (1.5 * DEFAULT_TIMEOUT)));
/*if(mDebugState != mState) {
DPRINT(DBG_INFO, F("State: "));
DBGHEXLN((uint8_t)(mState));
mDebugState = mState;
}*/
switch(mState) {
case States::RESET:
if (!mWaitTime.isTimeout())
return;
innerLoop(q);
});
}
mMaxFrameId = 0;
for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) {
mLocalBuf[i].len = 0;
}
private:
inline void innerLoop(const queue_s *q) {
switch(mState) {
case States::RESET:
if (!mWaitTime.isTimeout())
return;
mMaxFrameId = 0;
for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) {
mLocalBuf[i].len = 0;
}
if(*mSerialDebug)
mHeu.printStatus(q->iv);
mHeu.getTxCh(q->iv);
q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false;
q->iv->curFrmCnt = 0;
mIsRetransmit = false;
if(NULL == q->iv->radio)
cmdDone(false); // can't communicate while radio is not defined!
q->iv->mCmd = q->cmd;
q->iv->mIsSingleframeReq = false;
mState = States::START;
break;
if(*mSerialDebug)
mHeu.printStatus(q->iv);
mHeu.getTxCh(q->iv);
q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false;
q->iv->curFrmCnt = 0;
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;
mFramesExpected = getFramesExpected(q); // function to get expected frame count.
mTimeout = DURATION_TXFRAME + mFramesExpected*DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType];
mState = States::START;
break;
case States::START:
setTs(mTimestamp);
if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) {
// frequency was changed during runtime
if(q->iv->curCmtFreq != q->iv->config->frequency) {
if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency))
q->iv->curCmtFreq = q->iv->config->frequency;
}
case States::START:
setTs(mTimestamp);
if(INV_RADIO_TYPE_CMT == q->iv->ivRadioType) {
// frequency was changed during runtime
if(q->iv->curCmtFreq != q->iv->config->frequency) {
if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency))
q->iv->curCmtFreq = q->iv->config->frequency;
}
}
if(q->isDevControl) {
if(ActivePowerContr == q->cmd)
q->iv->powerLimitAck = false;
q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false);
} else
q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
q->iv->radioStatistics.txCnt++;
mWaitTime.startTimeMonitor(timeout);
mIsRetransmit = false;
setAttempt();
if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
incrAttempt(q->cmd == AlarmData? 5 : 3);
mState = States::WAIT;
break;
if(q->isDevControl) {
if(ActivePowerContr == q->cmd)
q->iv->powerLimitAck = false;
q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false);
} else
q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
q->iv->radioStatistics.txCnt++;
q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout);
mIsRetransmit = false;
setAttempt();
if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mState = States::WAIT;
break;
case States::WAIT:
if (!mWaitTime.isTimeout())
return;
mState = States::CHECK_FRAMES;
break;
case States::WAIT:
if (!q->iv->radio->mRadioWaitTime.isTimeout())
return;
mState = States::CHECK_FRAMES;
break;
case States::CHECK_FRAMES: {
if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("request timeout: "));
DBGPRINT(String(mWaitTime.getRunTime()));
DBGPRINTLN(F("ms"));
}
if(!q->iv->mGotFragment) {
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);
case States::CHECK_FRAMES: {
if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("request timeout: "));
DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime()));
DBGPRINTLN(F("ms"));
}
if(!q->iv->mGotFragment) {
if(q->iv->ivRadioType == INV_RADIO_TYPE_CMT) {
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++;
if(mFirstTry) {
mFirstTry = false;
setAttempt();
mHeu.evalTxChQuality(q->iv, false, 0, 0);
q->iv->radioStatistics.rxFailNoAnser++;
q->iv->radioStatistics.retransmits++;
q->iv->radio->mRadioWaitTime.stopTimeMonitor();
mState = States::START;
return;
}
}
closeRequest(q, false);
break;
}
mFirstTry = false; // for correct reset
if((IV_MI != q->iv->ivGen) || (0 == q->attempts))
mIsRetransmit = false;
closeRequest(q, false);
break;
}
mFirstTry = false; // for correct reset
if((IV_MI != q->iv->ivGen) || (0 == q->attempts))
mIsRetransmit = false;
while(!q->iv->radio->mBufCtrl.empty()) {
packet_t *p = &q->iv->radio->mBufCtrl.front();
printRxInfo(q, p);
while(!q->iv->radio->mBufCtrl.empty()) {
packet_t *p = &q->iv->radio->mBufCtrl.front();
if(validateIvSerial(&p->packet[1], q->iv)) {
q->iv->radioStatistics.frmCnt++;
q->iv->mDtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p))
q->iv->curFrmCnt++;
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
if(parseDevCtrl(p, q))
closeRequest(q, true);
else
closeRequest(q, false);
q->iv->radio->mBufCtrl.pop();
return; // don't wait for empty buffer
} else if(IV_MI == q->iv->ivGen) {
if(parseMiFrame(p, q))
q->iv->curFrmCnt++;
}
} //else -> serial does not match
if(validateIvSerial(&p->packet[1], q->iv)) {
printRxInfo(q, p);
q->iv->radioStatistics.frmCnt++;
q->iv->mDtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p))
q->iv->curFrmCnt++;
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
if(parseDevCtrl(p, q))
closeRequest(q, true);
else
closeRequest(q, false);
q->iv->radio->mBufCtrl.pop();
return; // don't wait for empty buffer
} else if(IV_MI == q->iv->ivGen) {
if(parseMiFrame(p, q))
q->iv->curFrmCnt++;
}
} //else -> serial does not match
q->iv->radio->mBufCtrl.pop();
yield();
}
q->iv->radio->mBufCtrl.pop();
yield();
}
if(0 == q->attempts) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("no attempts left"));
closeRequest(q, false);
if(q->iv->ivGen != IV_MI) {
mState = States::CHECK_PACKAGE;
} else {
bool fastNext = true;
if(q->iv->miMultiParts < 6) {
mState = States::WAIT;
if((q->iv->radio->mRadioWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
miRepeatRequest(q);
return;
}
} else {
if(q->iv->ivGen != IV_MI) {
mState = States::CHECK_PACKAGE;
} else {
bool fastNext = true;
if(q->iv->miMultiParts < 6) {
mState = States::WAIT;
if((mWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
miRepeatRequest(q);
return;
}
} else {
mHeu.evalTxChQuality(q->iv, true, (4 - 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))) {
miComplete(q->iv);
fastNext = false;
}
if(fastNext)
miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
else
closeRequest(q, true);
}
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))) {
miComplete(q->iv);
fastNext = false;
}
if(fastNext)
miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
else
closeRequest(q, true);
}
}
}
break;
}
break;
case States::CHECK_PACKAGE:
uint8_t framnr = 0;
if(0 == mMaxFrameId) {
uint8_t i = 0;
while(i < MAX_PAYLOAD_ENTRIES) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
i++;
case States::CHECK_PACKAGE:
uint8_t framnr = 0;
if(0 == mMaxFrameId) {
uint8_t i = 0;
while(i < MAX_PAYLOAD_ENTRIES) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
i++;
}
}
if(!framnr) {
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
if(!framnr) {
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
}
}
if(framnr) {
setAttempt();
if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("frame "));
DBGPRINT(String(framnr));
DBGPRINT(F(" missing: request retransmit ("));
DBGPRINT(String(q->attempts));
DBGPRINTLN(F(" attempts left)"));
}
if (!mIsRetransmit)
q->iv->mIsSingleframeReq = true;
sendRetransmit(q, (framnr-1));
mIsRetransmit = true;
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) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("frame "));
DBGPRINT(String(framnr));
DBGPRINT(F(" missing: request retransmit ("));
DBGPRINT(String(q->attempts));
DBGPRINTLN(F(" attempts left)"));
}
if (!mIsRetransmit)
q->iv->mIsSingleframeReq = true;
sendRetransmit(q, (framnr-1));
mIsRetransmit = true;
return;
}
compilePayload(q);
compilePayload(q);
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
(mCbPayload)(q->cmd, q->iv);
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
(mCbPayload)(q->cmd, q->iv);
closeRequest(q, true);
break;
}
});
closeRequest(q, true);
break;
}
}
private:
inline void printRxInfo(const queue_s *q, packet_t *p) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("RX "));
@ -267,7 +284,7 @@ class Communication : public CommQueue<> {
DBGPRINT(String(p->millis));
DBGPRINT(F("ms | "));
DBGPRINT(String(p->len));
if((IV_HM == q->iv->ivGen) || (IV_MI == q->iv->ivGen)) {
if(INV_RADIO_TYPE_NRF == q->iv->ivRadioType) {
DBGPRINT(F(" CH"));
if(3 == p->ch)
DBGPRINT(F("0"));
@ -292,16 +309,61 @@ class Communication : public CommQueue<> {
}
}
inline uint8_t getFramesExpected(const queue_s *q) {
if(q->isDevControl)
return 1;
if(q->iv->ivGen != IV_MI) {
if (q->cmd == RealTimeRunData_Debug) {
uint8_t framecnt[4] = {2, 3, 4, 7};
return framecnt[q->iv->type];
}
switch (q->cmd) {
case InverterDevInform_All:
case GetLossRate:
case SystemConfigPara:
return 1;
case AlarmData: return 0x0c;
case GridOnProFilePara: return 6;
/*HardWareConfig = 3, // 0x03
SimpleCalibrationPara = 4, // 0x04
RealTimeRunData_Reality = 12, // 0x0c
RealTimeRunData_A_Phase = 13, // 0x0d
RealTimeRunData_B_Phase = 14, // 0x0e
RealTimeRunData_C_Phase = 15, // 0x0f
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
RecordData = 19, // 0x13
InternalData = 20, // 0x14
GetSelfCheckState = 30, // 0x1e
*/
default: return 8; // for the moment, this should result in sth. like a default timeout of 500ms
}
} else { //MI
switch (q->cmd) {
case MI_REQ_CH1:
case MI_REQ_CH2:
return 2;
case 0x0f: return 3;
default: return 1;
}
}
}
inline bool validateIvSerial(uint8_t buf[], Inverter<> *iv) {
uint8_t tmp[4];
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;
}
}
@ -351,8 +413,13 @@ 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)) {
// 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;
@ -392,10 +459,11 @@ 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((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
(mCbPwrAck)(q->iv);
return accepted;
}
@ -487,6 +555,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();
@ -504,20 +573,18 @@ class Communication : public CommQueue<> {
}
void sendRetransmit(const queue_s *q, uint8_t i) {
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 {
//add(q, true);
closeRequest(q, false);
}
mFramesExpected = 1;
q->iv->radio->setExpectedFrames(mFramesExpected);
q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true);
q->iv->radioStatistics.retransmits++;
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
mState = States::WAIT;
}
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)
@ -535,7 +602,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("-----"));
}
@ -592,6 +658,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 ) {
@ -608,6 +676,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 "));
@ -650,22 +719,32 @@ 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;
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
/* 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) {
@ -709,21 +788,20 @@ 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
//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, (4 - 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
mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
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);
}
}
@ -737,16 +815,14 @@ 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++;
mFramesExpected = getFramesExpected(q);
q->iv->radio->setExpectedFrames(mFramesExpected);
q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true);
mWaitTime.startTimeMonitor(MI_TIMEOUT);
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
q->iv->miMultiParts = 0;
q->iv->mGotFragment = 0;
mIsRetransmit = true;
@ -766,8 +842,7 @@ class Communication : public CommQueue<> {
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
mWaitTime.startTimeMonitor(MI_TIMEOUT);
//mState = States::WAIT;
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
mIsRetransmit = false;
}
@ -833,6 +908,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)]) {
@ -851,6 +928,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));
@ -871,17 +968,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:
@ -905,14 +997,15 @@ class Communication : public CommQueue<> {
bool mFirstTry = false; // see, if we should do a second try
bool mIsRetransmit = false; // we already had waited one complete cycle
uint8_t mMaxFrameId;
uint8_t mFramesExpected = 12; // 0x8c was highest last frame for alarm data
uint16_t mTimeout = 0; // calculating that once should be ok
uint8_t mPayload[MAX_BUFFER];
payloadListenerType mCbPayload = NULL;
powerLimitAckListenerType mCbPwrAck = NULL;
alarmListenerType mCbAlarm = NULL;
Heuristic mHeu;
uint32_t mLastEmptyQueueMillis = 0;
bool mPrintSequenceDuration = false;
//States mDebugState = States::START;
};
#endif /*__COMMUNICATION_H__*/

11
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__
@ -76,6 +76,7 @@ enum {CMD_CALC = 0xffff};
enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
enum {INV_RADIO_TYPE_NRF = 0, INV_RADIO_TYPE_CMT};
#define WORK_FREQ_KHZ 865000 // desired work frequency between DTU and
// inverter in kHz
@ -86,6 +87,12 @@ enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
#define DURATION_ONEFRAME 50 // timeout parameter for each expected frame (ms)
//#define DURATION_RESERVE {90,120} // timeout parameter to still wait after last expected frame (ms)
#define DURATION_TXFRAME 85 // timeout parameter for first transmission and first expected frame (time to first channel switch from tx start!) (ms)
#define DURATION_LISTEN_MIN 5 // time to stay at least on a listening channel (ms)
#define DURATION_PAUSE_LASTFR 45 // how long to pause after last frame (ms)
const uint8_t duration_reserve[2] = {115,115};
typedef struct {
uint8_t fieldId; // field id

235
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://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_INVERTER_H__
@ -14,6 +14,7 @@
#define MAX_GRID_LENGTH 150
#include "hmDefines.h"
#include "../appInterface.h"
#include "HeuristicInv.h"
#include "../hms/hmsDefines.h"
#include <memory>
@ -64,13 +65,28 @@ struct calcFunc_t {
func_t<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<class T=float>
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,23 +110,16 @@ const calcFunc_t<T> calcFunctions[] = {
{ CALC_MPDC_CH, &calcMaxPowerDc }
};
enum class InverterStatus : uint8_t {
OFF,
STARTING,
PRODUCING,
WAS_PRODUCING,
WAS_ON
};
template <class REC_TYP>
class Inverter {
public:
uint8_t ivGen; // generation of inverter (HM / MI)
uint8_t ivRadioType; // refers to used radio (nRF24 / CMT)
cfgIv_t *config; // stored settings
uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index
uint16_t powerLimit[2]; // limit power output
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
@ -124,35 +133,32 @@ class Inverter {
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status
std::array<alarm_t, 10> 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
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 uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
//static IApp *app; // pointer to app interface
public:
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
@ -161,7 +167,6 @@ class Inverter {
alarmMesIndex = 0;
isConnected = false;
status = InverterStatus::OFF;
alarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
rssi = -127;
@ -171,10 +176,7 @@ class Inverter {
mIsSingleframeReq = false;
radio = NULL;
commEnabled = true;
mIvRxCnt = 0;
mIvTxCnt = 0;
mDtuRxCnt = 0;
mDtuTxCnt = 0;
tsMaxAcPower = 0;
memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5);
@ -187,36 +189,49 @@ 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 {
if(0 == getFwVersion())
cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
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 == 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
mGetLossInterval = 1;
cb(RealTimeRunData_Debug, false); // get live data
cb(GetLossRate, false);
} else
cb(RealTimeRunData_Debug, false); // get live data
}
} else { // MI
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 {
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
mIvRxCnt +=2;
} 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);
mGetLossInterval++;
if (type != INV_TYPE_4CH)
mIvRxCnt++; // statistics workaround...
}
}
}
}
@ -275,6 +290,7 @@ class Inverter {
if(isConnected) {
mDevControlRequest = true;
devControlCmd = cmd;
//app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
}
return isConnected;
}
@ -321,8 +337,8 @@ class Inverter {
}
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) {
if (alarmMesIndex < rec->record[pos]) {
@ -333,28 +349,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"));
@ -506,6 +521,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) {
@ -590,7 +606,7 @@ class Inverter {
void resetAlarms() {
lastAlarm.fill({0, 0, 0});
alarmNxtWrPos = 0;
mAlarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
@ -603,21 +619,44 @@ 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 (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
radioStatistics.ivSent = mDtuTxCnt;
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);
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.ivLoss));
DBGPRINT(F(" of "));
DBGPRINT(String(radioStatistics.ivSent));
DBGPRINT(F(", DTU loss: "));
DBGPRINT(String(radioStatistics.dtuLoss));
DBGPRINT(F(" of "));
if(mAckCount) {
DBGPRINT(String(radioStatistics.dtuSent));
DBGPRINT(F(". ACKs: "));
DBGPRINTLN(String(mAckCount));
mAckCount = 0;
} else
DBGPRINTLN(String(radioStatistics.dtuSent));
}
mIvRxCnt = rxCnt;
mIvTxCnt = txCnt;
mIvRxCnt = rxCnt;
mIvTxCnt = txCnt;
mDtuRxCnt = 0; // start new interval
mDtuTxCnt = 0; // start new interval
return true;
}
return false;
}
@ -789,9 +828,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) {
@ -809,6 +848,16 @@ class Inverter {
bool mDevControlRequest; // true if change needed
uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH];
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;
uint16_t mAckCount = 0;
};
template <class REC_TYP>
@ -920,8 +969,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;
}

209
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
//-----------------------------------------------------------------------------
@ -48,8 +48,8 @@ class HmRadio : public Radio {
pinMode(irq, INPUT_PULLUP);
mSerialDebug = serialDebug;
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrivacyMode = privacyMode;
mPrintWholeTrace = printWholeTrace;
generateDtuSn();
@ -78,21 +78,16 @@ class HmRadio : public Radio {
#else
mNrf24->begin(mSpi.get(), ce, cs);
#endif
mNrf24->setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
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);
mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&DTU_RADIO_ID));
// enable all receiving interrupts
mNrf24->maskIRQ(false, false, false);
mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts
mNrf24->setPALevel(1); // low is default
if(mNrf24->isChipConnected()) {
@ -104,62 +99,97 @@ class HmRadio : public Radio {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
void loop(void) {
if (!mIrqRcvd)
return; // nothing to do
mIrqRcvd = false;
bool tx_ok, tx_fail, rx_ready;
mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
mNrf24->flush_tx(); // empty TX FIFO
// start listening
uint8_t chOffset = 2;
mRxChIdx = (mTxChIdx + chOffset) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
// returns true if communication is active
bool loop(void) {
if (!mIrqRcvd && !mNRFisInRX)
return false; // first quick check => nothing to do at all here
if(NULL == mLastIv) // prevent reading on NULL object!
return;
return false;
uint32_t innerLoopTimeout = 55000;
uint32_t loopMillis = millis();
uint32_t outerLoopTimeout = (mLastIv->mIsSingleframeReq) ? 100 : ((mLastIv->mCmd != AlarmData) && (mLastIv->mCmd != GridOnProFilePara)) ? 400 : 600;
bool isRxInit = true;
if(!mIrqRcvd) { // no news from nRF, check timers
if ((millis() - mTimeslotStart) < innerLoopTimeout)
return true; // nothing to do, still waiting
if (mRadioWaitTime.isTimeout()) { // timeout reached!
mNRFisInRX = false;
return false;
}
while ((millis() - loopMillis) < outerLoopTimeout) {
uint32_t startMicros = micros();
while ((micros() - startMicros) < innerLoopTimeout) { // listen (4088us or?) 5110us to each channel
if (mIrqRcvd) {
mIrqRcvd = false;
// otherwise switch to next RX channel
mTimeslotStart = millis();
if(!mNRFloopChannels && ((mTimeslotStart - mLastIrqTime) > (DURATION_TXFRAME+DURATION_ONEFRAME)))
mNRFloopChannels = true;
if (getReceived()) { // everything received
return;
}
rxPendular = !rxPendular;
//innerLoopTimeout = (rxPendular ? 1 : 2)*DURATION_LISTEN_MIN;
innerLoopTimeout = DURATION_LISTEN_MIN;
innerLoopTimeout = 4088*5;
if (isRxInit) {
isRxInit = false;
if (micros() - startMicros < 42000) {
innerLoopTimeout = 4088*12;
mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
}
}
if(mNRFloopChannels)
tempRxChIdx = (tempRxChIdx + 4) % RF_CHANNELS;
else
tempRxChIdx = (mRxChIdx + rxPendular*4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[tempRxChIdx]);
isRxInit = false;
return true; // communicating, but changed RX channel
} else {
// here we got news from the nRF
mIrqRcvd = false;
mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
mLastIrqTime = millis();
startMicros = micros();
if(tx_ok || tx_fail) { // tx related interrupt, basically we should start listening
mNrf24->flush_tx(); // empty TX FIFO
mTxSetupTime = millis() - mMillis;
if(mNRFisInRX) {
DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
return false;
}
yield();
mNRFisInRX = true;
if(tx_ok)
mLastIv->mAckCount++;
mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
mTimeslotStart = millis();
tempRxChIdx = mRxChIdx;
rxPendular = false;
mNRFloopChannels = (mLastIv->ivGen == IV_MI);
innerLoopTimeout = DURATION_TXFRAME;
}
// switch to next RX channel
mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
innerLoopTimeout = 4088;
isRxInit = false;
if(rx_ready) {
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
mNrf24->stopListening();
} else {
innerLoopTimeout = DURATION_LISTEN_MIN;
mTimeslotStart = millis();
if (!mNRFloopChannels) {
//rxPendular = true; // stay longer on the next rx channel
if (isRxInit) {
isRxInit = false;
tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[tempRxChIdx]);
} else
mRxChIdx = tempRxChIdx;
}
}
return mNRFisInRX;
} /*else if(tx_fail) {
mNRFisInRX = false;
return false;
}*/
}
// not finished but time is over
return;
return false;
}
bool isChipConnected(void) {
@ -177,10 +207,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());
@ -249,6 +279,7 @@ class HmRadio : public Radio {
}
cnt++;
}
sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
}
@ -264,37 +295,40 @@ class HmRadio : public Radio {
private:
inline bool getReceived(void) {
bool tx_ok, tx_fail, rx_ready;
mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
bool isLastPackage = false;
rx_ready = false; // reset for ACK case
while(mNrf24->available()) {
uint8_t len;
len = mNrf24->getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
uint8_t len = mNrf24->getDynamicPayloadSize(); // payload size > 32 -> corrupt payload
if (len > 0) {
packet_t p;
p.ch = mRfChLst[mRxChIdx];
p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
p.ch = mRfChLst[tempRxChIdx];
p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
p.rssi = mNrf24->testRPD() ? -64 : -75;
p.millis = millis() - mMillis;
mNrf24->read(p.packet, p.len);
if (p.packet[0] != 0x00) {
if(!checkIvSerial(p.packet, mLastIv)) {
DPRINT(DBG_WARN, "RX other inverter ");
if(*mPrivacyMode)
ah::dumpBuf(p.packet, p.len, 1, 4);
else
if(!*mPrivacyMode)
ah::dumpBuf(p.packet, p.len);
return false;
} else {
mLastIv->mGotFragment = true;
mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
if(IV_MI == mLastIv->ivGen) {
if (p.packet[0] == (0x0f + ALL_FRAMES)) // response from MI get information command
isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore MI status messages //#0 was p.packet[0] != 0x00 &&
isLastPackage = true; // response from dev control command
}
rx_ready = true; //reset in case we first read messages from other inverter or ACK zero payloads
}
mLastIv->mGotFragment = true;
mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore MI status messages //#0 was p.packet[0] != 0x00 &&
isLastPackage = true; // response from dev control command
}
}
yield();
@ -312,6 +346,12 @@ class HmRadio : public Radio {
mTxChIdx = iv->heuristics.txRfChId;
if(*mSerialDebug) {
if(!isRetransmit) {
DPRINT(DBG_INFO, "last tx setup: ");
DBGPRINT(String(mTxSetupTime));
DBGPRINTLN("ms");
}
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX "));
DBGPRINT(String(len));
@ -340,6 +380,7 @@ class HmRadio : public Radio {
mLastIv = iv;
iv->mDtuTxCnt++;
mNRFisInRX = false;
}
uint64_t getIvId(Inverter<> *iv) {
@ -362,8 +403,18 @@ class HmRadio : public Radio {
uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0;
uint8_t tempRxChIdx = mRxChIdx;
bool mGotLastMsg = false;
uint32_t mMillis;
bool tx_ok, tx_fail, rx_ready = false;
unsigned long mTimeslotStart = 0;
unsigned long mLastIrqTime = 0;
bool mNRFloopChannels = false;
bool mNRFisInRX = false;
bool isRxInit = true;
bool rxPendular = false;
uint32_t innerLoopTimeout = DURATION_LISTEN_MIN;
uint8_t mTxSetupTime = 0;
std::unique_ptr<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24;

26
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 <functional>
@ -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<void(Inverter<> *iv)> cb) {
@ -49,15 +51,21 @@ class HmSystem {
}
if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04)
if((iv->config->serial.b[4] & 0x0f) == 0x04) {
iv->ivGen = IV_HMS;
else
iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else {
iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF;
}
}
else if((iv->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
iv->ivGen = IV_HM;
else // MI 2nd Gen
iv->ivRadioType = INV_RADIO_TYPE_NRF;
} else { // MI 2nd Gen
iv->ivGen = IV_MI;
iv->ivRadioType = INV_RADIO_TYPE_NRF;
}
} else if(iv->config->serial.b[5] == 0x13) {
iv->ivGen = IV_HMT;
iv->type = INV_TYPE_6CH;
@ -85,7 +93,7 @@ class HmSystem {
DBGPRINTLN(String(iv->config->serial.u64, HEX));
if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01))
DPRINTLN(DBG_WARN, F("MI Inverter are not fully supported now!!!"));
DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!"));
cb(iv);
}

15
src/hm/nrfHal.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://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __NRF_HAL_H__
@ -144,6 +144,8 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
if(len > NRF_MAX_TRANSFER_SZ)
len = NRF_MAX_TRANSFER_SZ;
memset(&data[1], 0xff, len);
request_spi();
@ -168,13 +170,16 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t data_len, uint8_t blank_len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
uint8_t len = data_len + blank_len;
data[0] = cmd;
memset(&data[1], 0xff, (data_len + blank_len));
if(len > (NRF_MAX_TRANSFER_SZ + 1))
len = (NRF_MAX_TRANSFER_SZ + 1);
memset(&data[1], 0xff, len);
request_spi();
size_t spiLen = (static_cast<size_t>(data_len) + static_cast<size_t>(blank_len) + 1u) << 3;
size_t spiLen = (static_cast<size_t>(len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,

40
src/hm/radio.h

@ -13,6 +13,9 @@
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../utils/timemonitor.h"
enum { IRQ_UNKNOWN = 0, IRQ_OK, IRQ_ERROR };
// forward declaration of class
template <class REC_TYP=float>
@ -25,11 +28,11 @@ class Radio {
virtual bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; }
virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; }
virtual bool isChipConnected(void) { return false; }
virtual void loop(void) {};
virtual bool loop(void) = 0;
void handleIntr(void) {
mIrqRcvd = true;
mIrqOk = IRQ_OK;
}
void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
@ -39,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;
}
@ -63,8 +68,14 @@ class Radio {
return mDtuSn;
}
void setExpectedFrames(uint8_t framesExpected) {
mFramesExpected = framesExpected;
}
public:
std::queue<packet_t> mBufCtrl;
uint8_t mIrqOk = IRQ_UNKNOWN;
TimeMonitor mRadioWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
@ -77,6 +88,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) {
@ -95,23 +108,26 @@ 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;
volatile bool mIrqRcvd;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
uint8_t mFramesExpected = 0x0c;
};
#endif /*__RADIO_H__*/

175
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 HMSYSTEM>
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__*/

2
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 {

10
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);

26
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
//-----------------------------------------------------------------------------
@ -26,15 +26,16 @@ class CmtRadio : public Radio {
mPrintWholeTrace = printWholeTrace;
}
void loop() {
bool loop() {
mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx))
return;
return false;
getRx();
if(CMT_SUCCESS == mCmt.goRx()) {
mIrqRcvd = false;
mRqstGetRx = false;
}
return false;
}
bool isChipConnected(void) {
@ -50,10 +51,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);
@ -134,8 +135,8 @@ class CmtRadio : public Radio {
mCmt.goRx();
}
mIrqRcvd = false;
mRqstGetRx = false;
mIrqRcvd = false;
mRqstGetRx = false;
}
inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
@ -163,11 +164,16 @@ class CmtRadio : public Radio {
uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi);
if(CMT_SUCCESS == status)
mBufCtrl.push(p);
// this code completly stops communication!
//if(p.packet[9] > ALL_FRAMES) // indicates last frame
// mRadioWaitTime.stopTimeMonitor(); // we got everything we expected and can exit rx mode...
//optionally instead: mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
}
CmtType mCmt;
bool mRqstGetRx;
bool mCmtAvail;
bool mRqstGetRx = false;
uint32_t mMillis;
};

286
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
@ -45,21 +46,66 @@ 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
[env:esp8266-de]
platform = espressif8266
board = esp12e
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
[env:esp8266-prometheus]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus-de]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-minimal]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8285]
platform = espressif8266
board = esp8285
@ -67,23 +113,79 @@ 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
[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
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
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
-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 =
esp32_exception_decoder
[env:esp32-wroom32-de]
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
[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
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
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
-DLANG_DE
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
@ -94,42 +196,172 @@ 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
[env:esp32-wroom32-ethernet-de]
platform = espressif32
board = esp32dev
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
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
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-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
[env:esp32-s2-mini-de]
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
-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
[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
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-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
[env:esp32-c3-mini-de]
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
-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
[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}
-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
#-DARDUINO_USB_CDC_ON_BOOT=1
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}
-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}
@ -152,7 +384,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
@ -162,13 +394,15 @@ 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
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
@ -194,20 +428,33 @@ build_flags = ${env.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-dev]
platform = espressif32@6.4.0
[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
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}
-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
-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
@ -223,7 +470,6 @@ 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
monitor_filters =
esp32_exception_decoder, colorize

399
src/plugins/Display/Display.h

@ -1,6 +1,8 @@
#ifndef __DISPLAY__
#define __DISPLAY__
#if defined(PLUGIN_DISPLAY)
#include <Timezone.h>
#include <U8g2lib.h>
@ -17,221 +19,228 @@
template <class HMSYSTEM, class RADIO>
class Display {
public:
Display() {
mMono = NULL;
}
void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
mApp = app;
mHmRadio = hmradio;
mHmsRadio = hmsradio;
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
mNewPayload = false;
mLoopCnt = 0;
mDisplayData.version = app->getVersion(); // version never changes, so only set once
switch (mCfg->type) {
case 0: mMono = NULL; break; // None
case 1: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
case 2: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
case 3: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
case 4: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
case 5: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case 6: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) && !defined(ETHERNET)
case 10:
mMono = NULL; // ePaper does not use this
mRefreshCycle = 0;
mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
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);
mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData);
public:
Display() {
mMono = NULL;
}
// setup PIR pin for motion sensor
#ifdef ESP32
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF))
pinMode(mCfg->pirPin, INPUT);
#endif
#ifdef ESP8266
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF) && (mCfg->pirPin != A0))
pinMode(mCfg->pirPin, INPUT);
#endif
void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
mApp = app;
mHmRadio = hmradio;
mHmsRadio = hmsradio;
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
mNewPayload = false;
mLoopCnt = 0;
}
mDisplayData.version = app->getVersion(); // version never changes, so only set once
switch (mCfg->type) {
case DISP_TYPE_T0_NONE: mMono = NULL; break; // None
case DISP_TYPE_T1_SSD1306_128X64: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
case DISP_TYPE_T2_SH1106_128X64: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
case DISP_TYPE_T3_PCD8544_84X48: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) && !defined(ETHERNET)
case DISP_TYPE_T10_EPAPER:
mMono = NULL; // ePaper does not use this
mRefreshCycle = 0;
mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
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);
mMono->init(&mDisplayData);
}
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
// setup PIR pin for motion sensor
#ifdef ESP32
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF))
pinMode(mCfg->pirPin, INPUT);
#endif
#ifdef ESP8266
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF) && (mCfg->pirPin != A0))
pinMode(mCfg->pirPin, INPUT);
#endif
void tickerSecond() {
if (mMono != NULL)
mMono->loop(mCfg->contrast, motionSensorActive());
}
if (mNewPayload || (((++mLoopCnt) % 5) == 0)) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
#if defined(ESP32) && !defined(ETHERNET)
mEpaper.tickerSecond();
#endif
}
private:
void DataScreen() {
if (mCfg->type == 0)
return;
float totalPower = 0.0;
float totalYieldDay = 0.0;
float totalYieldTotal = 0.0;
uint8_t nrprod = 0;
uint8_t nrsleep = 0;
int8_t minQAllInv = 4;
Inverter<> *iv;
record_t<> *rec;
bool allOff = true;
uint8_t nInv = mSys->getNumInverters();
for (uint8_t i = 0; i < nInv; i++) {
iv = mSys->getInverterByPos(i);
if (iv == NULL)
continue;
if (iv->isProducing()) // also updates inverter state engine
nrprod++;
else
nrsleep++;
void tickerSecond() {
bool request_refresh = false;
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (mMono != NULL)
request_refresh = mMono->loop(motionSensorActive());
if (iv->isAvailable()) { // consider only radio quality of inverters still communicating
int8_t maxQInv = -6;
for(uint8_t ch = 0; ch < RF_MAX_CHANNEL_ID; ch++) {
int8_t q = iv->heuristics.txRfQuality[ch];
if (q > maxQInv)
maxQInv = q;
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
}
#if defined(ESP32) && !defined(ETHERNET)
mEpaper.tickerSecond();
#endif
}
private:
void DataScreen() {
if (DISP_TYPE_T0_NONE == mCfg->type)
return;
float totalPower = 0.0;
float totalYieldDay = 0.0;
float totalYieldTotal = 0.0;
uint8_t nrprod = 0;
uint8_t nrsleep = 0;
int8_t minQAllInv = 4;
Inverter<> *iv;
record_t<> *rec;
bool allOff = true;
uint8_t nInv = mSys->getNumInverters();
for (uint8_t i = 0; i < nInv; i++) {
iv = mSys->getInverterByPos(i);
if (iv == NULL)
continue;
if (iv->isProducing()) // also updates inverter state engine
nrprod++;
else
nrsleep++;
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv->isAvailable()) { // consider only radio quality of inverters still communicating
int8_t maxQInv = -6;
for(uint8_t ch = 0; ch < RF_MAX_CHANNEL_ID; ch++) {
int8_t q = iv->heuristics.txRfQuality[ch];
if (q > maxQInv)
maxQInv = q;
}
if (maxQInv < minQAllInv)
minQAllInv = maxQInv;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
allOff = false;
}
if (maxQInv < minQAllInv)
minQAllInv = maxQInv;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
allOff = false;
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
if (allOff)
minQAllInv = -6;
// prepare display data
mDisplayData.nrProducing = nrprod;
mDisplayData.nrSleeping = nrsleep;
mDisplayData.totalPower = totalPower;
mDisplayData.totalYieldDay = totalYieldDay;
mDisplayData.totalYieldTotal = totalYieldTotal;
bool nrf_en = mApp->getNrfEnabled();
bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
#if defined(ESP32)
bool cmt_en = mApp->getCmtEnabled();
bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
#else
bool cmt_en = false;
bool cmt_ok = false;
#endif
mDisplayData.RadioSymbol = (nrf_ok && !cmt_en) || (cmt_ok && !nrf_en) || (nrf_ok && cmt_ok);
mDisplayData.WifiSymbol = (WiFi.status() == WL_CONNECTED);
mDisplayData.MQTTSymbol = mApp->getMqttIsConnected();
mDisplayData.RadioRSSI = ivQuality2RadioRSSI(minQAllInv); // Workaround as NRF24 has no RSSI. Approximation by quality levels from heuristic function
mDisplayData.WifiRSSI = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : SCHAR_MIN;
mDisplayData.ipAddress = WiFi.localIP();
// provide localized times to display classes
time_t utc= mApp->getTimestamp();
if (year(utc) > 2020)
mDisplayData.utcTs = gTimezone.toLocal(utc);
else
mDisplayData.utcTs = 0;
mDisplayData.pGraphStartTime = gTimezone.toLocal(mApp->getSunrise());
mDisplayData.pGraphEndTime = gTimezone.toLocal(mApp->getSunset());
if (allOff)
minQAllInv = -6;
// prepare display data
mDisplayData.nrProducing = nrprod;
mDisplayData.nrSleeping = nrsleep;
mDisplayData.totalPower = totalPower;
mDisplayData.totalYieldDay = totalYieldDay;
mDisplayData.totalYieldTotal = totalYieldTotal;
bool nrf_en = mApp->getNrfEnabled();
bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
#if defined(ESP32)
bool cmt_en = mApp->getCmtEnabled();
bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
#else
bool cmt_en = false;
bool cmt_ok = false;
#endif
mDisplayData.RadioSymbol = (nrf_ok && !cmt_en) || (cmt_ok && !nrf_en) || (nrf_ok && cmt_ok);
mDisplayData.WifiSymbol = (WiFi.status() == WL_CONNECTED);
mDisplayData.MQTTSymbol = mApp->getMqttIsConnected();
mDisplayData.RadioRSSI = ivQuality2RadioRSSI(minQAllInv); // Workaround as NRF24 has no RSSI. Approximation by quality levels from heuristic function
mDisplayData.WifiRSSI = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : SCHAR_MIN;
mDisplayData.ipAddress = WiFi.localIP();
time_t utc= mApp->getTimestamp();
if (year(utc) > 2020)
mDisplayData.utcTs = utc;
else
mDisplayData.utcTs = 0;
if (mMono ) {
mMono->disp();
}
#if defined(ESP32) && !defined(ETHERNET)
else if (mCfg->type == 10) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++;
}
if (mMono ) {
mMono->disp();
}
#if defined(ESP32) && !defined(ETHERNET)
else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++;
}
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
}
#endif
}
#endif
}
bool motionSensorActive() {
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
#if defined(ESP8266)
if (mCfg->pirPin == A0)
return((analogRead(A0) >= 512));
else
bool motionSensorActive() {
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
#if defined(ESP8266)
if (mCfg->pirPin == A0)
return((analogRead(A0) >= 512));
else
return(digitalRead(mCfg->pirPin));
#elif defined(ESP32)
return(digitalRead(mCfg->pirPin));
#elif defined(ESP32)
return(digitalRead(mCfg->pirPin));
#endif
#endif
}
else
return(false);
}
else
return(false);
}
// approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
int8_t ivQuality2RadioRSSI(int8_t invQuality) {
int8_t pseudoRSSIdB;
switch(invQuality) {
case 4: pseudoRSSIdB = -55; break;
case 3:
case 2:
case 1: pseudoRSSIdB = -65; break;
case 0:
case -1:
case -2: pseudoRSSIdB = -75; break;
case -3:
case -4:
case -5: pseudoRSSIdB = -85; break;
case -6:
default: pseudoRSSIdB = -95; break;
// approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
int8_t ivQuality2RadioRSSI(int8_t invQuality) {
int8_t pseudoRSSIdB;
switch(invQuality) {
case 4: pseudoRSSIdB = -55; break;
case 3:
case 2:
case 1: pseudoRSSIdB = -65; break;
case 0:
case -1:
case -2: pseudoRSSIdB = -75; break;
case -3:
case -4:
case -5: pseudoRSSIdB = -85; break;
case -6:
default: pseudoRSSIdB = -95; break;
}
return (pseudoRSSIdB);
}
return (pseudoRSSIdB);
}
// private member variables
IApp *mApp;
DisplayData mDisplayData;
bool mNewPayload;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
display_t *mCfg;
HMSYSTEM *mSys;
RADIO *mHmRadio;
RADIO *mHmsRadio;
uint16_t mRefreshCycle;
#if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper;
#endif
DisplayMono *mMono;
// private member variables
IApp *mApp;
DisplayData mDisplayData;
bool mNewPayload;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
display_t *mCfg;
HMSYSTEM *mSys;
RADIO *mHmRadio;
RADIO *mHmsRadio;
uint16_t mRefreshCycle;
#if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper;
#endif
DisplayMono *mMono;
};
#endif /*PLUGIN_DISPLAY*/
#endif /*__DISPLAY__*/

381
src/plugins/Display/Display_Mono.h

@ -20,101 +20,314 @@
#include "Display_data.h"
#include "../../utils/dbg.h"
#include "../../utils/timemonitor.h"
#include "../../config/settings.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) = 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) {
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(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(bool motion) {
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);
}
}
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);
}
}
}
if(mLuminance != lum) {
mLuminance = lum;
if(mLuminance != mCfg->contrast) {
mLuminance = mCfg->contrast;
mDisplay->setContrast(mLuminance);
}
return(monoMaintainDispSwitchState()); // return flag, if display content should be updated immediately
}
protected:
enum class DispSwitchState {
TEXT,
GRAPH
};
protected:
// Common initialization function to be called by subclasses
void monoInit(U8G2* display, DisplayData *displayData) {
mDisplay = display;
mDisplayData = displayData;
mDisplay->begin();
mDisplay->setPowerSave(false); // always start with display on
mDisplay->setContrast(mLuminance);
}
}
protected:
U8G2* mDisplay;
DisplayData *mDisplayData;
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 mLoopCnt;
uint8_t mLineXOffsets[5] = {};
uint8_t mLineYOffsets[5] = {};
uint8_t mExtra;
int8_t mPixelshift=0;
TimeMonitor mDisplayTime = TimeMonitor(1000 * 15, true);
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);
mDisplay->clearBuffer();
mDispWidth = mDisplay->getDisplayWidth();
mDispHeight = mDisplay->getDisplayHeight();
}
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;
}
mDisplay->clearBuffer();
mDispWidth = mDisplay->getDisplayWidth();
mDispHeight = mDisplay->getDisplayHeight();
mDispSwitchTime.stopTimeMonitor();
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
}
// pixelshift screensaver with wipe effect
void calcPixelShift(int range) {
int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
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();
}
// add new value to power graph and maintain state engine for period times
void addPowerGraphEntry(float val) {
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 = true; // 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 = true; // 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;
}
// 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
}
// store new value to mPgData
if (store_entry) {
mPgLastTime = mDisplayData->utcTs; // time of last datapoint
mPgLastPos = std::min((uint8_t) sss2PgPos(mPgLastTime - mPgStartTime), (uint8_t) (mPgWidth - 1)); // last datapoint based on seconds since start
mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values (= envelope curve)
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;
// 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 ((0 == mPgStartTime) || (0 == mPgEndTime) || (0 == mPgLastTime) || (0 == mPgLastPos) || (mPgMaxPwr < 1))
return;
// draw X scale
tmElements_t tm;
breakTime(mPgEndTime, tm);
uint8_t endHourPg = tm.Hour; // absolute last hour in diagram
breakTime(mPgLastTime, tm);
uint8_t endHour = std::min(endHourPg, tm.Hour); // last hour of current data point in scaled diagram
breakTime(mPgStartTime, 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) - mPgStartTime)); // scale horizontal axis
if (12 == tm.Hour) {
mDisplay->drawLine(xoff + x_pos_screen, yoff, xoff + x_pos_screen, yoff - 2); // mark noon
mDisplay->drawLine(xoff + x_pos_screen - 1, yoff - 1, xoff + x_pos_screen + 1, yoff - 1);
}
else
mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1);
}
// draw Y scale
uint16_t scale_y = 10;
uint32_t maxpwr_int = static_cast<uint32_t>(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<uint8_t>(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 - getPowerGraphValueYpos(i - 1),
xoff + getPowerGraphXpos(i), yoff - getPowerGraphValueYpos(i));
}
// print max power value
mDisplay->setFont(u8g2_font_4x6_tr);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast<uint16_t>(std::round(mPgMaxPwr)));
mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText);
}
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;
}
// reset power graph
void resetPowerGraph() {
if (mPgData != nullptr) {
mPgMaxPwr = 0.0;
mPgLastPos = 0;
mPgLastTime = 0;
for (uint8_t i = 0; i < mPgWidth; i++) {
mPgData[i] = 0.0;
}
}
}
// 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;
}
// 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
else
return 0;
}
// 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
else
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 mPgStartTime = 0;
uint32_t mPgEndTime = 0;
uint32_t mPgPeriod = 0; // seconds
uint8_t mPgLastPos = 0;
uint32_t mPgLastTime = 0;
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
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
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"

18
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
//-----------------------------------------------------------------------------
@ -12,15 +12,13 @@ class DisplayMono128X32 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
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);
@ -58,7 +56,7 @@ class DisplayMono128X32 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@ -109,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);

217
src/plugins/Display/Display_Mono_128X64.h

@ -4,6 +4,7 @@
//-----------------------------------------------------------------------------
#pragma once
#include "Display.h"
#include "Display_Mono.h"
class DisplayMono128X64 : public DisplayMono {
@ -12,27 +13,54 @@ class DisplayMono128X64 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
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 DISP_TYPE_T1_SSD1306_128X64:
monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
case DISP_TYPE_T2_SH1106_128X64:
monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
case DISP_TYPE_T6_SSD1309_128X64:
default:
monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
}
calcLinePositions();
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;
break;
case 1:
monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 2;
graph_last_line = 3;
break;
case 2:
monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 1;
graph_last_line = 3;
break;
case 6:
case 3:
graph_first_line = 2;
graph_last_line = 4;
break;
case 4:
default:
monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 1;
graph_last_line = 4;
break;
}
calcLinePositions();
widthShrink = (mCfg->screenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver
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);
@ -61,106 +89,117 @@ 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);
}
// add new power data to power graph
if (mDisplayData->nrProducing > 0)
addPowerGraphEntry(mDisplayData->totalPower);
// print Date and time
if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
printText(ah::getDateTimeStrShort(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 {
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
}
// 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 (showLine(l_TotalPower)) {
// 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->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);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 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 (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
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 (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);
}
if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
// plot power graph
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, 4 - i, 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 - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, 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->sendBuffer();
mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - (widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
mDisplay->sendBuffer();
mExtra++;
@ -184,7 +223,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;
@ -198,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+=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);
}
@ -226,4 +269,8 @@ class DisplayMono128X64 : public DisplayMono {
dispX += mPixelshift;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
bool showLine(uint8_t line) {
return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
}
};

16
src/plugins/Display/Display_Mono_64X48.h

@ -12,16 +12,14 @@ class DisplayMono64X48 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
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);
@ -60,7 +58,7 @@ class DisplayMono64X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@ -98,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);

165
src/plugins/Display/Display_Mono_84X48.h

@ -1,11 +1,10 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#pragma once
#include "Display_Mono.h"
#include "../../utils/dbg.h"
class DisplayMono84X48 : public DisplayMono {
public:
@ -13,16 +12,43 @@ class DisplayMono84X48 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
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(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;
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;
}
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);
printText(mDisplayData->version, l_Version, 0xff);
@ -45,66 +71,85 @@ 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
if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 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);
}
}
// 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);
if (showLine(l_TotalPower)) {
// 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 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
if (showLine(l_YieldDay)) {
// 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, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping);
printText(mFmtText, l_Status, 0xff);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, l_YieldDay, 0xff);
}
// print yields
printText("\x88", l_YieldDay, 10); // day symbol
printText("\x83", l_YieldTotal, 10); // 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 (showLine(l_YieldTotal)) {
// 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);
}
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 ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
// plot power graph
plotPowerGraph(8, mLineYOffsets[graph_last_line] - 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
@ -139,6 +184,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;
@ -150,7 +198,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 +206,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);
@ -174,6 +223,10 @@ class DisplayMono84X48 : public DisplayMono {
dispX = col;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
bool showLine(uint8_t line) {
return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
}
};

28
src/plugins/Display/Display_data.h

@ -4,19 +4,21 @@
#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 (localized utc unix time). 0 = time is not synchonized
uint32_t pGraphStartTime=0; // localized starttime for power graph (e.g. sunRise)
uint32_t pGraphEndTime=0; // localized endttime 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
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__*/

23
src/plugins/Display/Display_ePaper.cpp

@ -7,6 +7,7 @@
#endif
#include "../../utils/helper.h"
#include "imagedata.h"
#include "defines.h"
#if defined(ESP32)
@ -19,6 +20,7 @@ SPIClass hspi(HSPI);
DisplayEPaper::DisplayEPaper() {
mDisplayRotation = 2;
mHeadFootPadding = 16;
memset(_fmtText, 0, EPAPER_MAX_TEXT_LEN);
}
@ -29,7 +31,7 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u
mRefreshState = RefreshStatus::LOGO;
mSecondCnt = 0;
if (type == 10) {
if (DISP_TYPE_T10_EPAPER == type) {
Serial.begin(115200);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
@ -65,6 +67,7 @@ void DisplayEPaper::refreshLoop() {
case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
_display->display(false); // full update
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
break;
@ -112,9 +115,9 @@ void DisplayEPaper::headlineIP() {
do {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str());
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
} else {
snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected");
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -135,7 +138,7 @@ void DisplayEPaper::lastUpdatePaged() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -156,7 +159,7 @@ void DisplayEPaper::versionFooter() {
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK);
do {
snprintf(_fmtText, sizeof(_fmtText), "Version: %s", _version);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -177,7 +180,7 @@ void DisplayEPaper::offlineFooter() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
snprintf(_fmtText, sizeof(_fmtText), "offline");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -201,13 +204,13 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
do {
// actual Production
if (totalPower > 9999) {
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 1000));
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.1f kW", (totalPower / 1000));
_changed = true;
} else if ((totalPower > 0) && (totalPower <= 9999)) {
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
_changed = true;
} else
snprintf(_fmtText, sizeof(_fmtText), "offline");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
@ -262,7 +265,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
// Inverter online
_display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10);
snprintf(_fmtText, sizeof(_fmtText), " %d online", isprod);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;

6
src/plugins/Display/Display_ePaper.h

@ -9,11 +9,11 @@
// enable GxEPD2_GFX base class
#define ENABLE_GxEPD2_GFX 1
#include <GxEPD2_3C.h>
#define EPAPER_MAX_TEXT_LEN 35
#include <GxEPD2_BW.h>
#include <SPI.h>
#include <map>
// FreeFonts from Adafruit_GFX
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
@ -51,7 +51,7 @@ class DisplayEPaper {
uint8_t mDisplayRotation;
bool _changed = false;
char _fmtText[35];
char _fmtText[EPAPER_MAX_TEXT_LEN];
String _settedIP;
uint8_t mHeadFootPadding;
GxEPD2_GFX* _display;

128
src/plugins/history.h

@ -0,0 +1,128 @@
//-----------------------------------------------------------------------------
// 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__
#if defined(ENABLE_HISTORY)
#include <array>
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#define HISTORY_DATA_ARR_LENGTH 256
enum class HistoryStorageType : uint8_t {
POWER,
YIELD
};
template<class HMSYSTEM>
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<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> 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((++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) {
storage_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;
storage_t mCurPwr;
storage_t mYieldDay;
bool mDayStored = false;
uint16_t mMaximumDay = 0;
};
#endif /*ENABLE_HISTORY*/
#endif /*__HISTORY_DATA_H__*/

47
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
//-----------------------------------------------------------------------------
@ -8,6 +8,7 @@
#ifndef __PUB_MQTT_H__
#define __PUB_MQTT_H__
#if defined(ENABLE_MQTT)
#ifdef ESP8266
#include <ESP8266WiFi.h>
#elif defined(ESP32)
@ -47,7 +48,6 @@ class PubMqtt {
memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS);
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4);
mLastAnyAvail = false;
mZeroValues = false;
}
~PubMqtt() { }
@ -74,8 +74,8 @@ class PubMqtt {
if(strlen(mCfgMqtt->clientId) > 0)
snprintf(mClientId, 23, "%s", mCfgMqtt->clientId);
else{
snprintf(mClientId, 24, "%s-", mDevName);
else {
snprintf(mClientId, 23, "%s-", mDevName);
uint8_t pos = strlen(mClientId);
mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0];
mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0];
@ -138,14 +138,14 @@ class PubMqtt {
#endif
}
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) {
bool tickerSun(uint32_t sunrise, uint32_t sunset, int16_t offsM, int16_t offsE, bool isSunrise = false) {
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++) {
@ -157,13 +157,17 @@ 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 + 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;
}
void notAvailChanged(bool allNotAvail) {
if(!allNotAvail)
mSendIvData.resetYieldDay();
}
bool tickerComm(bool disabled) {
if (!mClient.connected())
return false;
@ -243,10 +247,6 @@ class PubMqtt {
}
}
void setZeroValuesEnable(void) {
mZeroValues = true;
}
private:
void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
@ -312,22 +312,25 @@ class PubMqtt {
bool limitAbs = false;
if(len > 0) {
char *pyld = new char[len + 1];
strncpy(pyld, (const char*)payload, len);
memcpy(pyld, 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) {
if(('/' == p[pos]) || ('\0' == p[pos])) {
strncpy(tmp, p, pos);
memcpy(tmp, p, pos);
tmp[pos] = '\0';
switch(elm++) {
case 1: root[F("path")] = String(tmp); break;
@ -337,8 +340,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;
@ -594,8 +596,7 @@ class PubMqtt {
if(mSendList.empty())
return;
mSendIvData.start(mZeroValues);
mZeroValues = false;
mSendIvData.start();
mLastAnyAvail = anyAvail;
}
@ -614,7 +615,6 @@ class PubMqtt {
std::array<bool, MAX_NUM_INVERTERS> mSendAlarm{};
subscriptionCb mSubscriptionCb;
bool mLastAnyAvail;
bool mZeroValues;
InverterStatus mLastIvState[MAX_NUM_INVERTERS];
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
uint16_t mIntervalTimeout;
@ -630,4 +630,5 @@ class PubMqtt {
discovery_t mDiscovery;
};
#endif /*ENABLE_MQTT*/
#endif /*__PUB_MQTT_H__*/

152
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
//-----------------------------------------------------------------------------
@ -22,20 +22,19 @@ template<class HMSYSTEM>
class PubMqttIvData {
public:
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
mZeroValues = false;
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
mYldTotalStore = 0;
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t));
mRTRDataHasBeenSent = false;
mTable[IDLE] = &PubMqttIvData::stateIdle;
mTable[START] = &PubMqttIvData::stateStart;
mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv;
mTable[SEND_DATA] = &PubMqttIvData::stateSend;
mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals;
mTable[IDLE] = &PubMqttIvData::stateIdle;
mTable[START] = &PubMqttIvData::stateStart;
mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv;
mTable[SEND_DATA] = &PubMqttIvData::stateSend;
mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals;
}
void loop() {
@ -43,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;
@ -102,7 +104,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,15 +114,20 @@ 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) {
if(mYldTotalStore > mTotal[2])
mSendTotalYd = false; // don't send yield total if last value was greater
else
mYldTotalStore = mTotal[2];
mState = SEND_TOTALS;
else {
} else {
mSendList->pop();
mZeroValues = false;
mState = START;
}
}
@ -132,75 +139,67 @@ 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(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::OFF) {
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: {
mTotal[2] += mIv->getValue(mPos, rec);
break;
}
case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec);
break;
}
} 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);
}
} else
mAllTotalFound = false;
}
mPos++;
} else {
}
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 {
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
sendRadioStat(rec->length);
mState = FIND_NXT_IV;
rec->mqttSentStatus = MqttSentStatus::DATA_SENT;
}
} else
mState = FIND_NXT_IV;
}
}
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.ivLoss,
mIv->radioStatistics.ivSent,
mIv->radioStatistics.dtuLoss,
mIv->radioStatistics.dtuSent);
mPublish(mSubTopic, mVal, false, QOS_0);
}
@ -240,7 +239,6 @@ class PubMqttIvData {
mPos++;
} else {
mSendList->pop();
mZeroValues = false;
mPos = 0;
mState = IDLE;
}
@ -255,16 +253,14 @@ 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;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[100];
bool mZeroValues; // makes sure that yield day is sent even if no inverter is online
char mVal[140];
std::queue<sendListCmdIv> *mSendList;
};

6
src/utils/crc.cpp

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2022 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
//-----------------------------------------------------------------------------
#include "crc.h"
@ -19,7 +19,7 @@ namespace ah {
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) {
uint16_t crc = start;
uint8_t shift = 0;
uint8_t shift;
for(uint8_t i = 0; i < len; i ++) {
crc = crc ^ buf[i];

6
src/utils/helper.cpp

@ -72,11 +72,13 @@ namespace ah {
String getTimeStrMs(uint64_t t) {
char str[13];
uint16_t m;
if(0 == t)
sprintf(str, "n/a");
else {
t = (t + (millis() % 1000)) / 1000;
sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), millis() % 1000);
m = t % 1000;
t = t / 1000;
sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), m);
}
return String(str);
}

8
src/utils/improv.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
//-----------------------------------------------------------------------------
@ -72,10 +72,10 @@ class Improv {
void dumpBuf(uint8_t buf[], uint8_t len) {
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i], false);
DBGPRINT(" ", false);
DHEX(buf[i]);
DBGPRINT(F(" "));
}
DBGPRINTLN("", false);
DBGPRINTLN("");
}
inline uint8_t buildChecksum(uint8_t buf[], uint8_t len) {

14
src/utils/scheduler.h

@ -1,7 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Lukas Pusch, lukas@lpusch.de
// 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 __SCHEDULER_H__
@ -34,13 +33,13 @@ namespace ah {
void setup(bool directStart) {
mUptime = 0;
mTimestamp = (directStart) ? 1 : 0;
mTsMillis = 0;
mMax = 0;
mPrevMillis = millis();
mTsMillis = mPrevMillis % 1000;
resetTicker();
}
void loop(void) {
virtual void loop(void) {
mMillis = millis();
mDiff = mMillis - mPrevMillis;
if (mDiff < 1000)
@ -62,7 +61,7 @@ namespace ah {
mUptime += mDiffSeconds;
if(0 != mTimestamp) {
mTimestamp += mDiffSeconds;
mTsMillis = mMillis % 1000;
mTsMillis = mPrevMillis % 1000;
}
checkTicker();
@ -80,7 +79,6 @@ namespace ah {
virtual void setTimestamp(uint32_t ts) {
mTimestamp = ts;
mTsMillis = millis() % 1000;
}
bool resetEveryById(uint8_t id) {
@ -121,7 +119,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;

9
src/utils/syslog.cpp

@ -1,3 +1,8 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#include <algorithm>
#include "syslog.h"
@ -87,7 +92,7 @@ void DbgSyslog::log(const char *hostname, uint8_t facility, uint8_t severity, ch
// This is a unit8 instead of a char because that's what udp.write() wants
uint8_t buffer[SYSLOG_MAX_PACKET_SIZE];
int len = snprintf((char*)buffer, SYSLOG_MAX_PACKET_SIZE, "<%d>%s %s: %s", priority, hostname, SYSLOG_APP, msg);
int len = snprintf(static_cast<char*>(buffer), SYSLOG_MAX_PACKET_SIZE, "<%d>%s %s: %s", priority, hostname, SYSLOG_APP, msg);
//printf("syslog::log %s\n",mSyslogIP.toString().c_str());
//printf("syslog::log %d %s\n",len,buffer);
// Send the raw UDP packet
@ -96,4 +101,4 @@ void DbgSyslog::log(const char *hostname, uint8_t facility, uint8_t severity, ch
mSyslogUdp.endPacket();
}
#endif
#endif

15
src/utils/timemonitor.h

@ -20,16 +20,14 @@
class TimeMonitor {
public:
/**
* A constructor for initializing a TimeMonitor
* @note TimeMonitor witch default constructor is stopped
* A constructor for creating a TimeMonitor object
*/
TimeMonitor(void) {}
/**
* A constructor for initializing a TimeMonitor
* A constructor for initializing a TimeMonitor object
* @param timeout timeout in ms
* @param start (optional) if true, start TimeMonitor immediately
* @note TimeMonitor witch default constructor is stopped
*/
TimeMonitor(uint32_t timeout, bool start = false) {
if (start)
@ -50,7 +48,8 @@ class TimeMonitor {
/**
* Restart the TimeMonitor with already set timeout configuration
* @note returns nothing
* @note a timeout has to be set before, no need to call
* 'startTimeMonitor' before
*/
void reStartTimeMonitor(void) {
mStartTime = millis();
@ -82,7 +81,7 @@ class TimeMonitor {
* false: TimeMonitor still in time or TimeMonitor was stopped
*/
bool isTimeout(void) {
if ((mStarted) && (millis() - mStartTime >= mTimeout))
if ((mStarted) && ((millis() - mStartTime) >= mTimeout))
return true;
else
return false;
@ -104,7 +103,7 @@ class TimeMonitor {
*/
uint32_t getResidualTime(void) const {
uint32_t delayed = millis() - mStartTime;
return(mStarted ? (delayed < mTimeout ? mTimeout - delayed : 0UL) : 0xFFFFFFFFUL);
return(mStarted ? (delayed < mTimeout ? (mTimeout - delayed) : 0UL) : 0xFFFFFFFFUL);
}
/**
@ -123,4 +122,4 @@ class TimeMonitor {
bool mStarted = false; // start/stop state of the TimeMonitor
};
#endif
#endif

162
src/web/RestApi.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __WEB_API_H__
@ -15,9 +15,12 @@
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#include "lang.h"
#include "AsyncJson.h"
#include "ESPAsyncWebServer.h"
#include "plugins/history.h"
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
@ -96,8 +99,11 @@ 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 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());
@ -163,15 +169,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;
}
}
@ -190,8 +196,16 @@ class RestApi {
ep[F("generic")] = url + F("generic");
ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup");
#if !defined(ETHERNET)
ep[F("setup/networks")] = url + F("setup/networks");
ep[F("setup/getip")] = url + F("setup/getip");
#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
}
@ -242,10 +256,14 @@ 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);
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
obj[F("cst_lnk")] = String(mConfig->plugin.customLink);
obj[F("cst_lnk_txt")] = String(mConfig->plugin.customLinkText);
#if defined(ESP32)
obj[F("esp_type")] = F("ESP32");
@ -314,7 +332,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("<a href=\"/factory\" class=\"btn\">AhoyFactory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
char tmp[200];
snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT);
obj[F("html")] = String(tmp);
}
void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) {
@ -391,7 +411,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);
@ -401,12 +421,16 @@ 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) {
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;
@ -459,7 +483,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;
}
@ -477,6 +501,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");
@ -521,7 +546,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;
}
@ -544,7 +569,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;
}
@ -596,7 +621,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) {
@ -606,8 +632,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;
}
@ -627,6 +654,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
@ -637,6 +665,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;
}
}
@ -657,18 +686,20 @@ 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_graph_size")] = (uint8_t)mConfig->plugin.display.graph_size;
obj[F("disp_clk")] = mConfig->plugin.display.disp_clk;
obj[F("disp_data")] = mConfig->plugin.display.disp_data;
obj[F("disp_cs")] = mConfig->plugin.display.disp_cs;
obj[F("disp_dc")] = mConfig->plugin.display.disp_dc;
obj[F("disp_rst")] = mConfig->plugin.display.disp_reset;
obj[F("disp_bsy")] = mConfig->plugin.display.disp_busy;
obj[F("pir_pin")] = mConfig->plugin.display.pirPin;
}
void getMqttInfo(JsonObject obj) {
@ -681,10 +712,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;
@ -709,14 +741,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) {
@ -740,6 +768,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) {
@ -765,11 +796,47 @@ 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++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
obj[F("value")][fld] = value;
if (value > max)
max = value;
}
obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay();
#else
obj[F("refresh")] = 86400; // 1 day;
#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++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld);
obj[F("value")][fld] = value;
if (value > max)
max = value;
}
obj[F("max")] = max;
#else
obj[F("refresh")] = 86400; // 1 day
#endif /*ENABLE_HISTORY*/
}
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
bool accepted = true;
if(NULL == iv) {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
jsonOut[F("error")] = F(INV_INDEX_INVALID) + jsonIn[F("id")].as<String>();
return false;
}
jsonOut[F("id")] = jsonIn[F("id")];
@ -790,16 +857,18 @@ 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<int>());
} else {
jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'";
jsonOut[F("error")] = F(UNKNOWN_CMD) + jsonIn["cmd"].as<String>() + "'";
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;
}
@ -820,6 +889,15 @@ 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<const char*>());
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>());
mApp->saveSettings(false); // without reboot
mApp->setStopApAllowedMode(false);
mApp->setupStation();
}
#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")];
@ -839,7 +917,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;
}

28
src/web/html/api.js

@ -1,42 +1,42 @@
/* SVG ICONS - https://icons.getbootstrap.com */
iconWifi1 = [
var iconWifi1 = [
"M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z"
];
iconWifi2 = [
var iconWifi2 = [
"M13.229 8.271c.216-.216.194-.578-.063-.745A9.456 9.456 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.577 1.336c.205.132.48.108.652-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.408.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .708 0l.707-.707z"
];
iconWifi3 = [
var iconWifi3 = [
"M15.384 6.115a.485.485 0 0 0-.047-.736A12.444 12.444 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.518.518 0 0 0 .668.05A11.448 11.448 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049z",
"M13.229 8.271a.482.482 0 0 0-.063-.745A9.455 9.455 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065zm-2.183 2.183c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.61-.091l.016-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.707-.707z"
];
iconWarn = [
var iconWarn = [
"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z",
"M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"
];
iconInfo = [
var iconInfo = [
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z",
"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"
];
iconSuccess = [
var iconSuccess = [
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z",
"M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"
];
iconSuccessFull = [
var iconSuccessFull = [
"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"
];
iconGear = [
var iconGear = [
"M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"
];
iconDel = [
var iconDel = [
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z",
"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"
];
@ -84,10 +84,18 @@ function topnav() {
}
function parseNav(obj) {
for(i = 0; i < 11; i++) {
for(i = 0; i < 13; i++) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i);
if(12 == i) {
if(obj.cst_lnk.length > 0) {
l.href = obj.cst_lnk
l.innerHTML = obj.cst_lnk_txt
l.classList.remove("hide");
}
continue;
}
if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop()) {
if((i != 8 )&& (i != 9))
l.classList.add("active");

59
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"
}
]
@ -702,7 +717,7 @@
},
{
"name": "Recovery High Frequency",
"div": 10,
"div": 100,
"min": 50.1,
"max": 52,
"def": 50.2,
@ -952,7 +967,7 @@
"unit": "%Pn"
},
{
"name": "Power Factor ar Rated Power",
"name": "Power Factor of Rated Power",
"div": 100,
"min": 0.8,
"max": 1,

117
src/web/html/history.html

@ -0,0 +1,117 @@
<!doctype html>
<html>
<head>
<title>{#NAV_HISTORY}</title>
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<h3>{#TOTAL_POWER}</h3>
<div>
<div class="chartDiv" id="pwrChart"> </div>
<p>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p>
</div>
<h3>{#TOTAL_YIELD_PER_DAY}</h3>
<div>
<div class="chartDiv" id="ydChart"> </div>
<p>
{#MAXIMUM}: <span id="ydMax"></span> Wh<br />
{#UPDATED} <span id="ydRefresh"></span> {#SECONDS}
</p>
</div>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
const svgns = "http://www.w3.org/2000/svg";
var pwrExeOnce = true;
var ydExeOnce = true;
// make a simple rectangle
var mRefresh = 60;
var mLastValue = 0;
const mChartHeight = 250;
function parseHistory(obj, namePrefix, execOnce) {
mRefresh = obj.refresh
var data = Object.assign({}, obj.value)
numDataPts = Object.keys(data).length
if (true == execOnce) {
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", i * 2);
rect.setAttribute("width", 2);
g.appendChild(rect);
}
document.getElementById(namePrefix+"Chart").appendChild(s);
}
// normalize data to chart
let divider = obj.max / mChartHeight;
if (divider == 0)
divider = 1;
for (var i = 0; i < numDataPts; i++) {
val = data[i];
if (val > 0)
mLastValue = val
val = val / divider
rect = document.getElementById(namePrefix+"Rect" + i);
rect.setAttribute("height", val);
rect.setAttribute("y", mChartHeight - val);
}
document.getElementById(namePrefix + "Max").innerHTML = obj.max;
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
}
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);
}
}
getAjax("/api/powerHistory", parsePowerHistory);
</script>
</body>
</html>

2
src/web/html/includes/footer.html

@ -1,6 +1,6 @@
<div id="footer">
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2024</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>

3
src/web/html/includes/header.html

@ -1,6 +1,7 @@
<link rel="stylesheet" type="text/css" href="style.css?v={#VERSION}"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<meta charset="utf-8">
<script type="text/javascript" src="api.js?v={#VERSION}"></script>
<link rel="stylesheet" type="text/css" href="colors.css?v={#VERSION}"/>
<meta name="robots" content="noindex, nofollow" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">

12
src/web/html/includes/nav.html

@ -6,16 +6,18 @@
<span></span>
</a>
<div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live?v={#VERSION}">Live</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">Webserial</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">Settings</a>
<a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a>
<a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
<span class="seperator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>
<a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
<span class="seperator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
<a id="nav10" href="/about?v={#VERSION}">About</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
<a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
<a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
<span class="seperator"></span>
<a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a>

83
src/web/html/index.html

@ -20,18 +20,15 @@
</p>
<div id="note">
<h3>Support this project:</h3>
<h3>{#SUPPORT}:</h3>
<ul>
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">Changelog</a></li>
<li>Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">documentation</a></li>
<li><a href="https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip" target="_blank">Download</a> & Test development firmware, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">Development Changelog</a></li>
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li>
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li>
<li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
<li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
<li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li>
<li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
<li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
</ul>
<p class="lic">
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a>
</p>
</div>
</div>
</div>
@ -45,12 +42,12 @@
function apiCb(obj) {
var e = document.getElementById("apiResult");
if(obj["success"]) {
e.innerHTML = " command executed";
if(obj.success) {
e.innerHTML = " {#COMMAND_EXE}";
getAjax("/api/index", parse);
}
else
e.innerHTML = " Error: " + obj["error"];
e.innerHTML = " {#ERROR}: " + obj.error;
}
function setTime() {
@ -68,9 +65,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 +80,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);
@ -92,24 +89,24 @@
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);
}
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_STOP} " + (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'));
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'));
}
else {
commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 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);
@ -190,11 +187,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 +207,7 @@
}
function parseRelease(obj) {
release = obj["name"].substring(6);
release = obj.name.substring(6);
getAjax("/api/index", parse);
}

14
src/web/html/save.html

@ -1,14 +1,14 @@
<!doctype html>
<html>
<head>
<title>Save</title>
<title>{#NAV_SAVE}</title>
{#HTML_HEADER}
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div id="html" class="mt-3 mb-3">Saving settings...</div>
<div id="html" class="mt-3 mb-3">{#SAVE_SETTINGS}</div>
</div>
</div>
{#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.";
meta.content = 3;
html = "{#SUCCESS_SAVED_RELOAD}";
meta.content = "2; URL=/setup"
} else {
html = "Settings successfully saved. Rebooting. Automatic redirect in " + obj.reload + " seconds.";
meta.content = obj.reload + "; URL=/";
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;

2
src/web/html/serial.html

@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title>Serial Console</title>
<title>{#NAV_WEBSERIAL}</title>
{#HTML_HEADER}
</head>
<body>

679
src/web/html/setup.html

File diff suppressed because it is too large

68
src/web/html/style.css

@ -33,6 +33,22 @@ 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);
}
.topnav {
background-color: var(--nav-bg);
position: fixed;
@ -650,39 +666,24 @@ div.hr {
}
.css-tooltip{
.tooltip:hover {
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 {
@ -829,3 +830,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;
}

42
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)) {
@ -44,20 +44,29 @@
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("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)
)
@ -66,14 +75,15 @@
/*IF_ESP32*/
if(obj.radioCmt.en) {
cmt = [
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")),
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)
)
@ -84,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"),
@ -103,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_offset) * 1000).toLocaleString('de-DE')),
tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offset) * 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_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')),
tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
])
)
);

37
src/web/html/update.html

@ -9,29 +9,48 @@
<div id="wrapper">
<div id="content">
<fieldset>
<legend class="des">Select firmware file (*.bin)</legend>
<legend class="des">{#SELECT_FILE} (*.bin)</legend>
<p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update">
<input type="button" class="btn my-4" value="Update" onclick="hide()">
<input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
</form>
</fieldset>
<div class="row mt-4">
<a href="https://fw.ahoydtu.de" target="_blank">Download latest Release and Development versions<a/>
<a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}<a/>
</div>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
var env;
function parseGeneric(obj) {
parseNav(obj);
parseESP(obj);
parseRssi(obj);
parseNav(obj)
parseESP(obj)
parseRssi(obj)
env = obj.env
document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin"
}
function hide() {
document.getElementById("form").submit();
var e = document.getElementById("content");
e.replaceChildren(span("update started"));
var bin = document.getElementsByName("update")[0].value.slice(-env.length-4, -4)
if (bin !== env) {
var html = ml("div", {class: "row"}, [
ml("div", {class: "row my-3"}, "{#WARN_DIFF_ENV}"),
ml("div", {class: "row"}, [
ml("div", {class: "col-6"}, ml("input", {type: "button", class: "btn", value: "{#CANCEL}", onclick: function() { modalClose(); }}, null)),
ml("div", {class: "col-6"}, ml("input", {type: "button", class: "btn", value: "{#CONTIUE}", onclick: function() { start(); modalClose(); }}, null))
])
])
modal("{#UPDATE_MODAL}", html)
} else
start()
}
function start() {
document.getElementById("form").submit()
var e = document.getElementById("content")
e.replaceChildren(span("{#UPDATE_STARTED}"))
}
getAjax("/api/generic", parseGeneric);

160
src/web/html/visualization.html

@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
<title>Live</title>
<title>{#NAV_LIVE}</title>
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
@ -10,7 +10,7 @@
<div id="wrapper">
<div id="content">
<div id="live"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
<p>{#EVERY} <span id="refresh"></span> {#UPDATE_SECS}</p>
</div>
</div>
{#HTML_FOOTER}
@ -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)
@ -69,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}")
])
])
])
@ -91,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
@ -106,8 +107,10 @@
if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%";
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) + "&nbsp;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},
@ -116,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"),
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", "{#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}")
])
])
])
@ -169,32 +172,32 @@
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)
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)";
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? "&gt;=" : "&lt;") + " -64&nbsp;dBm)";
else
ageInfo += " (RSSI: " + obj.rssi + "dBm)";
ageInfo += " (RSSI: " + obj.rssi + "&nbsp;dBm)";
}
return ml("div", {class: "mb-5"}, [
@ -249,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}"))
])
);
@ -268,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) {
@ -280,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"}, [
@ -288,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) {
@ -329,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])) {
@ -359,11 +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 my-2"}, ml("pre", {}, obj.grid))))
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}."))))
}
} else {
content.push(ml("div", {class: "row"},
@ -374,8 +376,10 @@
content.push(parseGridGroup(glob))
}
}
if(0 != obj.grid.length)
content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid))))
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])))
})
}
@ -383,54 +387,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(["{#TX_COUNT}", obj.tx_cnt, ""]),
tr2(["{#RX_SUCCESS}", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_FAIL}", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_NO_ANSWER}", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
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) + "&nbsp;%"]),
tr2(["{#DTU_LOSS_RATE}", "{#LOST_1} " + obj.dtuLoss + " {#LOST_2} " + obj.dtuSent + " {#LOST_3}", String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + "&nbsp;%"])
])
])
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-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")),
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) {
@ -450,7 +456,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));
}
@ -465,19 +471,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) {
@ -487,7 +493,7 @@
clearInterval(tPwrAck);
if(null == e)
return;
e.innerHTML = "inverter acknowledged active power control command";
e.innerHTML = "{#INV_ACK}";
}
function parse(obj) {

87
src/web/html/wizard.html

@ -0,0 +1,87 @@
<!doctype html>
<html>
<head>
<title>{#NAV_WIZARD}</title>
{#HTML_HEADER}
</head>
<body>
<div id="wrapper">
<div class="container d-flex aic jc">
<div id="con"></div>
</div>
</div>
<script type="text/javascript">
var v;
var found = false;
var c = document.getElementById("con");
function sect(e1, e2) {
return ml("div", {class: "row"}, [
ml("div", {class: "col-12"}, ml("p", {}, e1)),
ml("div", {class: "col-12"}, e2)
])
}
function wifi() {
return ml("div", {}, [
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}"))),
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);
}
function redirect() {
window.location.replace("http://192.168.4.1/")
}
function printIp(obj) {
if("0.0.0.0" != obj["ip"]) {
clearInterval(v)
setHide("btn", false)
document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip
}
}
function saveWifi() {
var ssid = document.getElementById("net").value;
if(-1 == ssid)
ssid = document.getElementById("man").value;
getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value}));
}
function nets(obj) {
var e = document.getElementById("net");
if(obj.networks.length > 0) {
var a = []
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;
}
e.replaceChildren(...a)
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
c.append(wifi())
v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500);
</script>
</body>
</html>

87
src/web/lang.h

@ -0,0 +1,87 @@
//-----------------------------------------------------------------------------
// 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
#ifdef LANG_DE
#define FACTORY_RESET "Ahoy auf Werkseinstellungen zurücksetzen"
#else /*LANG_EN*/
#define FACTORY_RESET "Ahoy Factory Reset"
#endif
#ifdef LANG_DE
#define BTN_REBOOT "Ahoy neustarten"
#else /*LANG_EN*/
#define BTN_REBOOT "Reboot"
#endif
#endif /*__LANG_H__*/

1479
src/web/lang.json

File diff suppressed because it is too large

289
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
//-----------------------------------------------------------------------------
@ -32,10 +32,12 @@
#include "html/h/update_html.h"
#include "html/h/visualization_html.h"
#include "html/h/about_html.h"
#include "html/h/wizard_html.h"
#include "html/h/history_html.h"
#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 HMSYSTEM>
class Web {
@ -73,9 +75,11 @@ 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));
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));
@ -245,6 +249,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)
@ -260,7 +266,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);
@ -418,7 +424,7 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) {
checkProtection(request);
request->redirect("/setup");
request->redirect("/wizard");
}
void onReboot(AsyncWebServerRequest *request) {
@ -440,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"));
@ -465,13 +478,22 @@ class Web {
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
mConfig->sys.schedReboot = (request->arg("schedReboot") == "on");
if (request->arg("cstLnk") != "") {
request->arg("cstLnk").toCharArray(mConfig->plugin.customLink, MAX_CUSTOM_LINK_LEN);
request->arg("cstLnkTxt").toCharArray(mConfig->plugin.customLinkText, MAX_CUSTOM_LINK_TEXT_LEN);
} else {
mConfig->plugin.customLink[0] = '\0';
mConfig->plugin.customLinkText[0] = '\0';
}
// protection
if (request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
}
mConfig->sys.protectionMask = 0x0000;
for (uint8_t i = 0; i < 6; i++) {
for (uint8_t i = 0; i < 7; i++) {
if (request->arg("protMask" + String(i)) == "on")
mConfig->sys.protectionMask |= (1 << i);
}
@ -502,7 +524,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;
@ -511,15 +533,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;
}
}
@ -537,11 +560,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
@ -567,17 +592,32 @@ class Web {
// display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
mConfig->plugin.display.screenSaver = request->arg("disp_screensaver").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();
mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_data").toInt();
mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_clk").toInt();
mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_cs").toInt();
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mConfig->plugin.display.pirPin = request->arg("pir_pin").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 == DISP_TYPE_T0_NONE) || // contrast available only according optionsMap in setup.html, otherwise default value
(mConfig->plugin.display.type == DISP_TYPE_T10_EPAPER) ? 140 : request->arg("disp_cont").toInt();
mConfig->plugin.display.screenSaver = ((mConfig->plugin.display.type == DISP_TYPE_T1_SSD1306_128X64) // screensaver available only according optionsMap in setup.html, otherwise default value
|| (mConfig->plugin.display.type == DISP_TYPE_T2_SH1106_128X64)
|| (mConfig->plugin.display.type == DISP_TYPE_T4_SSD1306_128X32)
|| (mConfig->plugin.display.type == DISP_TYPE_T5_SSD1306_64X48)
|| (mConfig->plugin.display.type == DISP_TYPE_T6_SSD1309_128X64)) ? request->arg("disp_screensaver").toInt() : 0;
mConfig->plugin.display.graph_ratio = ((mConfig->plugin.display.type == DISP_TYPE_T1_SSD1306_128X64) // display graph available only according optionsMap in setup.html, otherwise has to be 0
|| (mConfig->plugin.display.type == DISP_TYPE_T2_SH1106_128X64)
|| (mConfig->plugin.display.type == DISP_TYPE_T3_PCD8544_84X48)
|| (mConfig->plugin.display.type == DISP_TYPE_T6_SSD1309_128X64)) ? request->arg("disp_graph_ratio").toInt() : 0;
// available pins according pinMap in setup.html, otherwise default value
mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == DISP_TYPE_T0_NONE) ? DEF_PIN_OFF : request->arg("disp_data").toInt();
mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == DISP_TYPE_T0_NONE) ? DEF_PIN_OFF : request->arg("disp_clk").toInt();
mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type != DISP_TYPE_T3_PCD8544_84X48)
&& (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_cs").toInt();
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type != DISP_TYPE_T3_PCD8544_84X48)
&& (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mConfig->plugin.display.pirPin = (mConfig->plugin.display.screenSaver != DISP_TYPE_T2_SH1106_128X64) ? DEF_PIN_OFF : request->arg("pir_pin").toInt(); // pir pin only for motion screensaver
// otherweise default value
mApp->saveSettings((request->arg("reboot") == "on"));
@ -590,6 +630,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");
@ -619,17 +663,53 @@ 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 * 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, metricsStateInverterIvRxCnt=13, metricsStateInverterIvTxCnt=14,
metricsStateInverterDtuRxCnt=15, metricsStateInverterDtuTxCnt=16,
metricStateRealtimeFieldId=metricsStateInverterDtuTxCnt+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 *topic;
const char *type;
const char *format;
const std::function<uint64_t(Inverter<> *iv)> valueFunc;
} InverterMetric_t;
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_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;
bool metricDeclared;
bool metricDeclared, metricTotalDeclard;
void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
@ -650,79 +730,65 @@ 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",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 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",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 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",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 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",metricConstPrefix);
snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, 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;
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;});
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;
metricsStep = metricsStateInverterInfo;
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();});
// 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:
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());
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<MetricStep_t>( static_cast<int>(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;
@ -737,7 +803,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);
@ -751,22 +816,26 @@ 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",metricConstPrefix, 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
strncpy(total,"_total",sizeof(total));
// A declaration and value for channels have been delivered. So declare and deliver a _total metric
strncpy(total, "_total", 6);
}
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",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\"}",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
char chName[MAX_NAME_LENGTH];
if (iv->config->chName[channel-1][0] != 0) {
@ -774,7 +843,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\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName);
}
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec));
metrics += topic;
@ -783,12 +852,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;
}
@ -804,7 +875,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",metricConstPrefix,fields[FLD_LAST_ALARM_CODE]);
metrics = type;
for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
@ -816,7 +887,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\"}",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;
@ -827,11 +898,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);
@ -839,27 +912,19 @@ class Web {
// Traverse all inverters and collect the metric via valueFunc
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv, IApp *mApp)> valueFunc) {
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *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<String, String> convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"_ampere", "gauge"};
if(shortUnit == "V") return {"_volt", "gauge"};

19
src/wifi/ahoywifi.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
//-----------------------------------------------------------------------------
@ -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;
@ -290,7 +293,7 @@ bool ahoywifi::getNtpTime(void) {
if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
uint64_t secsSince1900;
mUdp.read(buf, NTP_PACKET_SIZE);
secsSince1900 = (buf[40] << 24);
secsSince1900 = ((uint64_t)buf[40] << 24);
secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8);
secsSince1900 |= (buf[43] );

18
src/wifi/ahoywifi.h

@ -1,7 +1,8 @@
//------------------------------------//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if !defined(ETHERNET)
#ifndef __AHOYWIFI_H__
#define __AHOYWIFI_H__
@ -28,6 +29,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 +51,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);
@ -60,7 +67,7 @@ class ahoywifi {
void welcome(String ip, String mode);
settings_t *mConfig;
settings_t *mConfig = NULL;
appWifiCb mAppWifiCb;
DNSServer mDns;
@ -78,6 +85,7 @@ class ahoywifi {
bool mScanActive;
bool mGotDisconnect;
std::list<uint8_t> mBSSIDList;
bool mStopApAllowed;
};
#endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save