Browse Source

1st merge of reworked history.html implementation

pull/1491/head
VArt67 11 months ago
parent
commit
f2f05a4de9
  1. 23
      src/app.h
  2. 6
      src/appInterface.h
  3. 214
      src/plugins/history.h
  4. 137
      src/web/RestApi.h
  5. 4
      src/web/html/colorBright.css
  6. 4
      src/web/html/colorDark.css
  7. 378
      src/web/html/history.html
  8. 24
      src/web/html/style.css
  9. 25
      src/web/lang.json

23
src/app.h

@ -314,6 +314,14 @@ class app : public IApp, public ah::Scheduler {
#endif
}
uint32_t getHistoryPeriode(uint8_t type) override {
#if defined(ENABLE_HISTORY)
return mHistory.getPeriode((HistoryStorageType)type);
#else
return 0;
#endif
}
uint16_t getHistoryMaxDay() override {
#if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay();
@ -322,6 +330,21 @@ class app : public IApp, public ah::Scheduler {
#endif
}
uint32_t getHistoryLastValueTs(uint8_t type) override {
#if defined(ENABLE_HISTORY)
return mHistory.getLastValueTs((HistoryStorageType)type);
#else
return 0;
#endif
}
#if defined(ENABLE_HISTORY_LOAD_DATA)
void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override {
#if defined(ENABLE_HISTORY)
return mHistory.addValue((HistoryStorageType)historyType, valueType, value);
#endif
}
#endif
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true

6
src/appInterface.h

@ -67,8 +67,12 @@ class IApp {
virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint32_t getHistoryPeriode(uint8_t type) = 0;
virtual uint16_t getHistoryMaxDay() = 0;
virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0;
#if defined(ENABLE_HISTORY_LOAD_DATA)
virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0;
#endif
virtual void* getRadioObj(bool nrf) = 0;
};

214
src/plugins/history.h

@ -17,6 +17,7 @@
enum class HistoryStorageType : uint8_t {
POWER,
POWER_DAY,
YIELD
};
@ -25,14 +26,18 @@ class HistoryData {
private:
struct storage_t {
uint16_t refreshCycle = 0;
uint16_t loopCnt = 0;
uint16_t listIdx = 0; // index for next Element to write into WattArr
uint16_t dispIdx = 0; // index for 1st Element to display from WattArr
bool wrapped = false;
uint16_t loopCnt;
uint16_t listIdx; // index for next Element to write into WattArr
// ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
storage_t() { data.fill(0); }
void reset() {
loopCnt = 0;
listIdx = 0;
for(uint16_t i = 0; i < (HISTORY_DATA_ARR_LENGTH + 1); i++) {
data[i] = 0;
}
}
};
public:
@ -42,8 +47,14 @@ class HistoryData {
mConfig = config;
mTs = ts;
mCurPwr.reset();
mCurPwr.refreshCycle = mConfig->inst.sendInterval;
//mYieldDay.refreshCycle = 60;
mCurPwrDay.reset();
mCurPwrDay.refreshCycle = mConfig->inst.sendInterval;
mYieldDay.reset();
mYieldDay.refreshCycle = 60;
mLastValueTs = 0;
mPgPeriod=0;
}
void tickerSecond() {
@ -51,6 +62,8 @@ class HistoryData {
float curPwr = 0;
float maxPwr = 0;
float yldDay = -0.1;
uint32_t ts = 0;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i);
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -59,46 +72,198 @@ class HistoryData {
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec);
yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
if (rec->ts > ts)
ts = rec->ts;
}
if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
mCurPwr.loopCnt = 0;
if (curPwr > 0)
if (curPwr > 0) {
mLastValueTs = ts;
addValue(&mCurPwr, roundf(curPwr));
}
if (maxPwr > 0)
mMaximumDay = roundf(maxPwr);
}
/*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
if (*mTs > mApp->getSunset()) {
if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) {
mCurPwrDay.loopCnt = 0;
if (curPwr > 0) {
mLastValueTs = ts;
addValueDay(&mCurPwrDay, roundf(curPwr));
}
}
if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
mYieldDay.loopCnt = 0;
if (*mTs > mApp->getSunset())
{
if ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay));
mDayStored = true;
}
} else if (*mTs > mApp->getSunrise())
}
else if (*mTs > mApp->getSunrise())
mDayStored = false;
}*/
}
}
uint16_t valueAt(HistoryStorageType type, uint16_t i) {
//storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay;
storage_t *s = &mCurPwr;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH;
storage_t *s=NULL;
uint16_t idx=i;
DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i));
switch (type) {
case HistoryStorageType::POWER:
s = &mCurPwr;
idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
break;
case HistoryStorageType::POWER_DAY:
s = &mCurPwrDay;
idx = i;
break;
case HistoryStorageType::YIELD:
s = &mYieldDay;
idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
break;
}
if (s)
return s->data[idx];
return 0;
}
uint16_t getMaximumDay() {
return mMaximumDay;
}
uint32_t getLastValueTs(HistoryStorageType type) {
DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type));
if (type == HistoryStorageType::POWER_DAY)
return mPgEndTime;
return mLastValueTs;
}
uint32_t getPeriode(HistoryStorageType type) {
DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type));
switch (type) {
case HistoryStorageType::POWER:
return mCurPwr.refreshCycle;
break;
case HistoryStorageType::POWER_DAY:
return mPgPeriod / HISTORY_DATA_ARR_LENGTH;
break;
case HistoryStorageType::YIELD:
return (60 * 60 * 24); // 1 day
break;
}
return 0;
}
#if defined(ENABLE_HISTORY_LOAD_DATA)
/* For filling data from outside */
void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) {
if (valueType<2) {
storage_t *s=NULL;
switch (historyType) {
case HistoryStorageType::POWER:
s = &mCurPwr;
break;
case HistoryStorageType::POWER_DAY:
s = &mCurPwrDay;
break;
case HistoryStorageType::YIELD:
s = &mYieldDay;
break;
}
if (s)
{
if (valueType==0)
addValue(s, value);
if (valueType==1)
{
if (historyType == HistoryStorageType::POWER)
s->refreshCycle = value;
if (historyType == HistoryStorageType::POWER_DAY)
mPgPeriod = value * HISTORY_DATA_ARR_LENGTH;
}
}
return;
}
if (valueType == 2)
{
if (historyType == HistoryStorageType::POWER)
mLastValueTs = value;
if (historyType == HistoryStorageType::POWER_DAY)
mPgEndTime = value;
}
}
#endif
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;
}
void addValueDay(storage_t *s, uint16_t value) {
DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value));
bool storeStartEndTimes = false;
bool store_entry = false;
uint32_t pGraphStartTime = mApp->getSunrise();
uint32_t pGraphEndTime = mApp->getSunset();
uint32_t utcTs = mApp->getTimestamp();
switch (mPgState) {
case PowerGraphState::NO_TIME_SYNC:
if ((pGraphStartTime > 0)
&& (pGraphEndTime > 0) // wait until period data is available ...
&& (utcTs >= pGraphStartTime)
&& (utcTs < pGraphEndTime)) // and current time is in period
{
storeStartEndTimes = true; // period was received -> store
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
}
break;
case PowerGraphState::IN_PERIOD:
if (utcTs > mPgEndTime) // check if end of day is reached ...
mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting
else
store_entry = true;
break;
case PowerGraphState::WAIT_4_NEW_PERIOD:
if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ...
storeStartEndTimes = true; // and store it for next period
mPgState = PowerGraphState::WAIT_4_RESTART;
}
break;
case PowerGraphState::WAIT_4_RESTART:
if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ...
mCurPwrDay.reset(); // then reset power graph data
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
mCurPwr.reset(); // also reset "last values" graph
mMaximumDay = 0; // and the maximum of the (last) day
}
break;
}
// store start and end times of current time period and calculate period length
if (storeStartEndTimes) {
mPgStartTime = pGraphStartTime;
mPgEndTime = pGraphEndTime;
mPgPeriod = pGraphEndTime - pGraphStartTime; // time period of power graph in sec for scaling of x-axis
}
if (store_entry) {
DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value));
if (mPgPeriod) {
uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod;
s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1));
} else
s->listIdx = 0;
DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx));
s->data[s->listIdx] = std::max(s->data[s->listIdx], value); // update current datapoint to maximum of all seen values
}
}
private:
@ -109,8 +274,21 @@ class HistoryData {
uint32_t *mTs = nullptr;
storage_t mCurPwr;
storage_t mCurPwrDay;
storage_t mYieldDay;
bool mDayStored = false;
uint16_t mMaximumDay = 0;
uint32_t mLastValueTs = 0;
enum class PowerGraphState {
NO_TIME_SYNC,
IN_PERIOD,
WAIT_4_NEW_PERIOD,
WAIT_4_RESTART
};
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
uint32_t mPgStartTime = 0;
uint32_t mPgEndTime = 0;
uint32_t mPgPeriod = 0; // seconds
};
#endif /*ENABLE_HISTORY*/

137
src/web/RestApi.h

@ -49,6 +49,12 @@ class RestApi {
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif
mConfig = config;
#if defined(ENABLE_HISTORY_LOAD_DATA)
//Vart67: Debugging history graph (loading data into graph storage
mSrv->on("/api/addYDHist",
HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1),
std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
#endif
mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(
std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
@ -103,6 +109,8 @@ class RestApi {
#endif /* !defined(ETHERNET) */
else if(path == "live") getLive(request,root);
else if (path == "powerHistory") getPowerHistory(request, root);
else if (path == "powerHistoryDay") getPowerHistoryDay(request, root);
else if (path == "yieldDayHistory") getYieldDayHistory(request, root);
else {
if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
@ -137,7 +145,94 @@ class RestApi {
#endif
}
void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) {
#if defined(ENABLE_HISTORY_LOAD_DATA)
// VArt67: For debugging history graph. Loading data into graph
void onApiPostYDHist(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) {
uint32_t total = request->contentLength();
DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final);
if (0 == index) {
if (NULL != mTmpBuf)
delete[] mTmpBuf;
mTmpBuf = new uint8_t[total + 1];
mTmpSize = total;
}
if (mTmpSize >= (len + index))
memcpy(&mTmpBuf[index], data, len);
if (!final)
return; // not last frame - nothing to do
mTmpSize = len + index; // correct the total size
mTmpBuf[mTmpSize] = 0;
#ifndef ESP32
DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap
#else
DynamicJsonDocument json(12000)); // does this work? I have no ESP32 :-(
#endif
DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize);
json.shrinkToFit();
JsonObject obj = json.as<JsonObject>();
// Debugging
// mTmpBuf[mTmpSize] = 0;
// DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf);
if (!err && obj) {
// insert data into yieldDayHistory object
HistoryStorageType dataType;
if (obj["maxDay"] > 0) // this is power history data
{
dataType = HistoryStorageType::POWER;
if (obj["refresh"] > 60)
dataType = HistoryStorageType::POWER_DAY;
}
else
dataType = HistoryStorageType::YIELD;
size_t cnt = obj[F("value")].size();
DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt));
for (uint16_t i = 0; i < cnt; i++) {
uint16_t val = obj[F("value")][i];
mApp->addValueToHistory((uint8_t)dataType, 0, val);
// DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", ");
}
uint32_t refresh = obj[F("refresh")];
mApp->addValueToHistory((uint8_t)dataType, 1, refresh);
if (dataType != HistoryStorageType::YIELD) {
uint32_t ts = obj[F("lastValueTs")];
mApp->addValueToHistory((uint8_t)dataType, 2, ts);
}
} else {
switch (err.code()) {
case DeserializationError::Ok:
break;
case DeserializationError::IncompleteInput:
DPRINTLN(DBG_DEBUG, F("Incomplete input"));
break;
case DeserializationError::InvalidInput:
DPRINTLN(DBG_DEBUG, F("Invalid input"));
break;
case DeserializationError::NoMemory:
DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes");
break;
default:
DPRINTLN(DBG_DEBUG, F("Deserialization failed"));
break;
}
}
request->send(204); // Success with no page load
delete[] mTmpBuf;
mTmpBuf = NULL;
}
#endif
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody");
if(0 == index) {
@ -207,6 +302,8 @@ class RestApi {
ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory");
ep[F("powerHistoryDay")] = url + F("powerHistoryDay");
ep[F("yieldDayHistory")] = url + F("yieldDayHistory");
#endif
}
@ -815,7 +912,7 @@ class RestApi {
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY)
obj[F("refresh")] = mConfig->inst.sendInterval;
obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::POWER);
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);
@ -825,8 +922,40 @@ class RestApi {
}
obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay();
#else
obj[F("refresh")] = 86400; // 1 day;
obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER);
#endif /*ENABLE_HISTORY*/
}
void getPowerHistoryDay(AsyncWebServerRequest *request, JsonObject obj){
getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY)
obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::POWER_DAY);
uint16_t max = 0;
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER_DAY, fld);
obj[F("value")][fld] = value;
if (value > max)
max = value;
}
obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay();
obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER_DAY);
#endif /*ENABLE_HISTORY*/
}
void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY)
obj[F("refresh")] = mApp->getHistoryPeriode((uint8_t)HistoryStorageType::YIELD);
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;
#endif /*ENABLE_HISTORY*/
}

4
src/web/html/colorBright.css

@ -30,4 +30,8 @@
--ch-head-bg: #006ec0;
--ts-head: #333;
--ts-bg: #555;
--chart-cont: #fbfbfb;
--chart-bg: #f9f9f9;
--chart-text: #000000;
}

4
src/web/html/colorDark.css

@ -30,4 +30,8 @@
--ch-head-bg: #236;
--ts-head: #333;
--ts-bg: #555;
--chart-cont: #0b0b0b;
--chart-bg: #090909;
--chart-text: #FFFFFF;
}

378
src/web/html/history.html

@ -13,80 +13,356 @@
<div id="wrapper">
<div id="content">
<h3>{#TOTAL_POWER}</h3>
<div>
{#LAST} <span id="pwrNumValues"></span> {#VALUES}
<div class="chartDivContainer">
<div class="chartDiv" id="pwrChart"> </div>
<p>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
{#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
{#MAXIMUM}: <span id="pwrMax"></span> W.
{#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p>
</div>
<h3>{#TOTAL_POWER_DAY}</h3>
<div class="chartDivContainer">
<div class="chartDiv" id="pwrDayChart"> </div>
<p>
{#MAX_DAY}: <span id="pwrDayMaxDay"></span> W. <br />
{#UPDATED} <span id="pwrDayRefresh"></span> {#SECONDS}
</p>
</div>
<h3>{#TOTAL_YIELD_PER_DAY}</h3>
<div class="chartDivContainer">
<div class="chartDiv" id="ydChart"> </div>
<p>
{#MAXIMUM}: <span id="ydMax"></span> Wh<br />
</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/addYDHist" 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>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
var powerHistObj = null;
var powerHistDayObj = null;
var ydHistObj = null;
Number.prototype.pad = function (size) {
var s = String(this);
while (s.length < (size || 2)) { s = "0" + s; }
return s;
}
class powChart {
static objcnt = 0; // to give each object elemets a unique name prefix
constructor(namePrefix) {
// configurable vars
this.mChartHight = 250;
this.datapoints = 256;
this.xGridDist = 50;
this.yGridDist = 100;
// info vars
this.maxValue = 0;
this.mLastValue = 0;
// intern vars
this.svg = null;
this.refreshIntervall = 30; // seconds
this.lastValueTs = 0; // Timestmp of last value
++this.objcnt;
if (namePrefix === undefined)
this.namePrefix = "powChart" + this.objcnt;
else
this.namePrefix = namePrefix;
}
init(numDatapoints) {
this.datapoints = numDatapoints;
// generate svg
const svgns = "http://www.w3.org/2000/svg";
var pwrExeOnce = true;
var ydExeOnce = true;
// make a simple rectangle
var mRefresh = 60;
var mLastValue = 0;
const mChartHeight = 250;
function parseHistory(obj, namePrefix, execOnce) {
mRefresh = obj.refresh
var data = Object.assign({}, obj.value)
numDataPts = Object.keys(data).length
if (true == execOnce) {
let s = document.createElementNS(svgns, "svg");
s.setAttribute("class", "chart");
s.setAttribute("width", (numDataPts + 2) * 2);
s.setAttribute("height", mChartHeight);
s.setAttribute("role", "img");
let g = document.createElementNS(svgns, "g");
s.appendChild(g);
for (var i = 0; i < numDataPts; i++) {
val = data[i];
let rect = document.createElementNS(svgns, "rect");
rect.setAttribute("id", namePrefix+"Rect" + i);
rect.setAttribute("x", i * 2);
rect.setAttribute("width", 2);
g.appendChild(rect);
this.svg = document.createElementNS(svgns, "svg");
this.svg.setAttribute("class", "container");
this.svg.setAttribute("id", this.namePrefix + "_svg");
this.svg.setAttribute("width", String(this.datapoints * 2 + 50));
this.svg.setAttribute("height", String(this.mChartHight + 20));
// Gradient Line
let defLgLine = document.createElementNS(svgns, "defs");
{
let lg = document.createElementNS(svgns, "linearGradient")
lg.setAttribute("id", "verlVertLine");
lg.setAttribute("x1", "0%");
lg.setAttribute("y1", "0%");
lg.setAttribute("x2", "0%");
lg.setAttribute("y2", "100%");
let s1 = document.createElementNS(svgns, "stop")
s1.setAttribute("offset", "0%");
s1.setAttribute("stop-color", "blue");
let s2 = document.createElementNS(svgns, "stop")
s2.setAttribute("offset", "80%");
s2.setAttribute("stop-color", "#5050FF");
let s3 = document.createElementNS(svgns, "stop")
s3.setAttribute("offset", "100%");
s3.setAttribute("stop-color", "gray");
lg.appendChild(s1);
lg.appendChild(s2);
lg.appendChild(s3);
defLgLine.appendChild(lg);
}
this.svg.appendChild(defLgLine);
// Gradient Fill
let defLg = document.createElementNS(svgns, "defs");
{
let lg = document.createElementNS(svgns, "linearGradient")
lg.setAttribute("id", "verlVertFill");
lg.setAttribute("x1", "0%");
lg.setAttribute("y1", "0%");
lg.setAttribute("x2", "0%");
lg.setAttribute("y2", "100%");
let s1 = document.createElementNS(svgns, "stop")
s1.setAttribute("offset", "0%");
s1.setAttribute("stop-color", "#A0A0FF");
let s2 = document.createElementNS(svgns, "stop")
s2.setAttribute("offset", "50%");
s2.setAttribute("stop-color", "#C0C0FF");
let s3 = document.createElementNS(svgns, "stop")
s3.setAttribute("offset", "100%");
s3.setAttribute("stop-color", "#E0E0F0");
lg.appendChild(s1);
lg.appendChild(s2);
lg.appendChild(s3);
defLgLine.appendChild(lg);
}
this.svg.appendChild(defLg);
let chartFrame = document.createElementNS(svgns, "rect");
chartFrame.setAttribute("id", this.namePrefix + "_chartFrame");
chartFrame.setAttribute("class", "chartFrame");
chartFrame.setAttribute("x", "0");
chartFrame.setAttribute("y", "0");
chartFrame.setAttribute("width", String(this.datapoints * 2));
chartFrame.setAttribute("height", String(this.mChartHight));
this.svg.appendChild(chartFrame);
// Group chart content
let chartContent = document.createElementNS(svgns, "g");
chartContent.setAttribute("id", this.namePrefix + "_svgChartContent");
chartFrame.setAttribute("transform", "translate(29, 5)");
chartContent.setAttribute("transform", "translate(30, 5)");
// Graph values in a polyline
let poly = document.createElementNS(svgns, "polyline");
poly.setAttribute("id", this.namePrefix + "Poly");
poly.setAttribute("stroke", "url(#verlVertLine)");
poly.setAttribute("fill", "none");
chartContent.appendChild(poly);
// hidden polyline for fill
let polyFill = document.createElementNS(svgns, "polyline");
polyFill.setAttribute("id", this.namePrefix + "PolyFill");
polyFill.setAttribute("stroke", "none");
polyFill.setAttribute("fill", "url(#verlVertFill)");
chartContent.appendChild(polyFill);
// X-grid lines
let numXGridLines = (this.mChartHight / this.xGridDist);
for (let i = 0; i < numXGridLines; i++) {
let line = document.createElementNS(svgns, "line");
line.setAttribute("id", this.namePrefix + "XGrid" + i);
line.setAttribute("x1", String(0));
line.setAttribute("x2", String(this.datapoints * 2));
line.setAttribute("y1", String(this.mChartHight - (i + 1) * this.xGridDist));
line.setAttribute("y2", String(this.mChartHight - (i + 1) * this.xGridDist));
line.setAttribute("stroke-width", "1");
line.setAttribute("stroke-dasharray", "1,1");
line.setAttribute("stroke", "#A0A0A0");
chartContent.appendChild(line);
let text = document.createElementNS(svgns, "text");
text.setAttribute("id", this.namePrefix + "XGridText" + i);
text.setAttribute("x", "0");
text.setAttribute("y", String(this.mChartHight + 10 - (i + 1) * this.xGridDist));
text.innerHTML = (i + 1) * this.xGridDist;
this.svg.appendChild(text);
}
document.getElementById(namePrefix+"Chart").appendChild(s);
// Y-grid lines
let numYGridLines = (this.datapoints / this.yGridDist) * 2;
for (let i = numYGridLines; i > 0; i--) {
let line = document.createElementNS(svgns, "line");
line.setAttribute("id", this.namePrefix + "YGrid" + i);
line.setAttribute("x1", String((i) * this.yGridDist) - 1);
line.setAttribute("x2", String((i) * this.yGridDist) - 1);
line.setAttribute("y1", String(0));
line.setAttribute("y2", String(this.mChartHight));
line.setAttribute("stroke-width", "1");
line.setAttribute("stroke-dasharray", "1,3");
line.setAttribute("stroke", "#A0A0A0");
chartContent.appendChild(line);
let text = document.createElementNS(svgns, "text");
text.setAttribute("id", this.namePrefix + "YGridText" + i);
text.setAttribute("x", String((i) * this.yGridDist + 15));
text.setAttribute("y", String(this.mChartHight + 17));
text.innerHTML = "";
this.svg.appendChild(text);
}
//
this.svg.appendChild(chartContent);
};
getContainer() { return this.svg; };
setXScale(refreshIntervall, lastValueTs) {
this.refreshIntervall = refreshIntervall;
this.lastValueTs = lastValueTs;
}
update(values, maxVal) {
if (maxVal === undefined) {
this.maxValue = 0;
for (let val in values)
if (val > this.maxValue) this.maxValue = val;
}
else
this.maxValue = maxVal;
// normalize data to chart
let divider = obj.max / mChartHeight;
let divider = this.maxValue / this.mChartHight;
if (divider == 0)
divider = 1;
for (var i = 0; i < numDataPts; i++) {
val = data[i];
if (val > 0)
mLastValue = val
val = val / divider
rect = document.getElementById(namePrefix+"Rect" + i);
rect.setAttribute("height", val);
rect.setAttribute("y", mChartHeight - val);
let firstValPos = -1; // position of first value >0 W
let lastValPos = -1; // position of last value >0 W
let points = "";
for (let i = 0; i < this.datapoints; i++) {
let val = values[i];
if (val > 0) {
this.mLastValue = val;
lastValPos = i;
if (firstValPos < 0)
firstValPos = i;
val = val / divider;
points += ' ' + String(i * 2) + ',' + String(this.mChartHight - val);
}
document.getElementById(namePrefix + "Max").innerHTML = obj.max;
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
}
let poly = document.getElementById(this.namePrefix + "Poly");
poly.setAttribute("points", points);
// "close" polyFill-line down to the x-axis
points += ' ' + +String(lastValPos * 2) + ',' + String(this.mChartHight);
points += ' ' + +String(firstValPos * 2) + ',' + String(this.mChartHight);
let polyFill = document.getElementById(this.namePrefix + "PolyFill");
polyFill.setAttribute("points", points);
// X-Grid lines
let numXGridLines = (this.mChartHight / this.xGridDist);
let dist = (this.maxValue / numXGridLines);
for (let i = 0; i < numXGridLines; i++) {
let tex = document.getElementById(this.namePrefix + "XGridText" + i);
tex.innerHTML = ((i + 1) * dist).toFixed(0);
}
// Y-Grid lines
if (isNaN(this.lastValueTs) || this.lastValueTs == 0)
this.lastValueTs = Date.now();
let date = new Date(this.lastValueTs);
let numYGridLines = (this.datapoints / this.yGridDist) * 2;
for (let i = numYGridLines; i > 0; i--) {
let tex = document.getElementById(this.namePrefix + "YGridText" + i);
if (this.refreshIntervall > 8600) // Display date
tex.innerHTML = date.getDate() + "." + (date.getMonth() + 1).pad(2);
else // Display time
tex.innerHTML = date.getHours() + ":" + date.getMinutes().pad(2);
date = new Date(date.getTime() - (this.refreshIntervall * (this.yGridDist / 2) * 1000));
}
};
}// class powChart
function parsePowerHistory(obj){
if (null != obj) {
let refresh = obj.refresh
let maximum = obj.max;
let addNextChart=false;
if (powerHistObj == null) {
powerHistObj = new powChart("ph");
powerHistObj.init(obj.value.length);
document.getElementById("pwrChart").appendChild(powerHistObj.getContainer());
// Regular update:
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", refresh * 1000);
// one after the other
addNextChart=true;
}
powerHistObj.setXScale(refresh, obj.lastValueTs * 1000);
powerHistObj.update(obj.value, maximum);
document.getElementById("pwrLast").innerHTML = powerHistObj.mLastValue;
//document.getElementById("pwrMaxDay").innerHTML = obj.maxDay;
document.getElementById("pwrMax").innerHTML = maximum;
document.getElementById("pwrRefresh").innerHTML = refresh;
document.getElementById("pwrNumValues").innerHTML = obj.value.length;
if (addNextChart)
setTimeout(() => { getAjax("/api/powerHistoryDay", parsePowerHistoryDay); }, 50);
}
}
function parsePowerHistoryDay(obj) {
if (null != obj) {
let refresh = obj.refresh
if (refresh<30)
refresh = 30;
let maximum = obj.max;
let addNextChart = false;
if (powerHistDayObj == null) {
powerHistDayObj = new powChart("phDay");
powerHistDayObj.init(obj.value.length);
document.getElementById("pwrDayChart").appendChild(powerHistDayObj.getContainer());
// Regular update:
window.setInterval("getAjax('/api/powerHistoryDay', parsePowerHistoryDay)", refresh * 1000);
// one after the other
addNextChart = true;
}
powerHistDayObj.setXScale(refresh, obj.lastValueTs * 1000);
powerHistDayObj.update(obj.value, maximum);
//document.getElementById("pwrDayLast").innerHTML = powerHistDayObj.mLastValue;
document.getElementById("pwrDayMaxDay").innerHTML = obj.maxDay;
//document.getElementById("pwrDayMax").innerHTML = maximum;
document.getElementById("pwrDayRefresh").innerHTML = refresh;
if (addNextChart)
setTimeout(() => { getAjax("/api/yieldDayHistory", parseYieldDayHistory); }, 50);
}
}
function parseYieldDayHistory(obj) {
if (null != obj) {
parseNav(obj.generic);
parseHistory(obj,"pwr", pwrExeOnce)
document.getElementById("pwrLast").innerHTML = mLastValue
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
let refresh = obj.refresh
let maximum = obj.max;
let addNextChart = false;
if (ydHistObj == null) {
ydHistObj = new powChart("yd");
ydHistObj.init(obj.value.length);
document.getElementById("ydChart").appendChild(ydHistObj.getContainer());
// Regular update:
window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", refresh * 500);
addNextChart = true;
}
if (pwrExeOnce) {
pwrExeOnce = false;
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000);
ydHistObj.setXScale(refresh, obj.lastValueTs * 1000);
ydHistObj.update(obj.value, maximum);
document.getElementById("ydMax").innerHTML = maximum;
}
}

24
src/web/html/style.css

@ -33,13 +33,27 @@ textarea {
color: var(--fg2);
}
svg rect {fill: #00A;}
svg.chart {
background: #f2f2f2;
border: 2px solid gray;
padding: 1px;
svg.container {
background:var(--chart-cont);
}
rect.chartFrame {
fill: var(--chart-bg);
stroke: gray;
stroke-width: 1px;
}
svg polyline {
fill-opacity: .5;
stroke-width: 1;
}
svg text {
font-size: x-small;
fill: var(--chart-text);
}
div.chartDivContainer {
padding: 1px;
margin: 1px;

25
src/web/lang.json

@ -1528,6 +1528,21 @@
"en": "Total Power",
"de": "Gesamtleistung"
},
{
"token": "LAST",
"en": "Last",
"de": "Die letzten"
},
{
"token": "VALUES",
"en": "values",
"de": "Werte"
},
{
"token": "TOTAL_POWER_DAY",
"en": "Total Power Today",
"de": "Gesamtleistung heute"
},
{
"token": "TOTAL_YIELD_PER_DAY",
"en": "Total Yield per day",
@ -1535,23 +1550,23 @@
},
{
"token": "MAX_DAY",
"en": "maximum day",
"en": "Maximum day",
"de": "Tagesmaximum"
},
{
"token": "LAST_VALUE",
"en": "last value",
"de": "letzter Wert"
"en": "Last value",
"de": "Letzter Wert"
},
{
"token": "MAXIMUM",
"en": "maximum value",
"en": "Maximum value",
"de": "Maximalwert"
},
{
"token": "UPDATED",
"en": "Updated every",
"de": "aktualisiert alle"
"de": "Aktualisiert alle"
},
{
"token": "SECONDS",

Loading…
Cancel
Save