mirror of https://github.com/lumapu/ahoy.git
				
				
			
				 17 changed files with 691 additions and 97 deletions
			
			
		| @ -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; | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
| @ -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<MAX_NUM_INVERTERS> 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 | ||||
| @ -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