diff --git a/src/app.cpp b/src/app.cpp index 6a93f96c..cae48149 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -92,6 +92,8 @@ void app::setup() { #endif #endif + mHistory.setup(this, &mSys, mConfig, &mTimestamp); + mPubSerial.setup(mConfig, &mSys, &mTimestamp); #if !defined(ETHERNET) @@ -149,6 +151,8 @@ void app::regularTickers(void) { #if !defined(ETHERNET) //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif + + everySec(std::bind(&HistoryType::tickerSecond, mHistory), "hist"); } #if defined(ETHERNET) diff --git a/src/app.h b/src/app.h index 41ff008b..a7a88609 100644 --- a/src/app.h +++ b/src/app.h @@ -24,6 +24,7 @@ #include "utils/scheduler.h" #include "utils/syslog.h" #include "web/RestApi.h" +#include "plugins/history.h" #include "web/web.h" #include "hm/Communication.h" #if defined(ETHERNET) @@ -35,6 +36,7 @@ #include // position is relevant since version 1.4.7 of this library + // convert degrees and radians for sun calculation #define SIN(x) (sin(radians(x))) #define COS(x) (cos(radians(x))) @@ -42,12 +44,11 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -#ifdef ESP32 -#endif typedef Web WebType; typedef RestApi RestApiType; typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; +typedef HistoryData HistoryType; // PLUGINS #if defined(PLUGIN_DISPLAY) @@ -251,6 +252,14 @@ class app : public IApp, public ah::Scheduler { Scheduler::setTimestamp(newTime); } + uint16_t getHistoryValue(uint8_t type, uint16_t i) { + return mHistory.valueAt((HistoryStorageType)type, i); + } + + uint16_t getHistoryMaxDay() { + return mHistory.getMaximumDay(); + } + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true @@ -358,6 +367,7 @@ class app : public IApp, public ah::Scheduler { DisplayType mDisplay; DisplayData mDispData; #endif + HistoryType mHistory; }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 76c5e935..b812f5fc 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -14,11 +14,6 @@ #include "ESPAsyncWebServer.h" #endif -//#include "hms/hmsRadio.h" -#if defined(ESP32) -//typedef CmtRadio> CmtRadioType; -#endif - // abstract interface to App. Make members of App accessible from child class // like web or API without forward declaration class IApp { @@ -65,8 +60,10 @@ class IApp { virtual bool getProtection(AsyncWebServerRequest *request) = 0; - virtual void* getRadioObj(bool nrf) = 0; + virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; + virtual uint16_t getHistoryMaxDay() = 0; + virtual void* getRadioObj(bool nrf) = 0; }; #endif /*__IAPP_H__*/ diff --git a/src/config/settings.h b/src/config/settings.h index 28739db3..24ebb976 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,6 +51,7 @@ #define DEF_PROT_SYSTEM 0x0020 #define DEF_PROT_API 0x0000 #define DEF_PROT_MQTT 0x0000 +#define DEF_PROT_HISTORY 0x0000 typedef struct { @@ -375,7 +377,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 @@ -555,7 +557,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/history.h b/src/plugins/history.h new file mode 100644 index 00000000..9ed7860d --- /dev/null +++ b/src/plugins/history.h @@ -0,0 +1,123 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __HISTORY_DATA_H__ +#define __HISTORY_DATA_H__ + +#include +#include "../appInterface.h" +#include "../hm/hmSystem.h" +#include "../utils/helper.h" + +#define HISTORY_DATA_ARR_LENGTH 256 + +enum class HistoryStorageType : uint8_t { + POWER, + YIELD +}; + +template +class HistoryData { + private: + struct storage_t { + uint16_t refreshCycle; + uint16_t loopCnt; + uint16_t listIdx; // index for next Element to write into WattArr + uint16_t dispIdx; // index for 1st Element to display from WattArr + bool wrapped; + // ring buffer for watt history + std::array data; + + void reset() { + loopCnt = 0; + listIdx = 0; + dispIdx = 0; + wrapped = false; + for(uint16_t i = 0; i < (HISTORY_DATA_ARR_LENGTH + 1); i++) { + data[i] = 0; + } + } + }; + + public: + void setup(IApp *app, HMSYSTEM *sys, settings_t *config, uint32_t *ts) { + mApp = app; + mSys = sys; + mConfig = config; + mTs = ts; + + mCurPwr.reset(); + mCurPwr.refreshCycle = mConfig->inst.sendInterval; + mYieldDay.reset(); + mYieldDay.refreshCycle = 60; + } + + void tickerSecond() { + Inverter<> *iv; + record_t<> *rec; + float curPwr = 0; + float maxPwr = 0; + float yldDay = -0.1; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); + yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + } + + if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { + mCurPwr.loopCnt = 0; + if (curPwr > 0) + addValue(&mCurPwr, roundf(curPwr)); + if (maxPwr > 0) + mMaximumDay = roundf(maxPwr); + } + + if (*mTs > mApp->getSunset()) { + if ((!mDayStored) && (yldDay > 0)) { + addValue(&mYieldDay, roundf(yldDay)); + mDayStored = true; + } + } else if (*mTs > mApp->getSunrise()) + mDayStored = false; + } + + uint16_t valueAt(HistoryStorageType type, uint16_t i) { + storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; + uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; + return s->data[idx]; + } + + uint16_t getMaximumDay() { + return mMaximumDay; + } + + 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; + } + + private: + IApp *mApp; + HMSYSTEM *mSys; + settings *mSettings; + settings_t *mConfig; + uint32_t *mTs; + + storage_t mCurPwr; + storage_t mYieldDay; + bool mDayStored = false; + uint16_t mMaximumDay = 0; +}; + +#endif diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 3d4a6c0d..26ca571f 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) @@ -103,6 +105,8 @@ class RestApi { else if(path == "setup/getip") getWifiIp(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()); @@ -197,6 +201,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"); } @@ -785,6 +791,36 @@ 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 max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; + obj[F("maxDay")] = mApp->getHistoryMaxDay(); + } + + 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 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; + } + + 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 c05099b7..4cd62d4a 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -7,6 +7,7 @@
{#NAV_LIVE} + {#NAV_HISTORY} {#NAV_WEBSERIAL} {#NAV_SETTINGS} diff --git a/src/web/html/style.css b/src/web/html/style.css index c9f9fbf7..3ad91a8b 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/lang.json b/src/web/lang.json index c7243728..9f1b9e5e 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -8,6 +8,11 @@ "en": "Live", "de": "Daten" }, + { + "token": "{#NAV_HISTORY}", + "en": "History", + "de": "Verlauf" + }, { "token": "NAV_WEBSERIAL", "en": "Webserial", diff --git a/src/web/web.h b/src/web/web.h index 38ecf171..e22d7192 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -37,6 +37,7 @@ #include "html/h/visualization_html.h" #include "html/h/about_html.h" #include "html/h/wizard_html.h" +#include "html/h/history_html.h" #define WEB_SERIAL_BUF_SIZE 2048 @@ -82,6 +83,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)); @@ -251,6 +253,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) @@ -266,7 +270,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); @@ -608,6 +612,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");