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