mirror of https://github.com/lumapu/ahoy.git
VArt67
1 year ago
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