Browse Source

Merge branch 'development03' into Zero-Export

pull/1155/head
DanielR92 2 years ago
parent
commit
8ef5347711
  1. 17
      doc/prometheus_ep_description.md
  2. BIN
      doc/screenshots/inverterSettings.png
  3. BIN
      doc/screenshots/settings.png
  4. 3
      manual/Getting_Started.md
  5. 19
      manual/User_Manual.md
  6. 27
      manual/ahoy_config.md
  7. 12
      src/CHANGES.md
  8. 21
      src/app.cpp
  9. 19
      src/config/settings.h
  10. 8
      src/defines.h
  11. 54
      src/hm/Communication.h
  12. 4
      src/hm/hmDefines.h
  13. 45
      src/hm/hmInverter.h
  14. 12
      src/hm/hmRadio.h
  15. 12
      src/hms/hmsRadio.h
  16. 10
      src/publisher/pubMqtt.h
  17. 10
      src/publisher/pubMqttIvData.h
  18. 14
      src/web/RestApi.h
  19. 2
      src/web/html/grid_info.json
  20. 38
      src/web/html/index.html
  21. 16
      src/web/html/setup.html
  22. 4
      src/web/html/visualization.html
  23. 173
      src/web/web.h

17
doc/prometheus_ep_description.md

@ -19,11 +19,21 @@ Prometheus metrics provided at `/metrics`.
|----------------------------------------------|---------|----------------------------------------------------------|--------------| |----------------------------------------------|---------|----------------------------------------------------------|--------------|
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | | `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_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_info` | Gauge | Information about the configured inverter(s) | name, serial |
| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | | `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_available` | Gauge | is the inverter available? | inverter |
| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | | `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter |
| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter | inverter |
| `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter |
| `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_fail` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_fail_answer` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | | `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter |
| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | | `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter |
| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | | `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter |
@ -46,9 +56,4 @@ Prometheus metrics provided at `/metrics`.
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | | `ahoy_solar_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_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_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

3
manual/Getting_Started.md

@ -1,5 +1,4 @@
## Overview
## 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. 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.

19
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 ```json
{ {
@ -205,10 +206,10 @@ The `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
"val": <VALUE> "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 ```json
{ {
@ -217,10 +218,10 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val": <VALUE> "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 ```json
{ {
@ -229,10 +230,10 @@ The `VALUE` represents watts in a range of `[0 .. 65535]`
"val": <VALUE> "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 ```json
{ {
@ -241,7 +242,7 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val": <VALUE> "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. - 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 ### Update your AHOY-DTU Firmware
To update your AHOY-DTU, you have to download the latest firmware package. 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. 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. 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 - If you use an ESP8266, select the file ending with esp8266.bin

27
manual/ahoy_config.md

@ -1,19 +1,18 @@
# 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.
## Ahoy configuration ## Start
So far we have built our own DTU, written a program on it and put it into operation.
But how do I get my data from the inverter? But how do I get my data from the inverter?
To do this, we need to configure the DTU.
The following steps are required: The following steps are required:
1. Set the pinning to communicate with the radio module. 1. Set the pinning to communicate with the radio module.
2. Check if Ahoy has a current time 2. Check if Ahoy has a current time
3. Set inverter data 3. Configure the inverter data (e.g. serialnumber)
### 1.) Set the pinning ### 1.) Set the pinning
Once you are in the web interface, you will find the "System Config" sub-item in the Setup area (left). Once you are in the web interface, you will find the "System Config" sub-item in the Setup area.
This is where you tell the ESP how you connected the radio module. 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. Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections.
@ -38,7 +37,7 @@ Note the schematics you saw earlier. - If you haven't noticed them yet, here's a
| FCSB| GPIO21 | FCSB| GPIO21
| GPIO3| GPIO8 | GPIO3| GPIO8
### 2.) Set current time (normal skip this step) ### 2.) Set current time (standard: skip this step)
Ahoy needs a current date and time to talk to the inverter. 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. 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. Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this.
@ -51,20 +50,22 @@ Now it's time to place the inverter. This is necessary because it is not the inv
Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter. Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter.
The S.Nr is a 12-digit number. You can look it up [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. The S.Nr is a 12-digit number. Check [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information.
#### set pv-modules (not necessary) #### set pv-modules (not necessary)
Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short! Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short!
![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b52a2d5d-513c-4895-848a-01ce129f93c1) ![grafik](https://github.com/lumapu/ahoy/doc/screenshots/settings.png)
![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b508824f-08a7-4b9c-bc41-29dfee02dced) ![grafik](https://github.com/lumapu/ahoy/doc/screenshots/inverterSettings.png)
In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter. 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) #### 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). In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only).
#### advanced options (not necessary) #### advanced options (not necessary to be changed)
In the "Advanced" section, you can customize more settings. In the "Advanced" section, you can customize more settings.
Save and reboot. Save and reboot.
# Done - Now check the live site
## ✅ Done - Now check the live site

12
src/CHANGES.md

@ -1,5 +1,17 @@
# Development Changes # Development Changes
## 0.8.39 - 2024-01-01
* fix MqTT dis_night_comm in the morning #1309 #1286
* seperated offset for sunrise and sunset #1308
* **BREAKING CHANGE**: powerlimit (active power control) now has one decimal place (MqTT / API) #1199
* 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 ## 0.8.37 - 2023-12-30
* added grid profiles * added grid profiles
* format version of grid profile * format version of grid profile

21
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 // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -238,15 +238,18 @@ void app::tickCalcSunrise(void) {
if (mSunrise == 0) // on boot/reboot calc sun values for current time if (mSunrise == 0) // on boot/reboot calc sun values for current time
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); 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); ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
tickIVCommunication(); 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"); onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri");
if (mMqttEnabled) if (mMqttEnabled) {
tickSun(); tickSun();
nxtTrig = mSunrise - mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly
onceAt(std::bind(&app::tickSun, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm'
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -263,14 +266,14 @@ void app::tickIVCommunication(void) {
iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on
if (!iv->commEnabled) { // inverter communication only during the day 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 if (mTimestamp < (mSunrise - mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start
nxtTrig = mSunrise - mConfig->sun.offsetSec; nxtTrig = mSunrise - mConfig->sun.offsetSecMorning;
} else { } 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; nxtTrig = 0;
} else { // current time lies within communication start/stop time, set next trigger to communication stop } else { // current time lies within communication start/stop time, set next trigger to communication stop
iv->commEnabled = true; iv->commEnabled = true;
nxtTrig = mSunset + mConfig->sun.offsetSec; nxtTrig = mSunset + mConfig->sun.offsetSecEvening;
} }
} }
if (nxtTrig != 0) if (nxtTrig != 0)
@ -291,7 +294,7 @@ void app::tickIVCommunication(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickSun(void) { void app::tickSun(void) {
// only used and enabled by MQTT (see setup()) // only used and enabled by MQTT (see setup())
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec)) if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
} }

19
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 // 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 * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */ * */
#define CONFIG_VERSION 7 #define CONFIG_VERSION 8
#define PROT_MASK_INDEX 0x0001 #define PROT_MASK_INDEX 0x0001
@ -106,7 +106,8 @@ typedef struct {
typedef struct { typedef struct {
float lat; float lat;
float lon; float lon;
uint16_t offsetSec; uint16_t offsetSecMorning;
uint16_t offsetSecEvening;
} cfgSun_t; } cfgSun_t;
typedef struct { typedef struct {
@ -444,7 +445,8 @@ class settings {
mCfg.sun.lat = 0.0; mCfg.sun.lat = 0.0;
mCfg.sun.lon = 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.showIv = false;
mCfg.serial.debug = false; mCfg.serial.debug = false;
@ -535,6 +537,9 @@ class settings {
if(mCfg.configVersion < 7) { if(mCfg.configVersion < 7) {
mCfg.led.luminance = 255; mCfg.led.luminance = 255;
} }
if(mCfg.configVersion < 8) {
mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
}
} }
} }
@ -665,11 +670,13 @@ class settings {
if(set) { if(set) {
obj[F("lat")] = mCfg.sun.lat; obj[F("lat")] = mCfg.sun.lat;
obj[F("lon")] = mCfg.sun.lon; 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 { } else {
getVal<float>(obj, F("lat"), &mCfg.sun.lat); getVal<float>(obj, F("lat"), &mCfg.sun.lat);
getVal<float>(obj, F("lon"), &mCfg.sun.lon); getVal<float>(obj, F("lon"), &mCfg.sun.lon);
getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec); getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSecMorning);
getVal<uint16_t>(obj, F("offsEve"), &mCfg.sun.offsetSecEvening);
} }
} }

8
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 // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 8 #define VERSION_MINOR 8
#define VERSION_PATCH 37 #define VERSION_PATCH 39
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {
@ -106,6 +106,10 @@ typedef struct {
uint32_t frmCnt; uint32_t frmCnt;
uint32_t txCnt; uint32_t txCnt;
uint32_t retransmits; uint32_t retransmits;
uint16_t ivRxCnt; // last iv rx frames (from GetLossRate)
uint16_t ivTxCnt; // last iv tx frames (from GetLossRate)
uint16_t dtuRxCnt; // current DTU rx frames (since last GetLossRate)
uint16_t dtuTxCnt; // current DTU tx frames (since last GetLossRate)
} statistics_t; } statistics_t;
#endif /*__DEFINES_H__*/ #endif /*__DEFINES_H__*/

54
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -151,7 +151,7 @@ class Communication : public CommQueue<> {
if(validateIvSerial(&p->packet[1], q->iv)) { if(validateIvSerial(&p->packet[1], q->iv)) {
q->iv->radioStatistics.frmCnt++; q->iv->radioStatistics.frmCnt++;
q->iv->mDtuRxCnt++; q->iv->radioStatistics.dtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p)) if(parseFrame(p))
@ -351,8 +351,13 @@ class Communication : public CommQueue<> {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
//mPayload[iv->id].txId = p->packet[0]; //mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q); miDataDecode(p, q);
} else if (p->packet[0] == (0x0f + ALL_FRAMES)) } else if (p->packet[0] == (0x0f + ALL_FRAMES)) {
miHwDecode(p, q); 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)) { else if ((p->packet[0] == 0x88) || (p->packet[0] == 0x92)) {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = q->ts; rec->ts = q->ts;
@ -392,7 +397,7 @@ class Communication : public CommQueue<> {
DBGPRINT(F("has ")); DBGPRINT(F("has "));
if(!accepted) DBGPRINT(F("not ")); if(!accepted) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point ")); DBGPRINT(F("accepted power limit set point "));
DBGPRINT(String(q->iv->powerLimit[0])); DBGPRINT(String(q->iv->powerLimit[0]/10));
DBGPRINT(F(" with PowerLimitControl ")); DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(q->iv->powerLimit[1])); DBGPRINTLN(String(q->iv->powerLimit[1]));
q->iv->actPowerLimit = 0xffff; // unknown, readback current value q->iv->actPowerLimit = 0xffff; // unknown, readback current value
@ -650,22 +655,31 @@ class Communication : public CommQueue<> {
(mCbPayload)(InverterDevInform_Simple, q->iv); (mCbPayload)(InverterDevInform_Simple, q->iv);
q->iv->miMultiParts++; q->iv->miMultiParts++;
} }
//if(q->iv->miMultiParts > 5) }
//closeRequest(q->iv, true);
//else inline void miGPFDecode(packet_t *p, const queue_s *q) {
//if(q->iv->miMultiParts < 6) record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
// mState = States::WAIT; rec->ts = q->ts;
/*if (mPayload[iv->id].multi_parts > 5) { q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE
iv->setQueuedCmdFinished(); q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION
mPayload[iv->id].complete = true;
mPayload[iv->id].rxTmo = true; /* according to xlsx (different start byte -1!)
mPayload[iv->id].requested= false; Polling Grid-connected Protection Parameter File Command - Receipt
iv->radioStatistics.rxSuccess++; 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
if (mHighPrioIv == NULL) byte[13] byte[14] Version indicates the version of the grid-connected protection file
mHighPrioIv = iv; 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) { inline void miDataDecode(packet_t *p, const queue_s *q) {

4
src/hm/hmDefines.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy // 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __HM_DEFINES_H__ #ifndef __HM_DEFINES_H__

45
src/hm/hmInverter.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2024 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __HM_INVERTER_H__ #ifndef __HM_INVERTER_H__
@ -110,7 +110,7 @@ class Inverter {
uint8_t id; // unique id uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index 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 float actPowerLimit; // actual power limit
bool powerLimitAck; // acknowledged power limit (default: false) bool powerLimitAck; // acknowledged power limit (default: false)
uint8_t devControlCmd; // carries the requested cmd uint8_t devControlCmd; // carries the requested cmd
@ -141,18 +141,12 @@ class Inverter {
uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime 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 bool commEnabled; // 'pause night communication' sets this field to false
uint16_t mIvRxCnt; // last iv rx frames (from GetLossRate)
uint16_t mIvTxCnt; // last iv tx frames (from GetLossRate)
uint16_t mDtuRxCnt; // cur dtu rx frames (since last GetLossRate)
uint16_t mDtuTxCnt; // cur dtu tx frames (since last getLoassRate)
uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debu
static uint32_t *timestamp; // system timestamp static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup static cfgInst_t *generalConfig; // general inverter configuration from setup
Inverter() { Inverter() {
ivGen = IV_HM; 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 powerLimit[1] = AbsolutNonPersistent; // default power limit setting
powerLimitAck = false; powerLimitAck = false;
actPowerLimit = 0xffff; // init feedback from inverter to -1 actPowerLimit = 0xffff; // init feedback from inverter to -1
@ -171,10 +165,6 @@ class Inverter {
mIsSingleframeReq = false; mIsSingleframeReq = false;
radio = NULL; radio = NULL;
commEnabled = true; commEnabled = true;
mIvRxCnt = 0;
mIvTxCnt = 0;
mDtuRxCnt = 0;
mDtuTxCnt = 0;
memset(&radioStatistics, 0, sizeof(statistics_t)); memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5); memset(heuristics.txRfQuality, -6, 5);
@ -215,6 +205,8 @@ class Inverter {
record_t<> *rec = getRecordStruct(InverterDevInform_Simple); 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 cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
cb(0x10, false); // legacy GPF command
else else
cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
} }
@ -603,21 +595,25 @@ class Inverter {
uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t rxCnt = (pyld[0] << 8) + pyld[1];
uint16_t txCnt = (pyld[2] << 8) + pyld[3]; uint16_t txCnt = (pyld[2] << 8) + pyld[3];
if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past if (radioStatistics.ivRxCnt || radioStatistics.ivTxCnt) { // there was successful GetLossRate in the past
DPRINT_IVID(DBG_INFO, id); DPRINT_IVID(DBG_INFO, id);
DBGPRINTLN("Inv loss: " + DBGPRINT(F("Inv loss: "));
String (mDtuTxCnt - (rxCnt - mIvRxCnt)) + " of " + DBGPRINT(String (radioStatistics.dtuTxCnt - (rxCnt - radioStatistics.ivRxCnt)));
String (mDtuTxCnt) + ", DTU loss: " + DBGPRINT(F(" of "));
String (txCnt - mIvTxCnt - mDtuRxCnt) + " of " + DBGPRINT(String (radioStatistics.dtuTxCnt));
String (txCnt - mIvTxCnt)); DBGPRINT(F(", DTU loss: "));
DBGPRINT(String (txCnt - radioStatistics.ivTxCnt - radioStatistics.dtuRxCnt));
DBGPRINT(F(" of "));
DBGPRINTLN(String (txCnt - radioStatistics.ivTxCnt));
} }
mIvRxCnt = rxCnt; radioStatistics.ivRxCnt = rxCnt;
mIvTxCnt = txCnt; radioStatistics.ivTxCnt = txCnt;
mDtuRxCnt = 0; // start new interval radioStatistics.dtuRxCnt = 0; // start new interval
mDtuTxCnt = 0; // start new interval radioStatistics.dtuTxCnt = 0; // start new interval
return true; return true;
} }
return false; return false;
} }
@ -809,6 +805,7 @@ class Inverter {
bool mDevControlRequest; // true if change needed bool mDevControlRequest; // true if change needed
uint8_t mGridLen = 0; uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH]; uint8_t mGridProfile[MAX_GRID_LENGTH];
uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
}; };
template <class REC_TYP> template <class REC_TYP>

12
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -177,10 +177,10 @@ class HmRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00; mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
} }
} else { //MI 2nd gen. specific } else { //MI 2nd gen. specific
uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower());
@ -339,7 +339,7 @@ class HmRadio : public Radio {
mMillis = millis(); mMillis = millis();
mLastIv = iv; mLastIv = iv;
iv->mDtuTxCnt++; iv->radioStatistics.dtuTxCnt++;
} }
uint64_t getIvId(Inverter<> *iv) { uint64_t getIvId(Inverter<> *iv) {

12
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 // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -50,10 +50,10 @@ class CmtRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00; mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
} }
sendPacket(iv, cnt, isRetransmit); sendPacket(iv, cnt, isRetransmit);
@ -112,7 +112,7 @@ class CmtRadio : public Radio {
if(CMT_ERR_RX_IN_FIFO == status) if(CMT_ERR_RX_IN_FIFO == status)
mIrqRcvd = true; mIrqRcvd = true;
} }
iv->mDtuTxCnt++; iv->radioStatistics.dtuTxCnt++;
} }
uint64_t getIvId(Inverter<> *iv) { uint64_t getIvId(Inverter<> *iv) {

10
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 // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -134,14 +134,14 @@ class PubMqtt {
#endif #endif
} }
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) { bool tickerSun(uint32_t sunrise, uint32_t sunset, uint16_t offsM, uint16_t offsE) {
if (!mClient.connected()) if (!mClient.connected())
return false; return false;
publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true); publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true);
publish(subtopics[MQTT_SUNSET], String(sunset).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_START], String(sunrise - offsM).c_str(), true);
publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true);
Inverter<> *iv; Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@ -155,7 +155,7 @@ class PubMqtt {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); 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; return true;
} }

10
src/publisher/pubMqttIvData.h

@ -195,12 +195,16 @@ class PubMqttIvData {
inline void sendRadioStat(uint8_t start) { inline void sendRadioStat(uint8_t start) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name); 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.txCnt,
mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxSuccess,
mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFail,
mIv->radioStatistics.rxFailNoAnser, mIv->radioStatistics.rxFailNoAnser,
mIv->radioStatistics.retransmits); mIv->radioStatistics.retransmits,
mIv->radioStatistics.ivRxCnt,
mIv->radioStatistics.ivTxCnt,
mIv->radioStatistics.dtuRxCnt,
mIv->radioStatistics.dtuTxCnt);
mPublish(mSubTopic, mVal, false, QOS_0); mPublish(mSubTopic, mVal, false, QOS_0);
} }
@ -263,7 +267,7 @@ class PubMqttIvData {
bool mRTRDataHasBeenSent; bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[100]; char mVal[140];
bool mZeroValues; // makes sure that yield day is sent even if no inverter is online bool mZeroValues; // makes sure that yield day is sent even if no inverter is online
std::queue<sendListCmdIv> *mSendList; std::queue<sendListCmdIv> *mSendList;

14
src/web/RestApi.h

@ -1,5 +1,5 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -616,7 +616,8 @@ class RestApi {
void getSun(JsonObject obj) { void getSun(JsonObject obj) {
obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : "";
obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 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) { void getPinout(JsonObject obj) {
@ -701,10 +702,11 @@ class RestApi {
void getIndex(AsyncWebServerRequest *request, JsonObject obj) { void getIndex(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("ts_now")] = mApp->getTimestamp(); obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset(); obj[F("ts_sunset")] = mApp->getSunset();
obj[F("ts_offset")] = mConfig->sun.offsetSec; obj[F("ts_offsSr")] = mConfig->sun.offsetSecMorning;
obj[F("ts_offsSs")] = mConfig->sun.offsetSecEvening;
JsonArray inv = obj.createNestedArray(F("inverter")); JsonArray inv = obj.createNestedArray(F("inverter"));
Inverter<> *iv; Inverter<> *iv;

2
src/web/html/grid_info.json

@ -17,7 +17,7 @@
{"0x2600": "BE_C10_26"}, {"0x2600": "BE_C10_26"},
{"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2900": "NL_NEN-EN50549-1_2019"},
{"0x2a00": "PL_PN-EN 50549-1:2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"},
{"0x3700": "CH_NA EEA-NE7–CH2020"} {"0x3700": "CH_NA EEA-NE7–CH2020"},
{"0xe100": "LN_50Hz"} {"0xe100": "LN_50Hz"}
], ],
"grp_codes": [ "grp_codes": [

38
src/web/html/index.html

@ -45,12 +45,12 @@
function apiCb(obj) { function apiCb(obj) {
var e = document.getElementById("apiResult"); var e = document.getElementById("apiResult");
if(obj["success"]) { if(obj.success) {
e.innerHTML = " command executed"; e.innerHTML = " command executed";
getAjax("/api/index", parse); getAjax("/api/index", parse);
} }
else else
e.innerHTML = " Error: " + obj["error"]; e.innerHTML = " Error: " + obj.error;
} }
function setTime() { function setTime() {
@ -68,9 +68,9 @@
} }
function parseSys(obj) { function parseSys(obj) {
ts = obj["ts_now"]; ts = obj.ts_now;
var date = new Date(obj["ts_now"] * 1000); var date = new Date(obj.ts_now * 1000);
var up = obj["generic"]["ts_uptime"]; var up = obj.generic["ts_uptime"];
var days = parseInt(up / 86400) % 365; var days = parseInt(up / 86400) % 365;
var hrs = parseInt(up / 3600) % 24; var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60; var min = parseInt(up / 60) % 60;
@ -83,8 +83,8 @@
+ ("0"+min).substr(-2) + ":" + ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2); + ("0"+sec).substr(-2);
var dSpan = document.getElementById("date"); var dSpan = document.getElementById("date");
if(0 != obj["ts_now"]) { if(0 != obj.ts_now) {
if(obj["ts_now"] < 1680000000) if(obj.ts_now < 1680000000)
setTime(); setTime();
else else
dSpan.innerHTML = toIsoDateStr(date); dSpan.innerHTML = toIsoDateStr(date);
@ -98,18 +98,18 @@
e.addEventListener("click", setTime); e.addEventListener("click", setTime);
} }
if(obj["disNightComm"]) { if(obj.disNightComm) {
if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) if(((obj.ts_sunrise - obj.ts_offsSr) < obj.ts_now)
&& ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) {
commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
} }
else { else {
commInfo = "Night time, inverter polling disabled, "; commInfo = "Night time, inverter polling disabled, ";
if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { if(obj.ts_now > (obj.ts_sunrise - obj.ts_offsSr)) {
commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
} }
else { else {
commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo += "will start polling at " + (new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE'));
} }
} }
} }
@ -190,11 +190,11 @@
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
if(exeOnce) if(exeOnce)
parseNav(obj["generic"]); parseNav(obj.generic);
parseGeneric(obj["generic"]); parseGeneric(obj.generic);
parseSys(obj); parseSys(obj);
parseIv(obj["inverter"], obj.ts_now); parseIv(obj.inverter, obj.ts_now);
parseWarn(obj["warnings"]); parseWarn(obj.warnings);
if(exeOnce) { if(exeOnce) {
window.setInterval("tick()", 1000); window.setInterval("tick()", 1000);
exeOnce = false; exeOnce = false;
@ -210,7 +210,7 @@
} }
function parseRelease(obj) { function parseRelease(obj) {
release = obj["name"].substring(6); release = obj.name.substring(6);
getAjax("/api/index", parse); getAjax("/api/index", parse);
} }

16
src/web/html/setup.html

@ -233,8 +233,12 @@
<div class="col-12 col-sm-9"><input type="number" name="sunLon" step="any"/></div> <div class="col-12 col-sm-9"><input type="number" name="sunLon" step="any"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div> <div class="col-12 col-sm-3 my-2">Offset (sunrise)</div>
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div> <div class="col-12 col-sm-9"><select name="sunOffsSr"></select></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Offset (sunset)</div>
<div class="col-12 col-sm-9"><select name="sunOffsSs"></select></div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@ -936,9 +940,11 @@
function parseSun(obj) { function parseSun(obj) {
document.getElementsByName("sunLat")[0].value = obj["lat"]; document.getElementsByName("sunLat")[0].value = obj["lat"];
document.getElementsByName("sunLon")[0].value = obj["lon"]; document.getElementsByName("sunLon")[0].value = obj["lon"];
const sel = document.getElementsByName("sunOffs")[0]; for(p of [["sunOffsSr", "offsSr"], ["sunOffsSs", "offsSs"]]) {
for(var i = 0; i <= 60; i++) { const sel = document.getElementsByName(p[0])[0];
sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); for(var i = 0; i <= 60; i++) {
sel.appendChild(opt(i, i + " minutes", (i == (obj[p[1]] / 60))));
}
} }
} }

4
src/web/html/visualization.html

@ -400,7 +400,7 @@
var html = ml("div", {}, [ var html = ml("div", {}, [
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"), ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"),
ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")), 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: "col-4 col-sm-2"}, sel("type", opt, "pct"))
]), ]),
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
@ -451,7 +451,7 @@
var obj = new Object(); var obj = new Object();
obj.id = id; obj.id = id;
obj.cmd = cmd; obj.cmd = cmd;
obj.val = val; obj.val = Math.round(val*10);
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
} }

173
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -541,11 +541,13 @@ class Web {
if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0; mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0; mConfig->sun.lon = 0.0;
mConfig->sun.offsetSec = 0; mConfig->sun.offsetSecMorning = 0;
mConfig->sun.offsetSecEvening = 0;
} else { } else {
mConfig->sun.lat = request->arg("sunLat").toFloat(); mConfig->sun.lat = request->arg("sunLat").toFloat();
mConfig->sun.lon = request->arg("sunLon").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 // mqtt
@ -656,17 +658,45 @@ class Web {
#ifdef ENABLE_PROMETHEUS_EP #ifdef ENABLE_PROMETHEUS_EP
// Note // Note
// Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md // 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) // NOTE: Grouping for fields with channels and totals is currently not working
enum { // TODO: Handle grouping and sorting for independant from channel number
metricsStateStart, // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, const char * metricPrefix = "ahoy_solar_";
metricStateRealtimeFieldId, metricStateRealtimeInverterId, typedef enum {
metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterProducing=3,
metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterMaxPower=6,
metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, metricsStateInverterRxFailAnswer=9,
metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, metricsStateInverterRetransmits=12,
metricStateRealtimeFieldId=metricsStateInverterRetransmits+1, // ensure that this state follows the last per_inverter state
metricStateRealtimeInverterId,
metricsStateAlarmData, metricsStateAlarmData,
metricsStateStart,
metricsStateEnd metricsStateEnd
} metricsStep; } MetricStep_t;
MetricStep_t metricsStep;
typedef struct {
const char *type;
const char *format;
const std::function<uint64_t(Inverter<> *iv)> valueFunc;
} InverterMetric_t;
InverterMetric_t inverterMetrics[13] = {
{ "info", "info{name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} },
{ "is_enabled", "is_enabled {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} },
{ "is_available", "is_available {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} },
{ "is_producing", "is_producing {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} },
{ "power_limit_read", "power_limit_read {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} },
{ "power_limit_ack", "power_limit_ack {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} },
{ "max_power", "max_power {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
{ "radio_rx_success", "radio_rx_success {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
{ "radio_rx_fail", "radio_rx_fail {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
{ "radio_rx_fail_answer", "radio_rx_fail_answer {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} },
{ "radio_frame_cnt", "radio_frame_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} },
{ "radio_tx_cnt", "radio_tx_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
{ "radio_retransmits", "radio_retransmits {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }
};
int metricsInverterId; int metricsInverterId;
uint8_t metricsFieldId; uint8_t metricsFieldId;
bool metricDeclared; bool metricDeclared, metricTotalDeclard;
void showMetrics(AsyncWebServerRequest *request) { void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
@ -687,79 +717,58 @@ class Web {
// Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. // 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 // So several "Info:" blocks are used to keep the transmission going
switch (metricsStep) { switch (metricsStep) {
case metricsStateStart: // System Info & NRF Statistics : fit to one packet case metricsStateStart: // System Info : fit to one packet
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricPrefix);
snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricPrefix,
mApp->getVersion(), mConfig->sys.deviceName); mApp->getVersion(), mConfig->sys.deviceName);
metrics = String(type) + String(topic); metrics = String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n"); snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricPrefix);
snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricPrefix,mConfig->sys.deviceName,ESP.getFreeHeap());
metrics += String(type) + String(topic); metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n"); snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricPrefix);
snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime()); snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricPrefix, mConfig->sys.deviceName, mApp->getUptime());
metrics += String(type) + String(topic); metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n"); snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricPrefix);
snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI()); snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricPrefix, mConfig->sys.deviceName, WiFi.RSSI());
metrics += String(type) + String(topic); 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()); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Next is Inverter information // Next is Inverter information
metricsInverterId = 0; metricsStep = metricsStateInverterInfo;
metricsStep = metricsStateInverter1;
break; break;
case metricsStateInverter1: // Information about all inverters configured : fit to one packet // Information about all inverters configured : each metric for all inverters must fit to one network packet
metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; case metricsStateInverterInfo:
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", case metricsStateInverterEnabled:
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); case metricsStateInverterAvailable:
case metricsStateInverterProducing:
case metricsStateInverterPowerLimitRead:
case metricsStateInverterPowerLimitAck:
case metricsStateInverterMaxPower:
case metricsStateInverterRxSuccess:
case metricsStateInverterRxFail:
case metricsStateInverterRxFailAnswer:
case metricsStateInverterFrameCnt:
case metricsStateInverterTxCnt:
case metricsStateInverterRetransmits:
metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].type) + " gauge\n";
metrics += inverterMetric(topic, sizeof(topic),(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].valueFunc);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter2; // ugly hack to increment the enum
break; metricsStep = static_cast<MetricStep_t>( static_cast<int>(metricsStep) + 1);
// Prepare Realtime Field loop, which may be startet next
case metricsStateInverter2: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter3;
break;
case metricsStateInverter3: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter4;
break;
case metricsStateInverter4: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Start Realtime Field loop
metricsFieldId = FLD_UDC; metricsFieldId = FLD_UDC;
metricsStep = metricStateRealtimeFieldId;
break; break;
case metricStateRealtimeFieldId: // Iterate over all defined fields case metricStateRealtimeFieldId: // Iterate over all defined fields
if (metricsFieldId < FLD_LAST_ALARM_CODE) { if (metricsFieldId < FLD_LAST_ALARM_CODE) {
metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n";
metricDeclared = false; metricDeclared = false;
metricTotalDeclard = false;
metricsInverterId = 0; metricsInverterId = 0;
metricsStep = metricStateRealtimeInverterId; metricsStep = metricStateRealtimeInverterId;
@ -774,7 +783,6 @@ class Web {
metrics = ""; metrics = "";
if (metricsInverterId < mSys->getNumInverters()) { if (metricsInverterId < mSys->getNumInverters()) {
// process all channels of this inverter // process all channels of this inverter
iv = mSys->getInverterByPos(metricsInverterId); iv = mSys->getInverterByPos(metricsInverterId);
if (NULL != iv) { if (NULL != iv) {
rec = iv->getRecordStruct(RealTimeRunData_Debug); rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -788,22 +796,27 @@ class Web {
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));
// Declare metric only once // Declare metric only once
if (channel != 0 && !metricDeclared) { if (channel != 0 && !metricDeclared) {
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
metrics += type; metrics += type;
metricDeclared = true; metricDeclared = true;
} }
// report value // report value
if (0 == channel) { if (0 == channel) {
// Report a _total value if also channel values were reported. Otherwise report without _total
char total[7]; char total[7];
total[0] = 0; total[0] = 0;
if (metricDeclared) { if (metricDeclared) {
// A declaration and value for channels has been delivered. So declare and deliver a _total metric // A declaration and value for channels have been delivered. So declare and deliver a _total metric
strncpy(total,"_total",sizeof(total)); strncpy(total,"_total",sizeof(total));
} }
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); if (!metricTotalDeclard) {
metrics += type; snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str());
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); metrics += type;
metricTotalDeclard = true;
}
snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name);
} else { } else {
// Report (non zero) channel value
// Use a fallback channel name (ch0, ch1, ...)if non is given by user // Use a fallback channel name (ch0, ch1, ...)if non is given by user
char chName[MAX_NAME_LENGTH]; char chName[MAX_NAME_LENGTH];
if (iv->config->chName[channel-1][0] != 0) { if (iv->config->chName[channel-1][0] != 0) {
@ -811,7 +824,7 @@ class Web {
} else { } else {
snprintf(chName,sizeof(chName),"ch%1d",channel); snprintf(chName,sizeof(chName),"ch%1d",channel);
} }
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName);
} }
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec));
metrics += topic; metrics += topic;
@ -841,7 +854,7 @@ class Web {
case metricsStateAlarmData: // Alarm Info loop : fit to one packet case metricsStateAlarmData: // Alarm Info loop : fit to one packet
// Perform grouping on metrics according to Prometheus exposition format specification // Perform grouping on metrics according to Prometheus exposition format specification
snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricPrefix,fields[FLD_LAST_ALARM_CODE]);
metrics = type; metrics = type;
for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
@ -853,7 +866,7 @@ class Web {
alarmChannelId = 0; alarmChannelId = 0;
if (alarmChannelId < rec->length) { if (alarmChannelId < rec->length) {
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec));
metrics += topic; metrics += topic;
metrics += val; metrics += val;
@ -864,11 +877,13 @@ class Web {
metricsStep = metricsStateEnd; metricsStep = metricsStateEnd;
break; break;
case metricsStateEnd:
default: // end of transmission default: // end of transmission
DBGPRINT("E: Prometheus: Bad metricsStep=");
DBGPRINTLN(String(metricsStep));
case metricsStateEnd:
len = 0; len = 0;
break; break;
} } // switch
return len; return len;
}); });
request->send(response); request->send(response);
@ -876,27 +891,19 @@ class Web {
// Traverse all inverters and collect the metric via valueFunc // 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; Inverter<> *iv;
String metric = ""; String metric = "";
for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
iv = mSys->getInverterByPos(metricsInverterId); iv = mSys->getInverterByPos(metricsInverterId);
if (NULL != iv) { 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); metric += String(buffer);
} }
} }
return metric; 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) { std::pair<String, String> convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"_ampere", "gauge"}; if(shortUnit == "A") return {"_ampere", "gauge"};
if(shortUnit == "V") return {"_volt", "gauge"}; if(shortUnit == "V") return {"_volt", "gauge"};

Loading…
Cancel
Save