mirror of https://github.com/lumapu/ahoy.git
				
				
			
				 12 changed files with 353 additions and 12 deletions
			
			
		| @ -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 <array> | |||
| #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 HMSYSTEM> | |||
| 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<uint16_t, HISTORY_DATA_ARR_LENGTH + 1> 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 | |||
| @ -0,0 +1,135 @@ | |||
| <!doctype html> | |||
| <html> | |||
| 
 | |||
| <head> | |||
|     <title>History</title> | |||
|     {#HTML_HEADER} | |||
|     <meta name="apple-mobile-web-app-capable" content="yes"> | |||
|     <meta name="format-detection" content="telephone=no"> | |||
| 
 | |||
| </head> | |||
| 
 | |||
| <body> | |||
|     {#HTML_NAV} | |||
|     <div id="wrapper"> | |||
|     <div id="content"> | |||
|         <h3>Total Power history</h3> | |||
|         <div class="chartDivContainer"> | |||
|             <div class="chartDiv" id="phHistoryChart">  </div> | |||
|             <p class="center" style="margin:0px;border:0px;"> | |||
|                 Maximum day: <span id="phMaximumDay"></span> W. Last value: <span id="phActual"></span> W.<br /> | |||
|                 Maximum graphics: <span id="phMaximum"></span> W. Updated every <span id="phRefresh"></span> seconds </p> | |||
|         </div> | |||
|         <h3>Yield per day history</h3> | |||
|         <div class="chartDivContainer"> | |||
|             <div class="chartDiv" id="ydHistoryChart"> </div> | |||
|             <p class="center" style="margin:0px;border:0px;"> | |||
|                 Maximum value: <span id="ydMaximum"></span> Wh<br /> | |||
|                 Updated every <span id="ydRefresh"></span> seconds </p> | |||
|         </div> | |||
| 
 | |||
|         <h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4> | |||
|         <fieldset style="padding: 1px;"> | |||
|             <legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call </legend> | |||
|             <form id="form" method="POST" action="/api/insertYieldDayHistory" enctype="multipart/form-data" accept-charset="utf-8"> | |||
|                 <input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()"> | |||
|                 <input type="file" name="insert" style="width: 80%;"> | |||
|             </form> | |||
|         </fieldset> | |||
|         <p></p> | |||
|     </div> | |||
|     </div> | |||
|     {#HTML_FOOTER} | |||
| 
 | |||
|     <script type="text/javascript"> | |||
|         const svgns = "http://www.w3.org/2000/svg"; | |||
|         var phExeOnce = true; | |||
|         var ydExeOnce = true; | |||
|         // make a simple rectangle | |||
|         var mRefresh = 60; | |||
|         var phDatapoints = 512; | |||
|         var mMaximum = 0; | |||
|         var mLastValue = 0; | |||
|         var mDataValues = []; | |||
|         const mChartHight = 250; | |||
| 
 | |||
|         function parseHistory(obj, namePrefix, execOnce) { | |||
|             mRefresh = obj["refresh"]; | |||
|             phDatapoints = obj["datapoints"]; | |||
|             mDataValues = Object.assign({}, obj["value"]); | |||
|             mMaximum = obj["maximum"]; | |||
|             // generate svg | |||
|             if (true == execOnce) { | |||
|                 let svg = document.createElementNS(svgns, "svg"); | |||
|                 svg.setAttribute("class", "chart"); | |||
|                 svg.setAttribute("width", String((phDatapoints+2) * 2)); | |||
|                 svg.setAttribute("height", String(mChartHight) + ""); | |||
|                 svg.setAttribute("aria-labelledby", "title desc"); | |||
|                 svg.setAttribute("role", "img"); | |||
|                 t = ml("title"); | |||
|                 t.innerHTML = "History of day"; | |||
|                 svg.appendChild(t); | |||
|                 let g = document.createElementNS(svgns, "g"); | |||
|                 svg.appendChild(g); | |||
|                 for (var i = 0; i < phDatapoints; i++) { | |||
|                     val = mDataValues[i]; | |||
|                     let rect = document.createElementNS(svgns, "rect"); | |||
|                     rect.setAttribute("id", namePrefix+"Rect" + i); | |||
|                     rect.setAttribute("x", String(i * 2) + ""); | |||
|                     rect.setAttribute("width", String(2) + ""); | |||
|                     g.appendChild(rect); | |||
|                 } | |||
|                 document.getElementById(namePrefix+"HistoryChart").appendChild(svg); | |||
|             } | |||
|             // normalize data to chart | |||
|             let divider = mMaximum / mChartHight; | |||
|             if (divider == 0) | |||
|                 divider = 1; | |||
|             for (var i = 0; i < phDatapoints; i++) { | |||
|                 val = mDataValues[i]; | |||
|                 if (val>0) | |||
|                     mLastValue = val | |||
|                 val = val / divider | |||
|                 rect = document.getElementById(namePrefix+"Rect" + i); | |||
|                 rect.setAttribute("height", val); | |||
|                 rect.setAttribute("y", mChartHight - val); | |||
|             } | |||
|             document.getElementById(namePrefix + "Maximum").innerHTML = mMaximum; | |||
|             if (mRefresh < 5) | |||
|                 mRefresh = 5; | |||
|             document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh; | |||
|         } | |||
|         function parsePowerHistory(obj){ | |||
|             if (null != obj) { | |||
|                 parseNav(obj["generic"]); | |||
|                 parseHistory(obj,"ph", phExeOnce) | |||
|                 let maximumDay = obj["maximumDay"]; | |||
|                 document.getElementById("phActual").innerHTML = mLastValue; | |||
|                 document.getElementById("phMaximumDay").innerHTML = maximumDay; | |||
|             } | |||
|             if (true == phExeOnce) { | |||
|                 phExeOnce = false; | |||
|                 window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); | |||
|                 // one after the other | |||
|                 setTimeout(() => { | |||
|                     getAjax("/api/yieldDayHistory", parseYieldDayHistory); | |||
|                 } , 20); | |||
|             } | |||
|         } | |||
|         function parseYieldDayHistory(obj) { | |||
|             if (null != obj) { | |||
|                 parseNav(obj["generic"]); | |||
|                 parseHistory(obj, "yd", ydExeOnce) | |||
|             } | |||
|             if (true == ydExeOnce) { | |||
|                 ydExeOnce = false; | |||
|                 window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", mRefresh * 500); | |||
|             } | |||
|         } | |||
| 
 | |||
|         getAjax("/api/powerHistory", parsePowerHistory); | |||
|     </script> | |||
| 
 | |||
| </body> | |||
| 
 | |||
| </html> | |||
					Loading…
					
					
				
		Reference in new issue