diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 711299dd..651cd937 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -1,10 +1,10 @@ # Prometheus Endpoint Metrics available for AhoyDTU device, inverters and channels. -Prometheus metrics provided at `/metrics`. +Prometheus metrics provided at `/metrics`. ## Labels -| Label name | Description | +| Label name | Description | |:-------------|:--------------------------------------| | version | current installed version of AhoyDTU | | image | currently not used | @@ -19,11 +19,21 @@ Prometheus metrics provided at `/metrics`. |----------------------------------------------|---------|----------------------------------------------------------|--------------| | `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | | `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | -| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | +| `ahoy_solar_freeheap` | Gauge | free heap memory of the AhoyDTU device | devicename | +| `ahoy_solar_wifi_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | | `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | | `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | | `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | | `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | +| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter | inverter | +| `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter | +| `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_fail` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_rx_fail_answer` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | | `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | | `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | @@ -46,9 +56,4 @@ Prometheus metrics provided at `/metrics`. | `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | | `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | | `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | -| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | diff --git a/doc/screenshots/inverterSettings.png b/doc/screenshots/inverterSettings.png new file mode 100644 index 00000000..ef9484c1 Binary files /dev/null and b/doc/screenshots/inverterSettings.png differ diff --git a/doc/screenshots/settings.png b/doc/screenshots/settings.png new file mode 100644 index 00000000..179a5d07 Binary files /dev/null and b/doc/screenshots/settings.png differ diff --git a/manual/Getting_Started.md b/manual/Getting_Started.md index 81193ed0..b1ef7995 100644 --- a/manual/Getting_Started.md +++ b/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. diff --git a/manual/User_Manual.md b/manual/User_Manual.md index b4ef8666..53da34f4 100644 --- a/manual/User_Manual.md +++ b/manual/User_Manual.md @@ -195,8 +195,9 @@ The `` should be set to `1` = `ON` and `0` = `OFF` } ``` +**beginning from verson `0.8.39` the wattage and percentage has one decimal place!** -### Power Limit relative persistent [%] +### Power Limit (active power control) relative persistent [%] ```json { @@ -205,10 +206,10 @@ The `` should be set to `1` = `ON` and `0` = `OFF` "val": } ``` -The `VALUE` represents a percent number in a range of `[2 .. 100]` +The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]` -### Power Limit absolute persistent [Watts] +### Power Limit (active power control) absolute persistent [Watts] ```json { @@ -217,10 +218,10 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]` "val": } ``` -The `VALUE` represents watts in a range of `[0 .. 65535]` +The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` -### Power Limit relative non persistent [%] +### Power Limit (active power control) relative non persistent [%] ```json { @@ -229,10 +230,10 @@ The `VALUE` represents watts in a range of `[0 .. 65535]` "val": } ``` -The `VALUE` represents a percent number in a range of `[2 .. 100]` +The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]` -### Power Limit absolute non persistent [Watts] +### Power Limit (active power control) absolute non persistent [Watts] ```json { @@ -241,7 +242,7 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]` "val": } ``` -The `VALUE` represents watts in a range of `[0 .. 65535]` +The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` @@ -328,7 +329,7 @@ Send Power Limit: - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. ### Update your AHOY-DTU Firmware To update your AHOY-DTU, you have to download the latest firmware package. -Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download. +Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://fw.ahoydtu.de/dev) available for download. As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file. From the unzipped files, select the right .bin file for your hardware and needs. - If you use an ESP8266, select the file ending with esp8266.bin diff --git a/manual/ahoy_config.md b/manual/ahoy_config.md index 73427b6f..1971848f 100644 --- a/manual/ahoy_config.md +++ b/manual/ahoy_config.md @@ -1,19 +1,18 @@ - +# Ahoy configuration -## Ahoy configuration +## Prerequists +You have build your own hardware (or purchased one). The firmware is already loaded on the ESP and the WebUI is accessible from your browser. - So far we have built our own DTU, written a program on it and put it into operation. +## Start But how do I get my data from the inverter? -To do this, we need to configure the DTU. - The following steps are required: 1. Set the pinning to communicate with the radio module. 2. Check if Ahoy has a current time -3. Set inverter data +3. Configure the inverter data (e.g. serialnumber) ### 1.) Set the pinning -Once you are in the web interface, you will find the "System Config" sub-item in the Setup area (left). +Once you are in the web interface, you will find the "System Config" sub-item in the Setup area. This is where you tell the ESP how you connected the radio module. Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections. @@ -38,7 +37,7 @@ Note the schematics you saw earlier. - If you haven't noticed them yet, here's a | FCSB| GPIO21 | GPIO3| GPIO8 -### 2.) Set current time (normal skip this step) +### 2.) Set current time (standard: skip this step) Ahoy needs a current date and time to talk to the inverter. It works without, but it is recommended to include a time. This allows you to analyze information from the inverter in more detail. Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this. @@ -51,20 +50,22 @@ Now it's time to place the inverter. This is necessary because it is not the inv Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter. -The S.Nr is a 12-digit number. You can look it up [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. +The S.Nr is a 12-digit number. Check [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information. + #### set pv-modules (not necessary) Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short! -![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b52a2d5d-513c-4895-848a-01ce129f93c1) +![grafik](https://github.com/lumapu/ahoy/doc/screenshots/settings.png) -![grafik](https://github.com/DanielR92/ahoy/assets/25644396/b508824f-08a7-4b9c-bc41-29dfee02dced) +![grafik](https://github.com/lumapu/ahoy/doc/screenshots/inverterSettings.png) In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter. #### set radio parameter (not necessary, only for EU) In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only). -#### advanced options (not necessary) +#### advanced options (not necessary to be changed) In the "Advanced" section, you can customize more settings. Save and reboot. -# Done - Now check the live site + +## ✅ Done - Now check the live site diff --git a/src/CHANGES.md b/src/CHANGES.md index 32efd79b..c83130c8 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,17 @@ # 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 * added grid profiles * format version of grid profile diff --git a/src/app.cpp b/src/app.cpp index d5258b9c..750a1c33 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -238,15 +238,18 @@ void app::tickCalcSunrise(void) { if (mSunrise == 0) // on boot/reboot calc sun values for current time ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); - if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day + if (mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) // current time is past communication stop, calc sun values for next day ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); tickIVCommunication(); - uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSecEvening + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); - if (mMqttEnabled) + if (mMqttEnabled) { tickSun(); + nxtTrig = mSunrise - mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly + onceAt(std::bind(&app::tickSun, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm' + } } //----------------------------------------------------------------------------- @@ -263,14 +266,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) @@ -291,7 +294,7 @@ void app::tickIVCommunication(void) { //----------------------------------------------------------------------------- void app::tickSun(void) { // only used and enabled by MQTT (see setup()) - if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec)) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening)) once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry } diff --git a/src/config/settings.h b/src/config/settings.h index 388e6d3c..620dcb17 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -30,7 +30,7 @@ * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ -#define CONFIG_VERSION 7 +#define CONFIG_VERSION 8 #define PROT_MASK_INDEX 0x0001 @@ -106,7 +106,8 @@ typedef struct { typedef struct { float lat; float lon; - uint16_t offsetSec; + uint16_t offsetSecMorning; + uint16_t offsetSecEvening; } cfgSun_t; typedef struct { @@ -444,7 +445,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; @@ -535,6 +537,9 @@ class settings { if(mCfg.configVersion < 7) { mCfg.led.luminance = 255; } + if(mCfg.configVersion < 8) { + mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning; + } } } @@ -665,11 +670,13 @@ class settings { if(set) { obj[F("lat")] = mCfg.sun.lat; obj[F("lon")] = mCfg.sun.lon; - obj[F("offs")] = mCfg.sun.offsetSec; + obj[F("offs")] = mCfg.sun.offsetSecMorning; + obj[F("offsEve")] = mCfg.sun.offsetSecEvening; } else { getVal(obj, F("lat"), &mCfg.sun.lat); getVal(obj, F("lon"), &mCfg.sun.lon); - getVal(obj, F("offs"), &mCfg.sun.offsetSec); + getVal(obj, F("offs"), &mCfg.sun.offsetSecMorning); + getVal(obj, F("offsEve"), &mCfg.sun.offsetSecEvening); } } diff --git a/src/defines.h b/src/defines.h index 18b43c82..8a48c036 100644 --- a/src/defines.h +++ b/src/defines.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 37 +#define VERSION_PATCH 39 //------------------------------------- typedef struct { @@ -106,6 +106,10 @@ typedef struct { uint32_t frmCnt; uint32_t txCnt; uint32_t retransmits; + uint16_t ivRxCnt; // last iv rx frames (from GetLossRate) + uint16_t ivTxCnt; // last iv tx frames (from GetLossRate) + uint16_t dtuRxCnt; // current DTU rx frames (since last GetLossRate) + uint16_t dtuTxCnt; // current DTU tx frames (since last GetLossRate) } statistics_t; #endif /*__DEFINES_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h index 9f96533d..a448884e 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -151,7 +151,7 @@ class Communication : public CommQueue<> { if(validateIvSerial(&p->packet[1], q->iv)) { q->iv->radioStatistics.frmCnt++; - q->iv->mDtuRxCnt++; + q->iv->radioStatistics.dtuRxCnt++; if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if(parseFrame(p)) @@ -351,8 +351,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,7 +397,7 @@ class Communication : public CommQueue<> { DBGPRINT(F("has ")); if(!accepted) DBGPRINT(F("not ")); DBGPRINT(F("accepted power limit set point ")); - DBGPRINT(String(q->iv->powerLimit[0])); + DBGPRINT(String(q->iv->powerLimit[0]/10)); DBGPRINT(F(" with PowerLimitControl ")); DBGPRINTLN(String(q->iv->powerLimit[1])); q->iv->actPowerLimit = 0xffff; // unknown, readback current value @@ -650,22 +655,31 @@ class Communication : public CommQueue<> { (mCbPayload)(InverterDevInform_Simple, q->iv); q->iv->miMultiParts++; } - //if(q->iv->miMultiParts > 5) - //closeRequest(q->iv, true); - //else - //if(q->iv->miMultiParts < 6) - // mState = States::WAIT; - - /*if (mPayload[iv->id].multi_parts > 5) { - iv->setQueuedCmdFinished(); - mPayload[iv->id].complete = true; - mPayload[iv->id].rxTmo = true; - mPayload[iv->id].requested= false; - iv->radioStatistics.rxSuccess++; - } - if (mHighPrioIv == NULL) - mHighPrioIv = iv; - */ + } + + inline void miGPFDecode(packet_t *p, const queue_s *q) { + record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure + rec->ts = q->ts; + + q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE + q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION + + /* according to xlsx (different start byte -1!) + Polling Grid-connected Protection Parameter File Command - Receipt + byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged + byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file + byte[13] byte[14] Version indicates the version of the grid-connected protection file + byte[15] byte[16] + */ + /*if(mSerialDebug) { + DPRINT(DBG_INFO,F("ST1 ")); + DBGPRINTLN(String(p->packet[9])); + DPRINT(DBG_INFO,F("CountryStd ")); + DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11])); + DPRINT(DBG_INFO,F("Version ")); + DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); + }*/ + q->iv->miMultiParts = 7; // indicate we are ready } inline void miDataDecode(packet_t *p, const queue_s *q) { diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 55259289..a2a2d6b4 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __HM_DEFINES_H__ diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 776b9c6f..37a469a7 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __HM_INVERTER_H__ @@ -110,7 +110,7 @@ class Inverter { uint8_t id; // unique id uint8_t type; // integer which refers to inverter type uint16_t alarmMesIndex; // Last recorded Alarm Message Index - uint16_t powerLimit[2]; // limit power output + uint16_t powerLimit[2]; // limit power output (multiplied by 10) float actPowerLimit; // actual power limit bool powerLimitAck; // acknowledged power limit (default: false) uint8_t devControlCmd; // carries the requested cmd @@ -141,18 +141,12 @@ class Inverter { uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime bool commEnabled; // 'pause night communication' sets this field to false - uint16_t mIvRxCnt; // last iv rx frames (from GetLossRate) - uint16_t mIvTxCnt; // last iv tx frames (from GetLossRate) - uint16_t mDtuRxCnt; // cur dtu rx frames (since last GetLossRate) - uint16_t mDtuTxCnt; // cur dtu tx frames (since last getLoassRate) - uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debu - static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup 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 @@ -171,10 +165,6 @@ class Inverter { mIsSingleframeReq = false; radio = NULL; commEnabled = true; - mIvRxCnt = 0; - mIvTxCnt = 0; - mDtuRxCnt = 0; - mDtuTxCnt = 0; memset(&radioStatistics, 0, sizeof(statistics_t)); memset(heuristics.txRfQuality, -6, 5); @@ -215,6 +205,8 @@ class Inverter { record_t<> *rec = getRecordStruct(InverterDevInform_Simple); if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1 + else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile + cb(0x10, false); // legacy GPF command else cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); } @@ -603,21 +595,25 @@ class Inverter { uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t txCnt = (pyld[2] << 8) + pyld[3]; - if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past + if (radioStatistics.ivRxCnt || radioStatistics.ivTxCnt) { // there was successful GetLossRate in the past DPRINT_IVID(DBG_INFO, id); - DBGPRINTLN("Inv loss: " + - String (mDtuTxCnt - (rxCnt - mIvRxCnt)) + " of " + - String (mDtuTxCnt) + ", DTU loss: " + - String (txCnt - mIvTxCnt - mDtuRxCnt) + " of " + - String (txCnt - mIvTxCnt)); + DBGPRINT(F("Inv loss: ")); + DBGPRINT(String (radioStatistics.dtuTxCnt - (rxCnt - radioStatistics.ivRxCnt))); + DBGPRINT(F(" of ")); + DBGPRINT(String (radioStatistics.dtuTxCnt)); + DBGPRINT(F(", DTU loss: ")); + DBGPRINT(String (txCnt - radioStatistics.ivTxCnt - radioStatistics.dtuRxCnt)); + DBGPRINT(F(" of ")); + DBGPRINTLN(String (txCnt - radioStatistics.ivTxCnt)); } - mIvRxCnt = rxCnt; - mIvTxCnt = txCnt; - mDtuRxCnt = 0; // start new interval - mDtuTxCnt = 0; // start new interval + radioStatistics.ivRxCnt = rxCnt; + radioStatistics.ivTxCnt = txCnt; + radioStatistics.dtuRxCnt = 0; // start new interval + radioStatistics.dtuTxCnt = 0; // start new interval return true; } + return false; } @@ -809,6 +805,7 @@ class Inverter { bool mDevControlRequest; // true if change needed uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; + uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug }; template diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 0754f83c..c7b9581c 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -177,10 +177,10 @@ class HmRadio : public Radio { mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = 0x00; if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction) + mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit + mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling } } else { //MI 2nd gen. specific uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); @@ -339,7 +339,7 @@ class HmRadio : public Radio { mMillis = millis(); mLastIv = iv; - iv->mDtuTxCnt++; + iv->radioStatistics.dtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index d2779012..3b3893ac 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy +// 2024 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -50,10 +50,10 @@ class CmtRadio : public Radio { mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = 0x00; if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction) + mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit + mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling } sendPacket(iv, cnt, isRetransmit); @@ -112,7 +112,7 @@ class CmtRadio : public Radio { if(CMT_ERR_RX_IN_FIFO == status) mIrqRcvd = true; } - iv->mDtuTxCnt++; + iv->radioStatistics.dtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index e062439c..9834f29a 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -134,14 +134,14 @@ class PubMqtt { #endif } - bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) { + bool tickerSun(uint32_t sunrise, uint32_t sunset, uint16_t offsM, uint16_t offsE) { if (!mClient.connected()) return false; publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true); publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); - publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); - publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); + publish(subtopics[MQTT_COMM_START], String(sunrise - offsM).c_str(), true); + publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true); Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { @@ -155,7 +155,7 @@ class PubMqtt { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); - publish(mSubTopic, (((*mUtcTimestamp > (sunset + offs)) || (*mUtcTimestamp < (sunrise - offs))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise - offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); return true; } diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 9a364ec9..3a38a5ed 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -195,12 +195,16 @@ class PubMqttIvData { inline void sendRadioStat(uint8_t start) { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name); - snprintf(mVal, 100, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d}", + snprintf(mVal, 140, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d,\"lossIvRx\":%d,\"lossIvTx\":%d,\"lossDtuRx\":%d,\"lossDtuTx\":%d}", mIv->radioStatistics.txCnt, mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFailNoAnser, - mIv->radioStatistics.retransmits); + mIv->radioStatistics.retransmits, + mIv->radioStatistics.ivRxCnt, + mIv->radioStatistics.ivTxCnt, + mIv->radioStatistics.dtuRxCnt, + mIv->radioStatistics.dtuTxCnt); mPublish(mSubTopic, mVal, false, QOS_0); } @@ -263,7 +267,7 @@ class PubMqttIvData { bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; - char mVal[100]; + char mVal[140]; bool mZeroValues; // makes sure that yield day is sent even if no inverter is online std::queue *mSendList; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 21d03e70..283674c2 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de +// 2024 Ahoy, https://ahoydtu.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -616,7 +616,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) { @@ -701,10 +702,11 @@ class RestApi { void getIndex(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("ts_now")] = mApp->getTimestamp(); - obj[F("ts_sunrise")] = mApp->getSunrise(); - obj[F("ts_sunset")] = mApp->getSunset(); - obj[F("ts_offset")] = mConfig->sun.offsetSec; + obj[F("ts_now")] = mApp->getTimestamp(); + obj[F("ts_sunrise")] = mApp->getSunrise(); + obj[F("ts_sunset")] = mApp->getSunset(); + obj[F("ts_offsSr")] = mConfig->sun.offsetSecMorning; + obj[F("ts_offsSs")] = mConfig->sun.offsetSecEvening; JsonArray inv = obj.createNestedArray(F("inverter")); Inverter<> *iv; diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index 5ac2e0b2..f14babe3 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -17,7 +17,7 @@ {"0x2600": "BE_C10_26"}, {"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"}, - {"0x3700": "CH_NA EEA-NE7–CH2020"} + {"0x3700": "CH_NA EEA-NE7–CH2020"}, {"0xe100": "LN_50Hz"} ], "grp_codes": [ diff --git a/src/web/html/index.html b/src/web/html/index.html index baa70742..3ac72e89 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -45,12 +45,12 @@ function apiCb(obj) { var e = document.getElementById("apiResult"); - if(obj["success"]) { + if(obj.success) { e.innerHTML = " command executed"; getAjax("/api/index", parse); } else - e.innerHTML = " Error: " + obj["error"]; + e.innerHTML = " Error: " + obj.error; } function setTime() { @@ -68,9 +68,9 @@ } function parseSys(obj) { - ts = obj["ts_now"]; - var date = new Date(obj["ts_now"] * 1000); - var up = obj["generic"]["ts_uptime"]; + ts = obj.ts_now; + var date = new Date(obj.ts_now * 1000); + var up = obj.generic["ts_uptime"]; var days = parseInt(up / 86400) % 365; var hrs = parseInt(up / 3600) % 24; var min = parseInt(up / 60) % 60; @@ -83,8 +83,8 @@ + ("0"+min).substr(-2) + ":" + ("0"+sec).substr(-2); var dSpan = document.getElementById("date"); - if(0 != obj["ts_now"]) { - if(obj["ts_now"] < 1680000000) + if(0 != obj.ts_now) { + if(obj.ts_now < 1680000000) setTime(); else dSpan.innerHTML = toIsoDateStr(date); @@ -98,18 +98,18 @@ e.addEventListener("click", setTime); } - if(obj["disNightComm"]) { - if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) - && ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { - commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + if(obj.disNightComm) { + if(((obj.ts_sunrise - obj.ts_offsSr) < obj.ts_now) + && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) { + commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { commInfo = "Night time, inverter polling disabled, "; - if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { - commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + if(obj.ts_now > (obj.ts_sunrise - obj.ts_offsSr)) { + commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); + commInfo += "will start polling at " + (new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')); } } } @@ -190,11 +190,11 @@ function parse(obj) { if(null != obj) { if(exeOnce) - parseNav(obj["generic"]); - parseGeneric(obj["generic"]); + parseNav(obj.generic); + parseGeneric(obj.generic); parseSys(obj); - parseIv(obj["inverter"], obj.ts_now); - parseWarn(obj["warnings"]); + parseIv(obj.inverter, obj.ts_now); + parseWarn(obj.warnings); if(exeOnce) { window.setInterval("tick()", 1000); exeOnce = false; @@ -210,7 +210,7 @@ } function parseRelease(obj) { - release = obj["name"].substring(6); + release = obj.name.substring(6); getAjax("/api/index", parse); } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 76e65b35..e7c83ff6 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -233,8 +233,12 @@
-
Offset (pre sunrise, post sunset)
-
+
Offset (sunrise)
+
+
+
+
Offset (sunset)
+
@@ -936,9 +940,11 @@ function parseSun(obj) { document.getElementsByName("sunLat")[0].value = obj["lat"]; document.getElementsByName("sunLon")[0].value = obj["lon"]; - const sel = document.getElementsByName("sunOffs")[0]; - for(var i = 0; i <= 60; i++) { - sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); + for(p of [["sunOffsSr", "offsSr"], ["sunOffsSs", "offsSs"]]) { + const sel = document.getElementsByName(p[0])[0]; + for(var i = 0; i <= 60; i++) { + sel.appendChild(opt(i, i + " minutes", (i == (obj[p[1]] / 60)))); + } } } diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 469fcfb7..179008c5 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -400,7 +400,7 @@ var html = ml("div", {}, [ ml("div", {class: "row mb-3"}, [ ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"), - ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")), + ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number", step: "0.1", min: 1}, "")), ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct")) ]), ml("div", {class: "row mb-3"}, [ @@ -451,7 +451,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)); } diff --git a/src/web/web.h b/src/web/web.h index 221567fa..d7f93dd8 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- @@ -541,11 +541,13 @@ class Web { if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { mConfig->sun.lat = 0.0; mConfig->sun.lon = 0.0; - mConfig->sun.offsetSec = 0; + mConfig->sun.offsetSecMorning = 0; + mConfig->sun.offsetSecEvening = 0; } else { mConfig->sun.lat = request->arg("sunLat").toFloat(); mConfig->sun.lon = request->arg("sunLon").toFloat(); - mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; + mConfig->sun.offsetSecMorning = request->arg("sunOffsSr").toInt() * 60; + mConfig->sun.offsetSecEvening = request->arg("sunOffsSs").toInt() * 60; } // mqtt @@ -656,17 +658,45 @@ class Web { #ifdef ENABLE_PROMETHEUS_EP // Note // Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md - // TODO: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) - enum { - metricsStateStart, - metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, - metricStateRealtimeFieldId, metricStateRealtimeInverterId, + // NOTE: Grouping for fields with channels and totals is currently not working + // TODO: Handle grouping and sorting for independant from channel number + // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) + const char * metricPrefix = "ahoy_solar_"; + typedef enum { + metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterProducing=3, + metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterMaxPower=6, + metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, metricsStateInverterRxFailAnswer=9, + metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, metricsStateInverterRetransmits=12, + metricStateRealtimeFieldId=metricsStateInverterRetransmits+1, // ensure that this state follows the last per_inverter state + metricStateRealtimeInverterId, metricsStateAlarmData, + metricsStateStart, metricsStateEnd - } metricsStep; + } MetricStep_t; + MetricStep_t metricsStep; + typedef struct { + const char *type; + const char *format; + const std::function *iv)> valueFunc; + } InverterMetric_t; + InverterMetric_t inverterMetrics[13] = { + { "info", "info{name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, + { "is_enabled", "is_enabled {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, + { "is_available", "is_available {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, + { "is_producing", "is_producing {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, + { "power_limit_read", "power_limit_read {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, + { "power_limit_ack", "power_limit_ack {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, + { "max_power", "max_power {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, + { "radio_rx_success", "radio_rx_success {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, + { "radio_rx_fail", "radio_rx_fail {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, + { "radio_rx_fail_answer", "radio_rx_fail_answer {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, + { "radio_frame_cnt", "radio_frame_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, + { "radio_tx_cnt", "radio_tx_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, + { "radio_retransmits", "radio_retransmits {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} } + }; int metricsInverterId; uint8_t metricsFieldId; - bool metricDeclared; + bool metricDeclared, metricTotalDeclard; void showMetrics(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); @@ -687,79 +717,58 @@ class Web { // Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. // So several "Info:" blocks are used to keep the transmission going switch (metricsStep) { - case metricsStateStart: // System Info & NRF Statistics : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n", + case metricsStateStart: // System Info : fit to one packet + snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricPrefix, mApp->getVersion(), mConfig->sys.deviceName); metrics = String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); + snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricPrefix,mConfig->sys.deviceName,ESP.getFreeHeap()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime()); + snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricPrefix); + snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricPrefix, mConfig->sys.deviceName, mApp->getUptime()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI()); + snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricPrefix); + snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricPrefix, mConfig->sys.deviceName, WiFi.RSSI()); metrics += String(type) + String(topic); - // NRF Statistics - // @TODO 2023-10-01: the statistic data is now available per inverter - /*stat = mApp->getNrfStatistics(); - metrics += radioStatistic(F("rx_success"), stat->rxSuccess); - metrics += radioStatistic(F("rx_fail"), stat->rxFail); - metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); - metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); - metrics += radioStatistic(F("tx_cnt"), stat->txCnt); - metrics += radioStatistic(F("retrans_cnt"), stat->retransmits);*/ - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); // Next is Inverter information - metricsInverterId = 0; - metricsStep = metricsStateInverter1; + metricsStep = metricsStateInverterInfo; break; - case metricsStateInverter1: // Information about all inverters configured : fit to one packet - metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); + // Information about all inverters configured : each metric for all inverters must fit to one network packet + case metricsStateInverterInfo: + case metricsStateInverterEnabled: + case metricsStateInverterAvailable: + case metricsStateInverterProducing: + case metricsStateInverterPowerLimitRead: + case metricsStateInverterPowerLimitAck: + case metricsStateInverterMaxPower: + case metricsStateInverterRxSuccess: + case metricsStateInverterRxFail: + case metricsStateInverterRxFailAnswer: + case metricsStateInverterFrameCnt: + case metricsStateInverterTxCnt: + case metricsStateInverterRetransmits: + metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].type) + " gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].valueFunc); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter2; - break; - - case metricsStateInverter2: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;}); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter3; - break; - - case metricsStateInverter3: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable();}); - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - metricsStep = metricsStateInverter4; - break; - - case metricsStateInverter4: // Information about all inverters configured : fit to one packet - metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", - [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing();}); - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Realtime Field loop + // ugly hack to increment the enum + metricsStep = static_cast( static_cast(metricsStep) + 1); + // Prepare Realtime Field loop, which may be startet next metricsFieldId = FLD_UDC; - metricsStep = metricStateRealtimeFieldId; break; + case metricStateRealtimeFieldId: // Iterate over all defined fields if (metricsFieldId < FLD_LAST_ALARM_CODE) { metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; metricDeclared = false; + metricTotalDeclard = false; metricsInverterId = 0; metricsStep = metricStateRealtimeInverterId; @@ -774,7 +783,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); @@ -788,22 +796,27 @@ class Web { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); // Declare metric only once if (channel != 0 && !metricDeclared) { - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); metrics += type; metricDeclared = true; } // report value if (0 == channel) { + // Report a _total value if also channel values were reported. Otherwise report without _total char total[7]; total[0] = 0; if (metricDeclared) { - // A declaration and value for channels has been delivered. So declare and deliver a _total metric + // A declaration and value for channels have been delivered. So declare and deliver a _total metric strncpy(total,"_total",sizeof(total)); } - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); - metrics += type; - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); + if (!metricTotalDeclard) { + snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); + metrics += type; + metricTotalDeclard = true; + } + snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); } else { + // Report (non zero) channel value // Use a fallback channel name (ch0, ch1, ...)if non is given by user char chName[MAX_NAME_LENGTH]; if (iv->config->chName[channel-1][0] != 0) { @@ -811,7 +824,7 @@ class Web { } else { snprintf(chName,sizeof(chName),"ch%1d",channel); } - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); } snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); metrics += topic; @@ -841,7 +854,7 @@ class Web { case metricsStateAlarmData: // Alarm Info loop : fit to one packet // Perform grouping on metrics according to Prometheus exposition format specification - snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); + snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricPrefix,fields[FLD_LAST_ALARM_CODE]); metrics = type; for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { @@ -853,7 +866,7 @@ class Web { alarmChannelId = 0; if (alarmChannelId < rec->length) { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); metrics += topic; metrics += val; @@ -864,11 +877,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); @@ -876,27 +891,19 @@ class Web { // Traverse all inverters and collect the metric via valueFunc - String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv, IApp *mApp)> valueFunc) { + String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv)> valueFunc) { Inverter<> *iv; String metric = ""; for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { iv = mSys->getInverterByPos(metricsInverterId); if (NULL != iv) { - snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp)); + snprintf(buffer,len,format,iv->config->name, valueFunc(iv)); metric += String(buffer); } } return metric; } - String radioStatistic(String statistic, uint32_t value) { - char type[60], topic[80], val[25]; - snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); - snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str()); - snprintf(val, sizeof(val), "%d", value); - return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n"); - } - std::pair convertToPromUnits(String shortUnit) { if(shortUnit == "A") return {"_ampere", "gauge"}; if(shortUnit == "V") return {"_volt", "gauge"};