diff --git a/src/app.h b/src/app.h index b16d7aeb..a6960d89 100644 --- a/src/app.h +++ b/src/app.h @@ -314,6 +314,14 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryPeriode(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getPeriode((HistoryStorageType)type); + #else + return 0; + #endif + } + uint16_t getHistoryMaxDay() override { #if defined(ENABLE_HISTORY) return mHistory.getMaximumDay(); @@ -322,6 +330,21 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryLastValueTs(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getLastValueTs((HistoryStorageType)type); + #else + return 0; + #endif + } + #if defined(ENABLE_HISTORY_LOAD_DATA) + void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override { + #if defined(ENABLE_HISTORY) + return mHistory.addValue((HistoryStorageType)historyType, valueType, value); + #endif + } + #endif + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true diff --git a/src/appInterface.h b/src/appInterface.h index 536455e0..8a18cf8d 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -67,8 +67,12 @@ class IApp { virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; + virtual uint32_t getHistoryPeriode(uint8_t type) = 0; virtual uint16_t getHistoryMaxDay() = 0; - + virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0; + #if defined(ENABLE_HISTORY_LOAD_DATA) + virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0; + #endif virtual void* getRadioObj(bool nrf) = 0; }; diff --git a/src/plugins/history.h b/src/plugins/history.h index 5076e295..2b5f0d32 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -17,6 +17,7 @@ enum class HistoryStorageType : uint8_t { POWER, + POWER_DAY, YIELD }; @@ -25,14 +26,16 @@ class HistoryData { private: struct storage_t { uint16_t refreshCycle = 0; - uint16_t loopCnt = 0; - uint16_t listIdx = 0; // index for next Element to write into WattArr - uint16_t dispIdx = 0; // index for 1st Element to display from WattArr - bool wrapped = false; + uint16_t loopCnt; + uint16_t listIdx; // index for next Element to write into WattArr // ring buffer for watt history std::array data; - storage_t() { data.fill(0); } + void reset() { + loopCnt = 0; + listIdx = 0; + data.fill(0); + } }; public: @@ -42,63 +45,225 @@ class HistoryData { mConfig = config; mTs = ts; + mCurPwr.reset(); mCurPwr.refreshCycle = mConfig->inst.sendInterval; - //mYieldDay.refreshCycle = 60; + mCurPwrDay.reset(); + mCurPwrDay.refreshCycle = mConfig->inst.sendInterval; + mYieldDay.reset(); + mYieldDay.refreshCycle = 60; + mLastValueTs = 0; + mPgPeriod=0; + mMaximumDay = 0; } void tickerSecond() { - ; float curPwr = 0; - float maxPwr = 0; + //float maxPwr = 0; float yldDay = -0.1; + uint32_t ts = 0; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { Inverter<> *iv = mSys->getInverterByPos(i); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); if (iv == NULL) continue; curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); + //maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + if (rec->ts > ts) + ts = rec->ts; } if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { mCurPwr.loopCnt = 0; - if (curPwr > 0) + if (curPwr > 0) { + mLastValueTs = ts; addValue(&mCurPwr, roundf(curPwr)); - if (maxPwr > 0) - mMaximumDay = roundf(maxPwr); + if (curPwr > mMaximumDay) + mMaximumDay = roundf(curPwr); + } + //if (maxPwr > 0) + // mMaximumDay = roundf(maxPwr); + } + + if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) { + mCurPwrDay.loopCnt = 0; + if (curPwr > 0) { + mLastValueTs = ts; + addValueDay(&mCurPwrDay, roundf(curPwr)); + } } - /*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { - if (*mTs > mApp->getSunset()) { + if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + mYieldDay.loopCnt = 0; + if (*mTs > mApp->getSunset()) + { if ((!mDayStored) && (yldDay > 0)) { addValue(&mYieldDay, roundf(yldDay)); mDayStored = true; } - } else if (*mTs > mApp->getSunrise()) + } + else if (*mTs > mApp->getSunrise()) mDayStored = false; - }*/ + } } uint16_t valueAt(HistoryStorageType type, uint16_t i) { - //storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; - storage_t *s = &mCurPwr; - uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; + storage_t *s=NULL; + uint16_t idx=i; + DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i)); + + switch (type) { + case HistoryStorageType::POWER: + s = &mCurPwr; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + idx = i; + break; + case HistoryStorageType::YIELD: + s = &mYieldDay; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + } + if (s) return s->data[idx]; + return 0; } uint16_t getMaximumDay() { return mMaximumDay; } + uint32_t getLastValueTs(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type)); + if (type == HistoryStorageType::POWER_DAY) + return mPgEndTime; + return mLastValueTs; + } + + uint32_t getPeriode(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type)); + switch (type) { + case HistoryStorageType::POWER: + return mCurPwr.refreshCycle; + break; + case HistoryStorageType::POWER_DAY: + return mPgPeriod / HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::YIELD: + return (60 * 60 * 24); // 1 day + break; + } + return 0; + } + + #if defined(ENABLE_HISTORY_LOAD_DATA) + /* For filling data from outside */ + void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) { + if (valueType<2) { + storage_t *s=NULL; + switch (historyType) { + case HistoryStorageType::POWER: + s = &mCurPwr; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + break; + case HistoryStorageType::YIELD: + s = &mYieldDay; + break; + } + if (s) + { + if (valueType==0) + addValue(s, value); + if (valueType==1) + { + if (historyType == HistoryStorageType::POWER) + s->refreshCycle = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgPeriod = value * HISTORY_DATA_ARR_LENGTH; + } + } + return; + } + if (valueType == 2) + { + if (historyType == HistoryStorageType::POWER) + mLastValueTs = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgEndTime = value; + } + } + #endif + private: void addValue(storage_t *s, uint16_t value) { - if (s->wrapped) // after 1st time array wrap we have to increase the display index - s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); s->data[s->listIdx] = value; s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); - if (s->listIdx == 0) - s->wrapped = true; + } + + void addValueDay(storage_t *s, uint16_t value) { + DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value)); + bool storeStartEndTimes = false; + bool store_entry = false; + uint32_t pGraphStartTime = mApp->getSunrise(); + uint32_t pGraphEndTime = mApp->getSunset(); + uint32_t utcTs = mApp->getTimestamp(); + switch (mPgState) { + case PowerGraphState::NO_TIME_SYNC: + if ((pGraphStartTime > 0) + && (pGraphEndTime > 0) // wait until period data is available ... + && (utcTs >= pGraphStartTime) + && (utcTs < pGraphEndTime)) // and current time is in period + { + storeStartEndTimes = true; // period was received -> store + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + } + break; + case PowerGraphState::IN_PERIOD: + if (utcTs > mPgEndTime) // check if end of day is reached ... + mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting + else + store_entry = true; + break; + case PowerGraphState::WAIT_4_NEW_PERIOD: + if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ... + storeStartEndTimes = true; // and store it for next period + mPgState = PowerGraphState::WAIT_4_RESTART; + } + break; + case PowerGraphState::WAIT_4_RESTART: + if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ... + mCurPwrDay.reset(); // then reset power graph data + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + mCurPwr.reset(); // also reset "last values" graph + mMaximumDay = 0; // and the maximum of the (last) day + } + break; + } + + // store start and end times of current time period and calculate period length + if (storeStartEndTimes) { + mPgStartTime = pGraphStartTime; + mPgEndTime = pGraphEndTime; + mPgPeriod = pGraphEndTime - pGraphStartTime; // time period of power graph in sec for scaling of x-axis + } + + if (store_entry) { + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value)); + if (mPgPeriod) { + uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod; + s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1)); + } else + s->listIdx = 0; + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx)); + s->data[s->listIdx] = std::max(s->data[s->listIdx], value); // update current datapoint to maximum of all seen values + } } private: @@ -109,8 +274,21 @@ class HistoryData { uint32_t *mTs = nullptr; storage_t mCurPwr; + storage_t mCurPwrDay; + storage_t mYieldDay; bool mDayStored = false; uint16_t mMaximumDay = 0; + uint32_t mLastValueTs = 0; + enum class PowerGraphState { + NO_TIME_SYNC, + IN_PERIOD, + WAIT_4_NEW_PERIOD, + WAIT_4_RESTART + }; + PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC; + uint32_t mPgStartTime = 0; + uint32_t mPgEndTime = 0; + uint32_t mPgPeriod = 0; // seconds }; #endif /*ENABLE_HISTORY*/ diff --git a/src/web/RestApi.h b/src/web/RestApi.h index f7a87e4d..182e3a5e 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -49,6 +49,12 @@ class RestApi { mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; + #if defined(ENABLE_HISTORY_LOAD_DATA) + //Vart67: Debugging history graph (loading data into graph storage + mSrv->on("/api/addYDHist", + HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), + std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + #endif 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)); @@ -103,6 +109,8 @@ class RestApi { #endif /* !defined(ETHERNET) */ else if(path == "live") getLive(request,root); else if (path == "powerHistory") getPowerHistory(request, root); + else if (path == "powerHistoryDay") getPowerHistoryDay(request, root); + else if (path == "yieldDayHistory") getYieldDayHistory(request, root); else { if(path.substring(0, 12) == "inverter/id/") getInverter(root, request->url().substring(17).toInt()); @@ -137,7 +145,94 @@ class RestApi { #endif } - void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) { + #if defined(ENABLE_HISTORY_LOAD_DATA) + // VArt67: For debugging history graph. Loading data into graph + void onApiPostYDHist(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 + HistoryStorageType dataType; + if (obj["maxDay"] > 0) // this is power history data + { + dataType = HistoryStorageType::POWER; + if (obj["refresh"] > 60) + dataType = HistoryStorageType::POWER_DAY; + + } + else + dataType = HistoryStorageType::YIELD; + + 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]; + mApp->addValueToHistory((uint8_t)dataType, 0, val); + // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); + } + uint32_t refresh = obj[F("refresh")]; + mApp->addValueToHistory((uint8_t)dataType, 1, refresh); + if (dataType != HistoryStorageType::YIELD) { + uint32_t ts = obj[F("lastValueTs")]; + mApp->addValueToHistory((uint8_t)dataType, 2, ts); + } + + } 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; + } + #endif + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); if(0 == index) { @@ -207,6 +302,8 @@ class RestApi { ep[F("live")] = url + F("live"); #if defined(ENABLE_HISTORY) ep[F("powerHistory")] = url + F("powerHistory"); + ep[F("powerHistoryDay")] = url + F("powerHistoryDay"); + ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); #endif } @@ -301,6 +398,7 @@ class RestApi { obj[F("heap_free")] = mHeapFree; obj[F("sketch_total")] = ESP.getFreeSketchSpace(); obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb + obj[F("wifi_channel")] = WiFi.channel(); getGeneric(request, obj); getRadioNrf(obj.createNestedObject(F("radioNrf"))); @@ -815,7 +913,7 @@ class RestApi { void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); #if defined(ENABLE_HISTORY) - obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::POWER); uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); @@ -825,8 +923,40 @@ class RestApi { } obj[F("max")] = max; obj[F("maxDay")] = mApp->getHistoryMaxDay(); - #else - obj[F("refresh")] = 86400; // 1 day; + obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER); + #endif /*ENABLE_HISTORY*/ + } + + void getPowerHistoryDay(AsyncWebServerRequest *request, JsonObject obj){ + getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) + obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::POWER_DAY); + uint16_t max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER_DAY, fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; + obj[F("maxDay")] = mApp->getHistoryMaxDay(); + obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER_DAY); + #endif /*ENABLE_HISTORY*/ + } + + + void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { + getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) + obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::YIELD); + uint16_t max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; #endif /*ENABLE_HISTORY*/ } diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css index 2e676029..aedd05d4 100644 --- a/src/web/html/colorBright.css +++ b/src/web/html/colorBright.css @@ -30,4 +30,8 @@ --ch-head-bg: #006ec0; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #fbfbfb; + --chart-bg: #f9f9f9; + --chart-text: #000000; } diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css index 40bd4cf3..b5b1a72b 100644 --- a/src/web/html/colorDark.css +++ b/src/web/html/colorDark.css @@ -30,4 +30,8 @@ --ch-head-bg: #236; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #0b0b0b; + --chart-bg: #090909; + --chart-text: #FFFFFF; } diff --git a/src/web/html/history.html b/src/web/html/history.html index 6372ab82..ab0bc4a6 100644 --- a/src/web/html/history.html +++ b/src/web/html/history.html @@ -13,81 +13,361 @@

{#TOTAL_POWER}

-
+ {#LAST} {#VALUES} +

- {#MAX_DAY}: W. {#LAST_VALUE}: W.
- {#MAXIMUM}: W. {#UPDATED} {#SECONDS} + {#LAST_VALUE}: W.
+ {#MAXIMUM}: W. + {#UPDATED} {#SECONDS}

+

{#TOTAL_POWER_DAY}

+
+
+

+ {#MAX_DAY}: W.
+ {#UPDATED} {#SECONDS} +

+
+ +
{#HTML_FOOTER}