diff --git a/src/CHANGES.md b/src/CHANGES.md index 94b1d10a..6b97e319 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,13 @@ # Development Changes +## 0.6.15 - 2023-05-25 +* improved Prometheus Endpoint PR #958 +* fix turn off ePaper only if setting was set #956 +* improved reset values and update MqTT #957 + +## 0.6.14 - 2023-05-21 +* merge PR #902 Mono-Display + ## 0.6.13 - 2023-05-16 * merge PR #934 (fix JSON API) and #944 (update manual) diff --git a/src/app.cpp b/src/app.cpp index d426ff92..88ad2bd4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -322,6 +322,7 @@ void app::tickComm(void) { //----------------------------------------------------------------------------- void app::tickZeroValues(void) { Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); @@ -329,7 +330,11 @@ void app::tickZeroValues(void) { continue; // skip to next inverter mPayload.zeroInverterValues(iv); + changed = true; } + + if(changed) + payloadEventListener(RealTimeRunData_Debug); } //----------------------------------------------------------------------------- @@ -337,15 +342,21 @@ void app::tickMinute(void) { // only triggered if 'reset values on no avail is enabled' Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter - if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) { mPayload.zeroInverterValues(iv); + changed = true; + } } + + if(changed) + payloadEventListener(RealTimeRunData_Debug); } //----------------------------------------------------------------------------- @@ -356,16 +367,20 @@ void app::tickMidnight(void) { onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); Inverter<> *iv; + bool changed = false; // set values to zero, except yield total for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter - mPayload.zeroInverterValues(iv); - mPayload.zeroYieldDay(iv); + mPayload.zeroInverterValues(iv, false); + changed = true; } + if(changed) + payloadEventListener(RealTimeRunData_Debug); + if (mMqttEnabled) mMqtt.tickerMidnight(); } diff --git a/src/defines.h b/src/defines.h index 22f5c8fb..baaea8b6 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 13 +#define VERSION_PATCH 15 //------------------------------------- typedef struct { diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index 7b0caaa1..1b36e26a 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -95,17 +95,7 @@ class HmPayload { notify(0x0b); }*/ - void zeroYieldDay(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - pos = iv->getPosByChFld(ch, FLD_YD, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - void zeroInverterValues(Inverter<> *iv) { + void zeroInverterValues(Inverter<> *iv, bool skipYieldDay = true) { DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for(uint8_t ch = 0; ch <= iv->channels; ch++) { @@ -113,15 +103,18 @@ class HmPayload { for(uint8_t fld = 0; fld < FLD_EVT; fld++) { switch(fld) { case FLD_YD: + if(skipYieldDay) + continue; + else + break; case FLD_YT: continue; } pos = iv->getPosByChFld(ch, fld, rec); iv->setValue(pos, rec, 0.0f); } + iv->doCalculations(); } - - notify(RealTimeRunData_Debug); } void ivSendHighPrio(Inverter<> *iv) { diff --git a/src/platformio.ini b/src/platformio.ini index a8fa0d03..904f6120 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -30,7 +30,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.2 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.34.17 - zinggjm/GxEPD2 @ ^1.5.0 + zinggjm/GxEPD2 @ ^1.5.2 [env:esp8266-release] @@ -98,9 +98,9 @@ monitor_filters = log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:esp32-wroom32-release] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 +build_flags = -D RELEASE -std=gnu++17 build_unflags = -std=gnu++11 monitor_filters = ;default ; Remove typical terminal control codes from input @@ -109,10 +109,10 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-release-prometheus] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 build_flags = -D RELEASE - -std=gnu++14 + -std=gnu++17 -DENABLE_PROMETHEUS_EP build_unflags = -std=gnu++11 monitor_filters = @@ -122,7 +122,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-debug] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE @@ -131,7 +131,7 @@ build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial - -std=gnu++14 + -std=gnu++17 build_unflags = -std=gnu++11 build_type = debug monitor_filters = @@ -140,13 +140,13 @@ monitor_filters = log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:opendtufusionv1-release] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin upload_speed = 115200 debug_tool = esp-builtin debug_speed = 12000 -build_flags = -D RELEASE -std=gnu++14 +build_flags = -D RELEASE -std=gnu++17 build_unflags = -std=gnu++11 monitor_filters = ;default ; Remove typical terminal control codes from input diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 1a0222b2..ba187c7d 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -7,108 +7,126 @@ #include "../../hm/hmSystem.h" #include "../../utils/helper.h" #include "Display_Mono.h" +#include "Display_Mono_128X32.h" +#include "Display_Mono_128X64.h" +#include "Display_Mono_84X48.h" #include "Display_ePaper.h" template class Display { public: - Display() {} + Display() {} - void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { - mCfg = cfg; - mSys = sys; - mUtcTs = utcTs; + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { + mCfg = cfg; + mSys = sys; + mUtcTs = utcTs; + mNewPayload = false; + mLoopCnt = 0; + mVersion = version; + + if (mCfg->type == 0) + return; + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + switch (mCfg->type) { + case 2: + case 1: + default: + mMono = new DisplayMono128X64(); + break; + case 3: + mMono = new DisplayMono84X48(); + break; + case 4: + mMono = new DisplayMono128X32(); + break; + } + mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); + mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + } else if (mCfg->type >= 10) { +#if defined(ESP32) + mRefreshCycle = 0; + mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline); + mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); +#endif + } + } + + void payloadEventListener(uint8_t cmd) { + mNewPayload = true; + } + + void tickerSecond() { + if (mMono != NULL) + mMono->loop(); + + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { mNewPayload = false; mLoopCnt = 0; - mVersion = version; + DataScreen(); + } + } - if (mCfg->type == 0) - return; + private: + void DataScreen() { + if (mCfg->type == 0) + return; + if (*mUtcTs == 0) + return; - if ((0 < mCfg->type) && (mCfg->type < 10)) { - mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); - mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); - } else if (mCfg->type >= 10) { - #if defined(ESP32) - mRefreshCycle = 0; - mEpaper.config(mCfg->rot); - mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); - #endif - } + float totalPower = 0; + float totalYieldDay = 0; + float totalYieldTotal = 0; + + uint8_t isprod = 0; + + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + + if (iv->isProducing(*mUtcTs)) + isprod++; + + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); } - void payloadEventListener(uint8_t cmd) { - mNewPayload = true; + if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) { + mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type >= 10) { +#if defined(ESP32) + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mRefreshCycle++; +#endif } - void tickerSecond() { - mMono.loop(); - if (mNewPayload || ((++mLoopCnt % 10) == 0)) { - mNewPayload = false; - mLoopCnt = 0; - DataScreen(); - } +#if defined(ESP32) + if (mRefreshCycle > 480) { + mEpaper.fullRefresh(); + mRefreshCycle = 0; } +#endif + } - private: - void DataScreen() { - if (mCfg->type == 0) - return; - if (*mUtcTs == 0) - return; + // private member variables + bool mNewPayload; + uint8_t mLoopCnt; + uint32_t *mUtcTs; + const char *mVersion; + display_t *mCfg; + HMSYSTEM *mSys; + uint16_t mRefreshCycle; - float totalPower = 0; - float totalYieldDay = 0; - float totalYieldTotal = 0; - - uint8_t isprod = 0; - - Inverter<> *iv; - record_t<> *rec; - for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { - iv = mSys->getInverterByPos(i); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv == NULL) - continue; - - if (iv->isProducing(*mUtcTs)) - isprod++; - - totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); - totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); - } - - if ((0 < mCfg->type) && (mCfg->type < 10)) { - mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); - } else if (mCfg->type >= 10) { - #if defined(ESP32) - mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); - mRefreshCycle++; - #endif - } - - #if defined(ESP32) - if (mRefreshCycle > 480) { - mEpaper.fullRefresh(); - mRefreshCycle = 0; - } - #endif - } - - // private member variables - bool mNewPayload; - uint8_t mLoopCnt; - uint32_t *mUtcTs; - const char *mVersion; - display_t *mCfg; - HMSYSTEM *mSys; - uint16_t mRefreshCycle; - - #if defined(ESP32) - DisplayEPaper mEpaper; - #endif - DisplayMono mMono; +#if defined(ESP32) + DisplayEPaper mEpaper; +#endif + DisplayMono *mMono; }; #endif /*__DISPLAY__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp deleted file mode 100644 index d55b6061..00000000 --- a/src/plugins/Display/Display_Mono.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "Display_Mono.h" - -#ifdef ESP8266 - #include -#elif defined(ESP32) - #include -#endif -#include "../../utils/helper.h" - -//#ifdef U8X8_HAVE_HW_SPI -//#include -//#endif -//#ifdef U8X8_HAVE_HW_I2C -//#include -//#endif - -DisplayMono::DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = true; - mLuminance = 60; - _dispY = 0; - mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) - mUtcTs = NULL; - mType = 0; -} - - - -void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { - if ((0 < type) && (type < 4)) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - mType = type; - switch(type) { - case 1: - mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - default: - case 2: - mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - case 3: - mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); - break; - } - - mUtcTs = utcTs; - - mDisplay->begin(); - - mIsLarge = (mDisplay->getWidth() > 120); - calcLineHeights(); - - mDisplay->clearBuffer(); - if (3 != mType) - mDisplay->setContrast(mLuminance); - printText("AHOY!", 0, 35); - printText("ahoydtu.de", 2, 20); - printText(version, 3, 46); - mDisplay->sendBuffer(); - } -} - -void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - mEnPowerSafe = enPowerSafe; - mEnScreenSaver = enScreenSaver; - mLuminance = lum; -} - -void DisplayMono::loop(void) { - if (mEnPowerSafe) - if(mTimeout != 0) - mTimeout--; -} - -void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { - - - mDisplay->clearBuffer(); - - // set Contrast of the Display to raise the lifetime - if (3 != mType) - mDisplay->setContrast(mLuminance); - - if ((totalPower > 0) && (isprod > 0)) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); - if (totalPower > 999) { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); - } else { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); - } - printText(_fmtText, 0); - } else { - printText("offline", 0, 25); - // check if it's time to enter power saving mode - if (mTimeout == 0) - mDisplay->setPowerSave(mEnPowerSafe); - } - - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); - printText(_fmtText, 1); - - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); - printText(_fmtText, 2); - - IPAddress ip = WiFi.localIP(); - if (!(_mExtra % 10) && (ip)) { - printText(ip.toString().c_str(), 3); - } else if (!(_mExtra % 5)) { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); - printText(_fmtText, 3); - } else { - if(mIsLarge && (NULL != mUtcTs)) - printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - else - printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - } - - mDisplay->sendBuffer(); - - _dispY = 0; - _mExtra++; -} - -void DisplayMono::calcLineHeights() { - uint8_t yOff = 0; - for (uint8_t i = 0; i < 4; i++) { - setFont(i); - yOff += (mDisplay->getMaxCharHeight()); - mLineOffsets[i] = yOff; - } -} - -inline void DisplayMono::setFont(uint8_t line) { - switch (line) { - case 0: - mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); - break; - case 3: - mDisplay->setFont(u8g2_font_5x8_tr); - break; - default: - mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); - break; - } -} - -void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) { - if (!mIsLarge) { - dispX = (line == 0) ? 10 : 5; - } - setFont(line); - - dispX += (mEnScreenSaver) ? (_mExtra % 7) : 0; - mDisplay->drawStr(dispX, mLineOffsets[line], text); -} diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index ad04c9f4..ed9154af 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -1,38 +1,45 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- +#pragma once #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds -#define DISP_FMT_TEXT_LEN 32 +#define DISP_FMT_TEXT_LEN 32 +#define BOTTOM_MARGIN 5 + + +#ifdef ESP8266 +#include +#elif defined(ESP32) +#include +#endif +#include "../../utils/helper.h" class DisplayMono { public: - DisplayMono(); + DisplayMono() {}; - void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version); - void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum); - void loop(void); - void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); - - private: - void calcLineHeights(); - void setFont(uint8_t line); - void printText(const char* text, uint8_t line, uint8_t dispX = 5); + virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t* utcTs, const char* version) = 0; + virtual void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0; + virtual void loop(void) = 0; + virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 0; + protected: U8G2* mDisplay; uint8_t mType; bool mEnPowerSafe, mEnScreenSaver; uint8_t mLuminance; - bool mIsLarge = false; uint8_t mLoopCnt; uint32_t* mUtcTs; - uint8_t mLineOffsets[5]; + uint8_t mLineXOffsets[5]; + uint8_t mLineYOffsets[5]; - uint16_t _dispY; + uint16_t mDispY; - uint8_t _mExtra; + uint8_t mExtra; uint16_t mTimeout; - char _fmtText[DISP_FMT_TEXT_LEN]; -}; + char mFmtText[DISP_FMT_TEXT_LEN];}; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h new file mode 100644 index 00000000..9d5ade7e --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X32 : public DisplayMono { + public: + DisplayMono128X32() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data); + + mUtcTs = utcTs; + + mDisplay->begin(); + + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff[] = {0, 0}; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff[getColumn(i)] += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff[getColumn(i)]; + if (isTwoRowLine(i)) + yOff[getColumn(i)] += mDisplay->getMaxCharHeight(); + yOff[getColumn(i)] += BOTTOM_MARGIN; + mLineXOffsets[i] = (getColumn(i) == 1 ? 80 : 0); + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_9x15_tf); + break; + case 3: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + default: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + } + } + + inline uint8_t getColumn(uint8_t line) { + if (line >= 1 && line <= 2) + return 1; + else + return 0; + } + + inline bool isTwoRowLine(uint8_t line) { + return ((line >= 1) && (line <= 2)); + } + + void printText(const char *text, uint8_t line) { + setFont(line); + + uint8_t dispX = mLineXOffsets[line] + ((mEnScreenSaver) ? (mExtra % 7) : 0); + + if (isTwoRowLine(line)) { + String stringText = String(text); + int space = stringText.indexOf(" "); + mDisplay->drawStr(dispX, mLineYOffsets[line], stringText.substring(0, space).c_str()); + if (space > 0) + mDisplay->drawStr(dispX, mLineYOffsets[line] + mDisplay->getMaxCharHeight(), stringText.substring(space + 1).c_str()); + } else + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h new file mode 100644 index 00000000..3d4f91ee --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X64 : public DisplayMono { + public: + DisplayMono128X64() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + + switch (type) { + case 1: + mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + default: + case 2: + mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + } + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0, 25); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_ncenB14_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_ncenB10_tr); + break; + } + } + void printText(const char *text, uint8_t line, uint8_t dispX = 5) { + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h new file mode 100644 index 00000000..82aa83fa --- /dev/null +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono84X48 : public DisplayMono { + public: + DisplayMono84X48() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + if (3 != mType) + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mExtra = 1; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_logisoso16_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + } + } + + void printText(const char *text, uint8_t line) { + uint8_t dispX = (line == 0) ? 10 : 5; + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 99d35ed8..924961a3 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -57,8 +57,9 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u } } -void DisplayEPaper::config(uint8_t rotation) { +void DisplayEPaper::config(uint8_t rotation, bool enPowerSafe) { mDisplayRotation = rotation; + mEnPowerSafe = enPowerSafe; } //*************************************************************************** @@ -120,7 +121,29 @@ void DisplayEPaper::lastUpdatePaged() { } while (_display->nextPage()); } //*************************************************************************** -void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { +void DisplayEPaper::offlineFooter() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + do { + if (NULL != mUtcTs) { + snprintf(_fmtText, sizeof(_fmtText), "offline"); + + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (_display->height() - 3)); + _display->println(_fmtText); + } + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { int16_t tbx, tby; uint16_t tbw, tbh, x, y; @@ -130,47 +153,52 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl _display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); _display->fillScreen(GxEPD_WHITE); do { - if (_totalPower > 9999) { - snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); + if (totalPower > 9999) { + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 10000)); _changed = true; - } else if ((_totalPower > 0) && (_totalPower <= 9999)) { - snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); + } else if ((totalPower > 0) && (totalPower <= 9999)) { + snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower); _changed = true; } else { snprintf(_fmtText, sizeof(_fmtText), "offline"); } - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, mHeadFootPadding + tbh + 10); - _display->print(_fmtText); + if (totalPower == 0){ + _display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + } else { + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, mHeadFootPadding + tbh + 10); + _display->print(_fmtText); - _display->setFont(&FreeSans12pt7b); - y = _display->height() / 2; - _display->setCursor(5, y); - _display->print("today:"); - snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 38, y); - _display->println("Wh"); + _display->setFont(&FreeSans12pt7b); + y = _display->height() / 2; + _display->setCursor(5, y); + _display->print("today:"); + snprintf(_fmtText, _display->width(), "%.0f", totalYieldDay); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 38, y); + _display->println("Wh"); - y = y + tbh + 7; - _display->setCursor(5, y); - _display->print("total:"); - snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 50, y); - _display->println("kWh"); + y = y + tbh + 7; + _display->setCursor(5, y); + _display->print("total:"); + snprintf(_fmtText, _display->width(), "%.1f", totalYieldTotal); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 50, y); + _display->println("kWh"); - _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); - snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); - _display->println(_fmtText); + _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod); + _display->println(_fmtText); + } } while (_display->nextPage()); } //*************************************************************************** @@ -185,11 +213,12 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield // call the PowerPage to change the PV Power Values actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); - // if there was an change and the Inverter is producing set a new Timestam in the footline + // if there was an change and the Inverter is producing set a new Timestamp in the footline if ((isprod > 0) && (_changed)) { _changed = false; lastUpdatePaged(); - } + } else if((0 == totalPower) && (mEnPowerSafe)) + offlineFooter(); _display->powerOff(); } diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index b2729f25..ad422b26 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -31,7 +31,7 @@ class DisplayEPaper { DisplayEPaper(); void fullRefresh(); void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); - void config(uint8_t rotation); + void config(uint8_t rotation, bool enPowerSafe); void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); @@ -39,6 +39,7 @@ class DisplayEPaper { void headlineIP(); void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); void lastUpdatePaged(); + void offlineFooter(); uint8_t mDisplayRotation; bool _changed = false; @@ -47,6 +48,7 @@ class DisplayEPaper { uint8_t mHeadFootPadding; GxEPD2_GFX* _display; uint32_t *mUtcTs; + bool mEnPowerSafe; }; #endif // ESP32 diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 049554d7..1f8519f5 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -564,7 +564,7 @@ class PubMqtt { void sendIvData() { bool anyAvail = processIvStatus(); if (mLastAnyAvail != anyAvail) - mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated + mSendList.push(RealTimeRunData_Debug); // makes sure that total values are calculated if(mSendList.empty()) return; diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 649491f0..f717c2e3 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -784,7 +784,7 @@ ); } - var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]]; + var opts = [[0, "None"], [1, "SSD1306 0.96\" 128X64"], [2, "SH1106 1.3\""], [3, "Nokia5110"], [4, "SSD1306 0.96\" 128X32"]]; if("ESP32" == type) opts.push([10, "ePaper"]); var dispType = sel("disp_typ", opts, obj["disp_typ"]); @@ -820,7 +820,7 @@ if(0 == dispType) cl.add("hide"); - else if(dispType <= 2) { // OLED + else if(dispType <= 2 || dispType == 4) { // OLED if(i < 2) cl.remove("hide"); else diff --git a/src/web/web.h b/src/web/web.h index ff774334..d1a0a4a7 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -642,14 +642,22 @@ 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. Successfull Tested with 4 Inverters (each with 4 channels) enum { - metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd + metricsStateStart, + metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, + metricStateRealtimeFieldId, metricStateRealtimeInverterId, + metricsStateAlarmData, + metricsStateEnd } metricsStep; - int metricsInverterId,metricsChannelId; + int metricsInverterId; + uint8_t metricsFieldId; + bool metricDeclared; void showMetrics(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); - metricsStep = metricsStateStart; AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t @@ -662,12 +670,16 @@ class Web { char type[60], topic[100], val[25]; size_t len = 0; int alarmChannelId; + int metricsChannelId; + // Perform grouping on metrics according to format specification + // 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", - mApp->getVersion(), mConfig->sys.deviceName); + mApp->getVersion(), mConfig->sys.deviceName); metrics = String(type) + String(topic); snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n"); @@ -694,93 +706,137 @@ class Web { metrics += radioStatistic(F("retrans_cnt"), *nrfRetransmits); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Inverter loop + // Next is Inverter information metricsInverterId = 0; - metricsStep = metricsStateInverter; + metricsStep = metricsStateInverter1; break; - case metricsStateInverter: // Inverter loop - if (metricsInverterId < mSys->getNumInverters()) { - iv = mSys->getInverterByPos(metricsInverterId); - if(NULL != iv) { - // Inverter info : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - iv->config->name, iv->config->serial.u64); - metrics = String(type) + String(topic); + case metricsStateInverter1: // Information about all inverters configured : fit to one packet + metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter2; + break; - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); - metrics += String(type) + String(topic); + 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;}); - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); - metrics += String(type) + String(topic); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter3; + break; - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); - metrics += String(type) + String(topic); + 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(mApp->getTimestamp());}); + 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(mApp->getTimestamp());}); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - - // Start Realtime Data Channel loop for this inverter - metricsChannelId = 0; - metricsStep = metricStateRealtimeData; - } - } else { - metricsStep = metricsStateEnd; - } + // Start Realtime Field loop + metricsFieldId = FLD_UDC; + metricsStep = metricStateRealtimeFieldId; break; - case metricStateRealtimeData: // Realtime Data Channel loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (metricsChannelId < rec->length) { - uint8_t channel = rec->assign[metricsChannelId].ch; - // Skip entry if maxPwr is 0 and it's not the inverter channel (channel 0) - if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); - if (0 == channel) { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); - } else { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); - } - snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); - } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - } + case metricStateRealtimeFieldId: // Iterate over all defined fields + if (metricsFieldId < FLD_LAST_ALARM_CODE) { + metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; + metricDeclared = false; - metricsChannelId++; + metricsInverterId = 0; + metricsStep = metricStateRealtimeInverterId; } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - - // All realtime data channels processed --> try alarm data + metrics = "# Info: all realtime fields processed\n"; metricsStep = metricsStateAlarmData; } + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); break; - case metricsStateAlarmData: // Alarm Info loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(AlarmData); - // simple hack : there is only one channel with alarm data - // TODO: find the right one channel with the alarm id - alarmChannelId = 0; - // printf("AlarmData Length %d\n",rec->length); - if (alarmChannelId < rec->length) { - //uint8_t channel = rec->assign[alarmChannelId].ch; - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); - snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); + case metricStateRealtimeInverterId: // Iterate over all inverters for this field + metrics = ""; + if (metricsInverterId < mSys->getNumInverters()) { + // process all channels of this inverter + + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(RealTimeRunData_Debug); + for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) { + uint8_t channel = rec->assign[metricsChannelId].ch; + + // Try inverter channel (channel 0) or any channel with maxPwr > 0 + if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { + if (metricsFieldId == iv->getByteAssign(metricsChannelId, rec)->fieldId) { + // This is the correct field to report + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); + // Declare metric only once + if (!metricDeclared) { + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + metrics += type; + metricDeclared = true; + } + // report value + if (0 == channel) { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); + } else { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); + } + snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); + metrics += topic; + metrics += val; + } + } + } + if (metrics.length() < 1) { + metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; + } + } else { + metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; + } + // Stay in this state and try next inverter + metricsInverterId++; } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; } - // alarm channel processed --> try next inverter - metricsInverterId++; - metricsStep = metricsStateInverter; + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + break; + + 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]); + metrics = type; + + for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(AlarmData); + // simple hack : there is only one channel with alarm data + // TODO: find the right one channel with the alarm id + alarmChannelId = 0; + 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(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); + metrics += topic; + metrics += val; + } + } + } + len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateEnd; break; case metricsStateEnd: @@ -793,6 +849,21 @@ class Web { request->send(response); } + + // 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) { + 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)); + 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());