mirror of
https://github.com/lumapu/ahoy.git
synced 2025-08-06 09:58:23 +02:00
parent
fa32906707
commit
fa2028e479
10 changed files with 164 additions and 139 deletions
|
@ -1,5 +1,8 @@
|
||||||
# Development Changes
|
# Development Changes
|
||||||
|
|
||||||
|
## 0.8.50 - 2024-01-09
|
||||||
|
* merge PR: added history charts to web #1336
|
||||||
|
|
||||||
## 0.8.49 - 2024-01-08
|
## 0.8.49 - 2024-01-08
|
||||||
* fix send total values if inverter state is different from `OFF` #1331
|
* fix send total values if inverter state is different from `OFF` #1331
|
||||||
* fix german language issues #1335
|
* fix german language issues #1335
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 8
|
#define VERSION_MINOR 8
|
||||||
#define VERSION_PATCH 49
|
#define VERSION_PATCH 50
|
||||||
|
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
@ -794,7 +794,6 @@ class RestApi {
|
||||||
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
|
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
|
||||||
getGeneric(request, obj.createNestedObject(F("generic")));
|
getGeneric(request, obj.createNestedObject(F("generic")));
|
||||||
obj[F("refresh")] = mConfig->inst.sendInterval;
|
obj[F("refresh")] = mConfig->inst.sendInterval;
|
||||||
obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH;
|
|
||||||
uint16_t max = 0;
|
uint16_t max = 0;
|
||||||
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
|
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
|
||||||
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
|
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
|
||||||
|
@ -809,7 +808,6 @@ class RestApi {
|
||||||
void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) {
|
void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) {
|
||||||
getGeneric(request, obj.createNestedObject(F("generic")));
|
getGeneric(request, obj.createNestedObject(F("generic")));
|
||||||
obj[F("refresh")] = 86400; // 1 day
|
obj[F("refresh")] = 86400; // 1 day
|
||||||
obj[F("datapoints")] = HISTORY_DATA_ARR_LENGTH;
|
|
||||||
uint16_t max = 0;
|
uint16_t max = 0;
|
||||||
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
|
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
|
||||||
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld);
|
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld);
|
||||||
|
|
|
@ -1,135 +1,113 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{#NAV_HISTORY}</title>
|
||||||
|
{#HTML_HEADER}
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<head>
|
</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}
|
||||||
<body>
|
<div id="wrapper">
|
||||||
{#HTML_NAV}
|
<div id="content">
|
||||||
<div id="wrapper">
|
<h3>{#TOTAL_POWER}</h3>
|
||||||
<div id="content">
|
<div>
|
||||||
<h3>Total Power history</h3>
|
<div class="chartDiv" id="pwrChart"> </div>
|
||||||
<div class="chartDivContainer">
|
<p>
|
||||||
<div class="chartDiv" id="phHistoryChart"> </div>
|
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
|
||||||
<p class="center" style="margin:0px;border:0px;">
|
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
|
||||||
Maximum day: <span id="phMaximumDay"></span> W. Last value: <span id="phActual"></span> W.<br />
|
</p>
|
||||||
Maximum graphics: <span id="phMaximum"></span> W. Updated every <span id="phRefresh"></span> seconds </p>
|
</div>
|
||||||
</div>
|
<h3>{#TOTAL_YIELD_PER_DAY}</h3>
|
||||||
<h3>Yield per day history</h3>
|
<div>
|
||||||
<div class="chartDivContainer">
|
<div class="chartDiv" id="ydChart"> </div>
|
||||||
<div class="chartDiv" id="ydHistoryChart"> </div>
|
<p>
|
||||||
<p class="center" style="margin:0px;border:0px;">
|
{#MAXIMUM}: <span id="ydMax"></span> Wh<br />
|
||||||
Maximum value: <span id="ydMaximum"></span> Wh<br />
|
{#UPDATED} <span id="ydRefresh"></span> {#SECONDS}
|
||||||
Updated every <span id="ydRefresh"></span> seconds </p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{#HTML_FOOTER}
|
||||||
|
|
||||||
<h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4>
|
<script type="text/javascript">
|
||||||
<fieldset style="padding: 1px;">
|
const svgns = "http://www.w3.org/2000/svg";
|
||||||
<legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call </legend>
|
var pwrExeOnce = true;
|
||||||
<form id="form" method="POST" action="/api/insertYieldDayHistory" enctype="multipart/form-data" accept-charset="utf-8">
|
var ydExeOnce = true;
|
||||||
<input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()">
|
// make a simple rectangle
|
||||||
<input type="file" name="insert" style="width: 80%;">
|
var mRefresh = 60;
|
||||||
</form>
|
var mLastValue = 0;
|
||||||
</fieldset>
|
const mChartHeight = 250;
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#HTML_FOOTER}
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
function parseHistory(obj, namePrefix, execOnce) {
|
||||||
const svgns = "http://www.w3.org/2000/svg";
|
mRefresh = obj.refresh
|
||||||
var phExeOnce = true;
|
var data = Object.assign({}, obj.value)
|
||||||
var ydExeOnce = true;
|
var numDataPts = data.length
|
||||||
// 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) {
|
if (true == execOnce) {
|
||||||
mRefresh = obj["refresh"];
|
let s = svg(null, (numDataPts + 2) * 2, mChartHeight, "chart");
|
||||||
phDatapoints = obj["datapoints"];
|
s.setAttribute("role", "img");
|
||||||
mDataValues = Object.assign({}, obj["value"]);
|
let g = document.createElementNS(svgns, "g");
|
||||||
mMaximum = obj["maximum"];
|
s.appendChild(g);
|
||||||
// generate svg
|
for (var i = 0; i < numDataPts; i++) {
|
||||||
if (true == execOnce) {
|
val = data[i];
|
||||||
let svg = document.createElementNS(svgns, "svg");
|
let rect = document.createElementNS(svgns, "rect");
|
||||||
svg.setAttribute("class", "chart");
|
rect.setAttribute("id", namePrefix+"Rect" + i);
|
||||||
svg.setAttribute("width", String((phDatapoints+2) * 2));
|
rect.setAttribute("x", String(i * 2) + "");
|
||||||
svg.setAttribute("height", String(mChartHight) + "");
|
rect.setAttribute("width", String(2) + "");
|
||||||
svg.setAttribute("aria-labelledby", "title desc");
|
g.appendChild(rect);
|
||||||
svg.setAttribute("role", "img");
|
}
|
||||||
t = ml("title");
|
document.getElementById(namePrefix+"Chart").appendChild(s);
|
||||||
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);
|
// normalize data to chart
|
||||||
</script>
|
let divider = obj.max / mChartHeight;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
document.getElementById(namePrefix + "Max").innerHTML = obj.max;
|
||||||
|
if (mRefresh < 5)
|
||||||
|
mRefresh = 5;
|
||||||
|
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
</body>
|
function parsePowerHistory(obj){
|
||||||
|
if (null != obj) {
|
||||||
|
parseHistory(obj,"pwr", pwrExeOnce)
|
||||||
|
document.getElementById("pwrLast").innerHTML = mLastValue
|
||||||
|
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
|
||||||
|
}
|
||||||
|
if (pwrExeOnce) {
|
||||||
|
pwrExeOnce = false;
|
||||||
|
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000);
|
||||||
|
|
||||||
</html>
|
setTimeout(() => {
|
||||||
|
getAjax("/api/yieldDayHistory", parseYieldDayHistory);
|
||||||
|
} , 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function parseYieldDayHistory(obj) {
|
||||||
|
if (null != obj) {
|
||||||
|
parseNav(obj.generic);
|
||||||
|
parseHistory(obj, "yd", ydExeOnce)
|
||||||
|
}
|
||||||
|
if (ydExeOnce) {
|
||||||
|
ydExeOnce = false;
|
||||||
|
window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", mRefresh * 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAjax("/api/powerHistory", parsePowerHistory);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Save</title>
|
<title>{#NAV_SAVE}</title>
|
||||||
{#HTML_HEADER}
|
{#HTML_HEADER}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Serial Console</title>
|
<title>{#NAV_WEBSERIAL}</title>
|
||||||
{#HTML_HEADER}
|
{#HTML_HEADER}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -36,21 +36,17 @@ textarea {
|
||||||
svg rect {fill: #0000AA;}
|
svg rect {fill: #0000AA;}
|
||||||
svg.chart {
|
svg.chart {
|
||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
border: 2px solid gray;
|
border: 2px solid gray;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chartDivContainer {
|
div.chartDivContainer {
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
}
|
}
|
||||||
div.chartdivContainer span {
|
div.chartdivContainer span {
|
||||||
color: var(--fg2);
|
color: var(--fg2);
|
||||||
}
|
}
|
||||||
div.chartDiv {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.topnav {
|
.topnav {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Live</title>
|
<title>{#NAV_LIVE}</title>
|
||||||
{#HTML_HEADER}
|
{#HTML_HEADER}
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Setup Wizard</title>
|
<title>{#NAV_WIZARD}</title>
|
||||||
{#HTML_HEADER}
|
{#HTML_HEADER}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -3,13 +3,18 @@
|
||||||
{
|
{
|
||||||
"name": "general",
|
"name": "general",
|
||||||
"list": [
|
"list": [
|
||||||
|
{
|
||||||
|
"token": "NAV_WIZARD",
|
||||||
|
"en": "Setup Wizard",
|
||||||
|
"de": "Daten"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"token": "NAV_LIVE",
|
"token": "NAV_LIVE",
|
||||||
"en": "Live",
|
"en": "Live",
|
||||||
"de": "Daten"
|
"de": "Daten"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"token": "{#NAV_HISTORY}",
|
"token": "NAV_HISTORY",
|
||||||
"en": "History",
|
"en": "History",
|
||||||
"de": "Verlauf"
|
"de": "Verlauf"
|
||||||
},
|
},
|
||||||
|
@ -28,6 +33,11 @@
|
||||||
"en": "Documentation",
|
"en": "Documentation",
|
||||||
"de": "Dokumentation"
|
"de": "Dokumentation"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"token": "NAV_SAVE",
|
||||||
|
"en": "save",
|
||||||
|
"de": "speichern"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"token": "NAV_ABOUT",
|
"token": "NAV_ABOUT",
|
||||||
"en": "About",
|
"en": "About",
|
||||||
|
@ -1334,6 +1344,46 @@
|
||||||
"de": "Fehler beim Speichern"
|
"de": "Fehler beim Speichern"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "history.html",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"token": "TOTAL_POWER",
|
||||||
|
"en": "Total Power",
|
||||||
|
"de": "Gesamtleistung"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "TOTAL_YIELD_PER_DAY",
|
||||||
|
"en": "Total Yield per day",
|
||||||
|
"de": "Gesamtertrag pro Tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "MAX_DAY",
|
||||||
|
"en": "maximum day",
|
||||||
|
"de": "Tagesmaximum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "LAST_VALUE",
|
||||||
|
"en": "last value",
|
||||||
|
"de": "letzter Wert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "MAXIMUM",
|
||||||
|
"en": "maximum value",
|
||||||
|
"de": "Maximalwert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "UPDATED",
|
||||||
|
"en": "Updated every",
|
||||||
|
"de": "aktualisiert alle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "SECONDS",
|
||||||
|
"en": "seconds",
|
||||||
|
"de": "Sekunden"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue