diff --git a/src/app.cpp b/src/app.cpp index 0bba14da..9ec3d86c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -7,7 +7,7 @@ #include "app.h" #include "utils/sun.h" - +#include "plugins/history.h" //----------------------------------------------------------------------------- app::app() : ah::Scheduler {} {} @@ -91,6 +91,11 @@ void app::setup() { #endif #endif + mTotalPowerHistory = new TotalPowerHistory(); + mTotalPowerHistory->setup(this, &mSys, mConfig); + mYieldDayHistory = new YieldDayHistory(); + mYieldDayHistory->setup(this, &mSys, mConfig); + mPubSerial.setup(mConfig, &mSys, &mTimestamp); #if !defined(ETHERNET) @@ -148,6 +153,9 @@ void app::regularTickers(void) { #if !defined(ETHERNET) //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif + + everySec(std::bind(&TotalPowerHistory::tickerSecond, mTotalPowerHistory), "totalPowerHistory"); + everySec(std::bind(&YieldDayHistory::tickerSecond, mYieldDayHistory), "yieldDayHistory"); } #if defined(ETHERNET) diff --git a/src/app.h b/src/app.h index a24cccb3..029f0f1e 100644 --- a/src/app.h +++ b/src/app.h @@ -55,6 +55,7 @@ typedef PubSerial PubSerialType; #include "plugins/Display/Display_data.h" typedef Display DisplayType; #endif +#include "plugins/history.h" class app : public IApp, public ah::Scheduler { public: @@ -243,6 +244,9 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } + TotalPowerHistory *getTotalPowerHistoryPtr() { return mTotalPowerHistory; }; + YieldDayHistory *getYieldDayHistoryPtr() { return mYieldDayHistory; }; + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true @@ -350,6 +354,8 @@ class app : public IApp, public ah::Scheduler { DisplayType mDisplay; DisplayData mDispData; #endif + TotalPowerHistory *mTotalPowerHistory; + YieldDayHistory *mYieldDayHistory; }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 34dc5ddc..89d52d9e 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -14,6 +14,8 @@ #include "ESPAsyncWebServer.h" #endif +class TotalPowerHistory; +class YieldDayHistory; //#include "hms/hmsRadio.h" #if defined(ESP32) //typedef CmtRadio> CmtRadioType; @@ -63,6 +65,8 @@ class IApp { virtual bool getProtection(AsyncWebServerRequest *request) = 0; + virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0; + virtual YieldDayHistory *getYieldDayHistoryPtr() = 0; virtual void* getRadioObj(bool nrf) = 0; }; diff --git a/src/config/config.h b/src/config/config.h index 2cb9bcd3..393c78e3 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -36,6 +36,9 @@ // CONFIGURATION - COMPILE TIME //------------------------------------- +// Draw power chart in MONO-Display +#define DISPLAY_CHART 1 + // ethernet #if defined(ETHERNET) diff --git a/src/config/settings.h b/src/config/settings.h index fe2ad1b0..ec61d733 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -41,6 +41,7 @@ #define PROT_MASK_SYSTEM 0x0020 #define PROT_MASK_API 0x0040 #define PROT_MASK_MQTT 0x0080 +#define PROT_MASK_HISTORY 0x0100 #define DEF_PROT_INDEX 0x0001 #define DEF_PROT_LIVE 0x0000 @@ -50,7 +51,7 @@ #define DEF_PROT_SYSTEM 0x0020 #define DEF_PROT_API 0x0000 #define DEF_PROT_MQTT 0x0000 - +#define DEF_PROT_HISTORY 0x0000 typedef struct { uint8_t ip[4]; // ip address @@ -373,7 +374,7 @@ class settings { // erase all settings and reset to default memset(&mCfg, 0, sizeof(settings_t)); mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP - | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; mCfg.sys.darkMode = false; mCfg.sys.schedReboot = false; // restore temp settings @@ -546,7 +547,7 @@ class settings { if(mCfg.sys.protectionMask == 0) mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP - | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; } } diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 0cfbd710..2f0a94bf 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -99,7 +99,8 @@ class Display { uint8_t nrprod = 0; uint8_t nrsleep = 0; - int8_t minQAllInv = 4; + uint8_t nrAvailable = 0; + int8_t minQAllInv = 4; Inverter<> *iv; record_t<> *rec; @@ -114,6 +115,8 @@ class Display { nrprod++; else nrsleep++; + if (iv->isAvailable()) + nrAvailable++; rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -141,6 +144,7 @@ class Display { // prepare display data mDisplayData.nrProducing = nrprod; mDisplayData.nrSleeping = nrsleep; + mDisplayData.nrAvailable = nrAvailable; mDisplayData.totalPower = totalPower; mDisplayData.totalYieldDay = totalYieldDay; mDisplayData.totalYieldTotal = totalYieldTotal; @@ -165,7 +169,17 @@ class Display { else mDisplayData.utcTs = 0; - if (mMono ) { + const uint32_t sunriseTime = mApp->getSunrise(); + if (mDisplayData.utcTs == 0) + mDisplayData.sunIsShining = true; // Start with sunshine :-) + else { + mDisplayData.sunIsShining = false; + // new sunrise is calculated after sunset + user-offset + if (utc > sunriseTime) + mDisplayData.sunIsShining = true; + } + + if (mMono) { mMono->disp(); } #if defined(ESP32) && !defined(ETHERNET) diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index 3e998b6d..f8c81914 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- #pragma once +#include "config/config.h" #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds #define DISP_FMT_TEXT_LEN 32 @@ -101,6 +102,13 @@ class DisplayMono { int8_t mod = (millis() / 10000) % ((range >> 1) << 2); mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; } + +#ifdef DISPLAY_CHART +#define DISP_WATT_ARR_LENGTH 128 // Number of WATT history values + float m_wattArr[DISP_WATT_ARR_LENGTH + 1]; // ring buffer for watt history + uint16_t m_wattListIdx; // index for next Element to write into WattArr + void drawPowerChart(); +#endif }; /* adapted 5x8 Font for low-res displays with symbols diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index afb581dd..88c72979 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -9,6 +9,12 @@ class DisplayMono128X64 : public DisplayMono { public: DisplayMono128X64() : DisplayMono() { +#ifdef DISPLAY_CHART + for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) + m_wattArr[i] = 0.0; + m_wattListIdx = 0; + mDrawChart = false; +#endif mExtra = 0; } @@ -41,7 +47,6 @@ class DisplayMono128X64 : public DisplayMono { } void disp(void) { - uint8_t pos, sun_pos, moon_pos; mDisplay->clearBuffer(); @@ -61,109 +66,149 @@ class DisplayMono128X64 : public DisplayMono { // calculate current pixelshift for pixelshift screensaver calcPixelShift(pixelShiftRange); - // print total power - if (mDisplayData->nrProducing > 0) { - if (mDisplayData->totalPower > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - - printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); +#ifdef DISPLAY_CHART + static uint32_t dataUpdateTime = mDisplayData->utcTs + 60; // update chart every minute + if (mDisplayData->utcTs >= dataUpdateTime) + { + dataUpdateTime = mDisplayData->utcTs + 60; // next minute + m_wattArr[m_wattListIdx] = mDisplayData->totalPower; + m_wattListIdx = (m_wattListIdx + 1) % (DISP_WATT_ARR_LENGTH); } - // print Date and time - if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); + if (mDrawChart && mDisplayData->sunIsShining && (mDisplayData->nrAvailable > 0)) + { + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); + printText(mFmtText, l_Time, 10); + } else { + printText("offline", l_Time, 0xff); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_Status, 10); + + drawPowerChart(); - // dynamic status bar, alternatively: - // print ip address - if (!(mExtra % 5) && (mDisplayData->ipAddress)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); - printText(mFmtText, l_Status, 0xff); } - // print status of inverters - else { - sun_pos = -1; - moon_pos = -1; - setLineFont(l_Status); - if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); - else if (0 == mDisplayData->nrSleeping) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - sun_pos = 0; + else +#endif + { + // print total power + if (mDisplayData->nrProducing > 0) { + if (mDisplayData->totalPower > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); + + printText(mFmtText, l_TotalPower, 0xff); + } else { + printText("offline", l_TotalPower, 0xff); + } + + // print Date and time + if (0 != mDisplayData->utcTs) + printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); + + // dynamic status bar, alternatively: + // print ip address + if (!(mExtra % 5) && (mDisplayData->ipAddress)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str()); + printText(mFmtText, l_Status, 0xff); + } + // print status of inverters + else { + uint8_t pos, sun_pos, moon_pos; + sun_pos = -1; + moon_pos = -1; + setLineFont(l_Status); + if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + else if (0 == mDisplayData->nrSleeping) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + sun_pos = 0; } else if (0 == mDisplayData->nrProducing) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); - moon_pos = 0; + snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); + moon_pos = 0; } else { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); - sun_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); - moon_pos = mDisplay->getStrWidth(mFmtText) + 1; - snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " "); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing); + sun_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText + 2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping); + moon_pos = mDisplay->getStrWidth(mFmtText) + 1; + snprintf(mFmtText + 7, DISP_FMT_TEXT_LEN, " "); + } + printText(mFmtText, l_Status, 0xff); + + pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; + mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); + if (sun_pos != -1) + mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol + if (moon_pos != -1) + mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol } - printText(mFmtText, l_Status, 0xff); - pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; - mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); - if (sun_pos!=-1) - mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol - if (moon_pos!=-1) - mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol + // print yields + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol + mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol + + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); + printText(mFmtText, l_YieldDay, 0xff); + + if (mDisplayData->totalYieldTotal > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); + printText(mFmtText, l_YieldTotal, 0xff); + + // draw dynamic RSSI bars + int xoffs; + if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver + xoffs = pixelShiftRange / 2; + else + xoffs = 0; + int rssi_bar_height = 9; + for (int i = 0; i < 4; i++) { + int radio_rssi_threshold = -60 - i * 10; + int wifi_rssi_threshold = -60 - i * 10; + if (mDisplayData->RadioRSSI > radio_rssi_threshold) + mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + if (mDisplayData->WifiRSSI > wifi_rssi_threshold) + mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); + } + // draw dynamic antenna and WiFi symbols + mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); + char sym[] = " "; + sym[0] = mDisplayData->RadioSymbol ? 'A' : 'E'; // NRF + mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + + if (mDisplayData->MQTTSymbol) + sym[0] = 'J'; // MQTT + else + sym[0] = mDisplayData->WifiSymbol ? 'B' : 'F'; // Wifi + mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); + mDisplay->sendBuffer(); } - // print yields - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol - mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol - - if (mDisplayData->totalYieldDay > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay); - printText(mFmtText, l_YieldDay, 0xff); - - if (mDisplayData->totalYieldTotal > 9999.0) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0); - else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal); - printText(mFmtText, l_YieldTotal, 0xff); - - // draw dynamic RSSI bars - int xoffs; - if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver - xoffs = pixelShiftRange/2; - else - xoffs = 0; - int rssi_bar_height = 9; - for (int i = 0; i < 4; i++) { - int radio_rssi_threshold = -60 - i * 10; - int wifi_rssi_threshold = -60 - i * 10; - if (mDisplayData->RadioRSSI > radio_rssi_threshold) - mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - if (mDisplayData->WifiRSSI > wifi_rssi_threshold) - mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height); - } - // draw dynamic antenna and WiFi symbols - mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy); - char sym[]=" "; - sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF - mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); - - if (mDisplayData->MQTTSymbol) - sym[0] = 'J'; // MQTT - else - sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi - mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym); mDisplay->sendBuffer(); - - - mDisplay->sendBuffer(); - + mExtra++; + +#ifdef DISPLAY_CHART + static uint32_t switchDisplayTime = mDisplayData->utcTs + 20; + if (mDisplayData->utcTs >= switchDisplayTime) { + switchDisplayTime = mDisplayData->utcTs + 20; + mDrawChart = !mDrawChart; + } +#endif } private: @@ -226,4 +271,46 @@ class DisplayMono128X64 : public DisplayMono { dispX += mPixelshift; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } + +#ifdef DISPLAY_CHART + bool mDrawChart ; + + void drawPowerChart() { + const int hight = 40; // chart hight + + // Clear area + // mDisplay->draw_rectangle(0, 63 - hight, DISP_WATT_ARR_LENGTH, 63, OLED::SOLID, OLED::BLACK); + mDisplay->setDrawColor(0); + mDisplay->drawBox(0, 63 - hight, DISP_WATT_ARR_LENGTH, hight); + mDisplay->setDrawColor(1); + + // Get max value for scaling + float maxValue = 0.0; + for (int i = 0; i < DISP_WATT_ARR_LENGTH; i++) { + float fValue = m_wattArr[i]; + if (fValue > maxValue) + maxValue = fValue; + } + // calc divider to fit into chart hight + int divider = round(maxValue / (float)hight); + if (divider < 1) + divider = 1; + + // draw chart bars + // Start display of data right behind last written data + uint16_t idx = m_wattListIdx; + for (uint16_t i = 0; i < DISP_WATT_ARR_LENGTH; i++) { + float fValue = m_wattArr[idx]; + int iValue = roundf(fValue); + iValue /= divider; + if (iValue > hight) + iValue = hight; + // mDisplay->draw_line(i, 63 - iValue, i, 63); + // mDisplay->drawVLine(i, 63 - iValue, iValue); + if (iValue>0) + mDisplay->drawLine(i, 63 - iValue, i, 63); + idx = (idx + 1) % (DISP_WATT_ARR_LENGTH); + } + } +#endif }; diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h index a400377d..1ccc5f7d 100644 --- a/src/plugins/Display/Display_data.h +++ b/src/plugins/Display/Display_data.h @@ -11,12 +11,14 @@ struct DisplayData { uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized uint8_t nrProducing=0; // indicate number of producing inverters uint8_t nrSleeping=0; // indicate number of sleeping inverters + uint8_t nrAvailable=0; // number of available (comunicating) inverters bool WifiSymbol = false; // indicate if WiFi is connected bool RadioSymbol = false; // indicate if radio module is connecting and working bool MQTTSymbol = false; // indicate if MQTT is connected int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio IPAddress ipAddress; // indicate ip adress of ahoy + bool sunIsShining; // indicate if time is between sunrise and sunset }; #endif /*__DISPLAY_DATA__*/ diff --git a/src/plugins/history.cpp b/src/plugins/history.cpp new file mode 100644 index 00000000..451d3e1e --- /dev/null +++ b/src/plugins/history.cpp @@ -0,0 +1,94 @@ + +#include "plugins/history.h" + +#include "appInterface.h" +#include "config/config.h" +#include "utils/dbg.h" + +void TotalPowerHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + mRefreshCycle = mConfig->inst.sendInterval; + mMaximumDay = 0; + + // Debug + //for (uint16_t i = 0; i < HISTORY_DATA_ARR_LENGTH *1.5; i++) { + // addValue(i); + //} +} + +void TotalPowerHistory::tickerSecond() { + ++mLoopCnt; + if ((mLoopCnt % mRefreshCycle) == 0) { + //DPRINTLN(DBG_DEBUG,F("TotalPowerHistory::tickerSecond > refreshCycle" + String(mRefreshCycle) + "|" + String(mLoopCnt) + "|" + String(mRefreshCycle % mLoopCnt)); + mLoopCnt = 0; + float totalPower = 0; + float totalPowerDay = 0; + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalPowerDay += iv->getChannelFieldValue(CH0, FLD_MP, rec); + } + if (totalPower > 0) { + uint16_t iTotalPower = roundf(totalPower); + DPRINTLN(DBG_DEBUG, F("[TotalPowerHistory]: addValue(iTotalPower)=") + String(iTotalPower)); + addValue(iTotalPower); + } + if (totalPowerDay > 0) { + mMaximumDay = roundf(totalPowerDay); + } + } +} + +void YieldDayHistory::setup(IApp *app, HmSystemType *sys, settings_t *config) { + mApp = app; + mSys = sys; + mConfig = config; + mRefreshCycle = 60; // every minute + mDayStored = false; +}; + +void YieldDayHistory::tickerSecond() { + ++mLoopCnt; + if ((mLoopCnt % mRefreshCycle) == 0) { + mLoopCnt = 0; + // check for sunset. if so store yield of day once + uint32_t sunsetTime = mApp->getSunset(); + uint32_t sunriseTime = mApp->getSunrise(); + uint32_t currentTime = mApp->getTimestamp(); + DPRINTLN(DBG_DEBUG,F("[YieldDayHistory] current | rise | set -> ") + String(currentTime) + " | " + String(sunriseTime) + " | " + String(sunsetTime)); + + if (currentTime > sunsetTime) { + if (!mDayStored) { + DPRINTLN(DBG_DEBUG,F("currentTime > sunsetTime ") + String(currentTime) + " > " + String(sunsetTime)); + float totalYieldDay = -0.1; + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + } + if (totalYieldDay > 0) { + uint16_t iTotalYieldDay = roundf(totalYieldDay); + DPRINTLN(DBG_DEBUG,F("addValue(iTotalYieldDay)=") + String(iTotalYieldDay)); + addValue(iTotalYieldDay); + mDayStored = true; + } + } + } else { + if (currentTime > sunriseTime) { + DPRINTLN(DBG_DEBUG,F("currentTime > sunriseTime ") + String(currentTime) + " > " + String(sunriseTime)); + mDayStored = false; + } + } + } +} \ No newline at end of file diff --git a/src/plugins/history.h b/src/plugins/history.h new file mode 100644 index 00000000..f6485b17 --- /dev/null +++ b/src/plugins/history.h @@ -0,0 +1,86 @@ +#ifndef __HISTORY_DATA_H__ +#define __HISTORY_DATA_H__ + +#include "utils/helper.h" +#include "defines.h" +#include "hm/hmSystem.h" + +typedef HmSystem HmSystemType; +class IApp; + +#define HISTORY_DATA_ARR_LENGTH 256 + +class HistoryData { + public: + HistoryData() { + for (int i = 0; i < HISTORY_DATA_ARR_LENGTH; i++) + m_dataArr[i] = 0; + m_listIdx = 0; + m_dispIdx = 0; + m_wrapped = false; + }; + void addValue(uint16_t value) + { + if (m_wrapped) // after 1st time array wrap we have to increas the display index + m_dispIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + m_dataArr[m_listIdx] = value; + m_listIdx = (m_listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); + if (m_listIdx == 0) + m_wrapped = true; + }; + + uint16_t valueAt(int i){ + uint16_t idx = m_dispIdx + i; + idx = idx % HISTORY_DATA_ARR_LENGTH; + uint16_t value = m_dataArr[idx]; + return value; + }; + + private: + uint16_t m_dataArr[HISTORY_DATA_ARR_LENGTH + 1]; // ring buffer for watt history + uint16_t m_listIdx; // index for next Element to write into WattArr + uint16_t m_dispIdx; // index for 1st Element to display from WattArr + bool m_wrapped; +}; + +class TotalPowerHistory : public HistoryData { + public: + TotalPowerHistory() : HistoryData() { + mLoopCnt = 0; + }; + + void setup(IApp *app, HmSystemType *sys, settings_t *config); + void tickerSecond(); + uint16_t getMaximumDay() { return mMaximumDay; } + + private: + IApp *mApp; + HmSystemType *mSys; + settings *mSettings; + settings_t *mConfig; + uint16_t mRefreshCycle; + uint16_t mLoopCnt; + + uint16_t mMaximumDay; +}; + +class YieldDayHistory : public HistoryData { + public: + YieldDayHistory() : HistoryData(){ + mLoopCnt = 0; + }; + + void setup(IApp *app, HmSystemType *sys, settings_t *config); + void tickerSecond(); + + private: + IApp *mApp; + HmSystemType *mSys; + settings *mSettings; + settings_t *mConfig; + uint16_t mRefreshCycle; + uint16_t mLoopCnt; + bool mDayStored; +}; + +#endif \ No newline at end of file diff --git a/src/web/RestApi.h b/src/web/RestApi.h index a74f4f14..45c2670d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -22,6 +22,8 @@ #include "ESPAsyncWebServer.h" #endif +#include "plugins/history.h" + #if defined(F) && defined(ESP32) #undef F #define F(sl) (sl) @@ -50,8 +52,10 @@ class RestApi { mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; - mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( - std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + mSrv->on("/api/insertYieldDayHistory", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), + std::bind(&RestApi::onApiPostYieldDHistory, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)) + .onBody(std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); @@ -102,6 +106,8 @@ class RestApi { else if(path == "setup/networks") getNetworks(root); #endif /* !defined(ETHERNET) */ else if(path == "live") getLive(request,root); + else if (path == "powerHistory") getPowerHistory(request, root); + else if (path == "yieldDayHistory") getYieldDayHistory(request, root); else { if(path.substring(0, 12) == "inverter/id/") getInverter(root, request->url().substring(17).toInt()); @@ -136,6 +142,83 @@ class RestApi { #endif } + void onApiPostYieldDHistory(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) + { + uint32_t total = request->contentLength(); + DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final); + + if (0 == index) { + if (NULL != mTmpBuf) + delete[] mTmpBuf; + mTmpBuf = new uint8_t[total + 1]; + mTmpSize = total; + } + if (mTmpSize >= (len + index)) + memcpy(&mTmpBuf[index], data, len); + + if (!final) + return; // not last frame - nothing to do + + mTmpSize = len + index ; // correct the total size + mTmpBuf[mTmpSize] = 0; + +#ifndef ESP32 + DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap +#else + DynamicJsonDocument json(12000)); // does this work? I have no ESP32 :-( +#endif + DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize); + json.shrinkToFit(); + JsonObject obj = json.as(); + + + // Debugging + // mTmpBuf[mTmpSize] = 0; + // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf); + + if (!err && obj) + { + // insert data into yieldDayHistory object + HistoryData *p; + if (obj["maximumDay"]>0) // this is power history data + p = mApp->getTotalPowerHistoryPtr(); + else + p = mApp->getYieldDayHistoryPtr(); + + size_t cnt = obj[F("value")].size(); + DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt)); + + for (uint16_t i = 0; i < cnt; i++) { + uint16_t val = obj[F("value")][i]; + p->addValue(val); + // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); + } + } + else + { + switch (err.code()) { + case DeserializationError::Ok: + break; + case DeserializationError::IncompleteInput: + DPRINTLN(DBG_DEBUG, F("Incomplete input")); + break; + case DeserializationError::InvalidInput: + DPRINTLN(DBG_DEBUG, F("Invalid input")); + break; + case DeserializationError::NoMemory: + DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes"); + break; + default: + DPRINTLN(DBG_DEBUG, F("Deserialization failed")); + break; + } + } + + request->send(204); // Success with no page load + delete[] mTmpBuf; + mTmpBuf = NULL; + } + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); @@ -196,6 +279,8 @@ class RestApi { ep[F("setup")] = url + F("setup"); ep[F("system")] = url + F("system"); ep[F("live")] = url + F("live"); + ep[F("powerHistory")] = url + F("powerHistory"); + ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); } @@ -769,6 +854,38 @@ class RestApi { } } + void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; + uint16_t maximum = 0; + TotalPowerHistory *p = mApp->getTotalPowerHistoryPtr(); + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = p->valueAt(fld); + obj[F("value")][fld] = value; + if (value > maximum) + maximum = value; + } + obj[F("maximum")] = maximum; + obj[F("maximumDay")] = p->getMaximumDay(); + } + + void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 86400; // 1 day + obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH; + uint16_t maximum = 0; + YieldDayHistory *p = mApp->getYieldDayHistoryPtr(); + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = p->valueAt(fld); + obj[F("value")][fld] = value; + if (value > maximum) + maximum = value; + } + obj[F("maximum")] = maximum; + } + + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; diff --git a/src/web/html/api.js b/src/web/html/api.js index b059cc69..3b8b266d 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -84,7 +84,7 @@ function topnav() { } function parseNav(obj) { - for(i = 0; i < 11; i++) { + for(i = 0; i < 12; i++) { if(i == 2) continue; var l = document.getElementById("nav"+i); diff --git a/src/web/html/history.html b/src/web/html/history.html new file mode 100644 index 00000000..5b6c1fb5 --- /dev/null +++ b/src/web/html/history.html @@ -0,0 +1,135 @@ + + + + + History + {#HTML_HEADER} + + + + + + + {#HTML_NAV} +
+
+

Total Power history

+
+
+

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

+
+

Yield per day history

+
+
+

+ Maximum value: Wh
+ Updated every seconds

+
+ +

Insert data into Yield per day history

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

+
+
+ {#HTML_FOOTER} + + + + + + \ No newline at end of file diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 91de5047..2f13c2e8 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -7,6 +7,7 @@
Live + History Webserial Settings diff --git a/src/web/html/style.css b/src/web/html/style.css index b31ae7c9..14a1b002 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -33,6 +33,26 @@ textarea { color: var(--fg2); } +svg rect {fill: #0000AA;} +svg.chart { + background: #f2f2f2; + border: 2px solid gray; + padding: 1px; +} + +div.chartDivContainer { + padding: 1px; + margin: 1px; +} +div.chartdivContainer span { + color: var(--fg2); +} +div.chartDiv { + padding: 0px; + margin: 0px; +} + + .topnav { background-color: var(--nav-bg); position: fixed; diff --git a/src/web/web.h b/src/web/web.h index 1e87547b..38c927f7 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -36,6 +36,7 @@ #include "html/h/update_html.h" #include "html/h/visualization_html.h" #include "html/h/about_html.h" +#include "html/h/history_html.h" #define WEB_SERIAL_BUF_SIZE 2048 @@ -80,6 +81,7 @@ class Web { mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); + mWeb.on("/history", HTTP_ANY, std::bind(&Web::onHistory, this, std::placeholders::_1)); #ifdef ENABLE_PROMETHEUS_EP mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); @@ -249,6 +251,8 @@ class Web { request->redirect(F("/index")); else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE) request->redirect(F("/live")); + else if ((mConfig->sys.protectionMask & PROT_MASK_HISTORY) != PROT_MASK_HISTORY) + request->redirect(F("/history")); else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL) request->redirect(F("/serial")); else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM) @@ -264,7 +268,7 @@ class Web { } } - void getPage(AsyncWebServerRequest *request, uint8_t mask, const uint8_t *zippedHtml, uint32_t len) { + void getPage(AsyncWebServerRequest *request, uint16_t mask, const uint8_t *zippedHtml, uint32_t len) { if (CHECK_MASK(mConfig->sys.protectionMask, mask)) checkProtection(request); @@ -594,6 +598,10 @@ class Web { getPage(request, PROT_MASK_LIVE, visualization_html, visualization_html_len); } + void onHistory(AsyncWebServerRequest *request) { + getPage(request, PROT_MASK_HISTORY, history_html, history_html_len); + } + void onAbout(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len); response->addHeader(F("Content-Encoding"), "gzip");