mirror of https://github.com/lumapu/ahoy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
479 lines
21 KiB
479 lines
21 KiB
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Live</title>
|
|
<link rel="stylesheet" type="text/css" href="style.css?v=0.8.0001"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta charset="UTF-8">
|
|
<script type="text/javascript" src="api.js?v=0.8.0001"></script>
|
|
<link rel="stylesheet" type="text/css" href="colors.css?v=0.8.0001"/>
|
|
<meta name="robots" content="noindex, nofollow" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
</head>
|
|
<body>
|
|
<div class="topnav">
|
|
<a href="/?v=0.8.0001" class="title">AhoyDTU</a>
|
|
<a href="javascript:void(0);" class="icon" onclick="topnav()">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</a>
|
|
<div id="topnav" class="mobile">
|
|
<a id="nav3" class="hide" href="/live?v=0.8.0001">Live</a>
|
|
<a id="nav4" class="hide" href="/serial?v=0.8.0001">Webserial</a>
|
|
<a id="nav5" class="hide" href="/setup?v=0.8.0001">Settings</a>
|
|
<span class="seperator"></span>
|
|
<a id="nav6" class="hide" href="/update?v=0.8.0001">Update</a>
|
|
<a id="nav7" class="hide" href="/system?v=0.8.0001">System</a>
|
|
<span class="seperator"></span>
|
|
<a id="nav8" href="/api" target="_blank">REST API</a>
|
|
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
|
|
<a id="nav10" href="/about?v=0.8.0001">About</a>
|
|
<span class="seperator"></span>
|
|
<a id="nav0" class="hide" href="/login">Login</a>
|
|
<a id="nav1" class="hide" href="/logout">Logout</a>
|
|
</div>
|
|
<div id="wifiicon" class="info"></div>
|
|
</div>
|
|
<div id="wrapper">
|
|
<div id="content">
|
|
<div id="live"></div>
|
|
<p>Every <span id="refresh"></span> seconds the values are updated</p>
|
|
</div>
|
|
</div>
|
|
<div id="footer">
|
|
<div class="left">
|
|
<a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2023</a>
|
|
<ul>
|
|
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
|
|
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="right">
|
|
<ul>
|
|
<li><a target="_blank" href="https://github.com/lumapu/ahoy/commits/bf77275">GIT SHA: bf77275 :: 0.8.0001</a></li>
|
|
<li id="esp_type"></li>
|
|
<li><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed" target="_blank" >CC BY-NC-SA 4.0</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<script type="text/javascript">
|
|
var exeOnce = true;
|
|
var units, ivEn;
|
|
var mIvHtml = [];
|
|
var mNum = 0;
|
|
var total = Array(5).fill(0);
|
|
var tPwrAck;
|
|
|
|
function parseGeneric(obj) {
|
|
if(true == exeOnce){
|
|
parseNav(obj);
|
|
parseESP(obj);
|
|
}
|
|
parseRssi(obj);
|
|
}
|
|
|
|
function numBig(val, unit, des) {
|
|
return ml("div", {class: "col-6 col-sm-4 a-c"}, [
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"}, [
|
|
ml("span", {class: "fs-5 fs-md-4"}, String(Math.round(val * 100) / 100)),
|
|
ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit)
|
|
])),
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"},
|
|
ml("span", {class: "fs-9 px-1"}, des)
|
|
)
|
|
)
|
|
]);
|
|
}
|
|
|
|
function numMid(val, unit, des) {
|
|
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"}, [
|
|
ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)),
|
|
ml("span", {class: "fs-8 mx-1"}, unit)
|
|
])),
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"},
|
|
ml("span", {class: "fs-9"}, des)
|
|
)
|
|
)
|
|
]);
|
|
}
|
|
|
|
function totals() {
|
|
for(var i = 0; i < 5; i++) {
|
|
total[i] = Math.round(total[i] * 100) / 100;
|
|
}
|
|
|
|
return ml("div", {class: "row mt-3 mb-5"},
|
|
ml("div", {class: "col"}, [
|
|
ml("div", {class: "p-2 total-h"},
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL")
|
|
),
|
|
),
|
|
ml("div", {class: "p-2 total-bg"}, [
|
|
ml("div", {class: "row"}, [
|
|
numBig(total[0], "W", "AC Power"),
|
|
numBig(total[1], "Wh", "Yield Day"),
|
|
numBig(total[2], "kWh", "Yield Total")
|
|
]),
|
|
ml("div", {class: "hr"}),
|
|
ml("div", {class: "row"}, [
|
|
numMid(total[3], "W", "DC Power"),
|
|
numMid(total[4], "var", "Reactive Power")
|
|
])
|
|
])
|
|
])
|
|
);
|
|
}
|
|
function ivHead(obj) {
|
|
if(0 != obj.status) { // only add totals if inverter is online
|
|
total[0] += obj.ch[0][2]; // P_AC
|
|
total[3] += obj.ch[0][8]; // P_DC
|
|
total[4] += obj.ch[0][10]; // Q_AC
|
|
}
|
|
total[1] += obj.ch[0][7]; // YieldDay
|
|
total[2] += obj.ch[0][6]; // YieldTotal
|
|
|
|
var t = span(" °C");
|
|
var clh = (0 == obj.status) ? "iv-h-dis" : "iv-h";
|
|
var clbg = (0 == obj.status) ? "iv-bg-dis" : "iv-bg";
|
|
var pwrLimit = "n/a";
|
|
|
|
if(65535 != obj.power_limit_read) {
|
|
pwrLimit = obj.power_limit_read + " %";
|
|
if(0 != obj.max_pwr)
|
|
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W";
|
|
}
|
|
|
|
return ml("div", {class: "row mt-2"},
|
|
ml("div", {class: "col"}, [
|
|
ml("div", {class: "p-2 " + clh},
|
|
ml("div", {class: "row"}, [
|
|
ml("div", {class: "col mx-2 mx-md-1"}, ml("span", { class: "pointer", onclick: function() {
|
|
getAjax("/api/inverter/version/" + obj.id, parseIvVersion);
|
|
}}, obj.name)),
|
|
ml("div", {class: "col a-c", onclick: function() {limitModal(obj)}}, [
|
|
ml("span", {class: "d-none d-sm-block pointer"}, "Active Power Control: " + pwrLimit),
|
|
ml("span", {class: "d-block d-sm-none pointer"}, "APC: " + pwrLimit)
|
|
]),
|
|
ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
|
|
getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
|
|
}}, ("Alarms: " + obj.alarm_cnt))),
|
|
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5]) + t.innerText)
|
|
])
|
|
),
|
|
ml("div", {class: "p-2 " + clbg}, [
|
|
ml("div", {class: "row"},[
|
|
numBig(obj.ch[0][2], "W", "AC Power"),
|
|
numBig(obj.ch[0][7], "Wh", "Yield Day"),
|
|
numBig(obj.ch[0][6], "kWh", "Yield Total")
|
|
]),
|
|
ml("div", {class: "hr"}),
|
|
ml("div", {class: "row mt-2"},[
|
|
numMid(obj.ch[0][11], "W", "Max AC Power"),
|
|
numMid(obj.ch[0][8], "W", "DC Power"),
|
|
numMid(obj.ch[0][0], "V", "AC Voltage"),
|
|
numMid(obj.ch[0][1], "A", "AC Current"),
|
|
numMid(obj.ch[0][3], "Hz", "Frequency"),
|
|
numMid(obj.ch[0][9], "%", "Efficiency"),
|
|
numMid(obj.ch[0][10], "var", "Reactive Power"),
|
|
numMid(obj.ch[0][4], "", "Power Factor")
|
|
])
|
|
])
|
|
])
|
|
);
|
|
}
|
|
|
|
function numCh(val, unit, des) {
|
|
return ml("div", {class: "col-12 col-sm-6 col-md-12 mb-2"}, [
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"}, [
|
|
ml("span", {class: "fs-6 fs-md-7"}, String(Math.round(val * 100) / 100)),
|
|
ml("span", {class: "fs-8 mx-2"}, unit)
|
|
])),
|
|
ml("div", {class: "row"},
|
|
ml("div", {class: "col"},
|
|
ml("span", {class: "fs-9"}, des)
|
|
)
|
|
)
|
|
]);
|
|
}
|
|
|
|
function ch(status, name, vals) {
|
|
var clh = (0 == status) ? "iv-h-dis" : "ch-h";
|
|
var clbg = (0 == status) ? "iv-bg-dis" : "ch-bg";
|
|
return ml("div", {class: "col-6 col-md-3 mt-2"}, [
|
|
ml("div", {class: "p-2 a-c " + clh}, name),
|
|
ml("div", {class: "p-2 " + clbg}, [
|
|
ml("div", {class: "row"}, [
|
|
numCh(vals[2], units[2], "DC Power"),
|
|
numCh(vals[6], units[2], "Max Power"),
|
|
numCh(vals[5], units[5], "Irradiation"),
|
|
numCh(vals[3], units[3], "Yield Day"),
|
|
numCh(vals[4], units[4], "Yield Total"),
|
|
numCh(vals[0], units[0], "DC Voltage"),
|
|
numCh(vals[1], units[1], "DC Current")
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
|
|
function tsInfo(obj) {
|
|
var ageInfo = "Last received data requested at: ";
|
|
if(obj.ts_last_success > 0) {
|
|
var date = new Date(obj.ts_last_success * 1000);
|
|
ageInfo += toIsoDateStr(date);
|
|
}
|
|
else
|
|
ageInfo += "nothing received";
|
|
|
|
if(obj.rssi > -127) {
|
|
if(obj.generation < 2)
|
|
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)";
|
|
else
|
|
ageInfo += " (RSSI: " + obj.rssi + "dBm)";
|
|
}
|
|
|
|
return ml("div", {class: "mb-5"}, [
|
|
ml("div", {class: "row p-1 ts-h mx-2"},
|
|
ml("div", {class: "col"}, "")
|
|
),
|
|
ml("div", {class: "row p-2 ts-bg mx-2"},
|
|
ml("div", { class: "pointer col mx-2", onclick: function() {
|
|
getAjax("/api/inverter/radiostat/" + obj.id, parseIvRadioStats);
|
|
}}, ageInfo)
|
|
)
|
|
]);
|
|
}
|
|
|
|
function parseIv(obj) {
|
|
mNum++;
|
|
|
|
var chn = [];
|
|
for(var i = 1; i < obj.ch.length; i++) {
|
|
var name = obj.ch_name[i];
|
|
if(name.length == 0)
|
|
name = "CHANNEL " + i;
|
|
if(obj.ch_max_pwr[i] > 0) // show channel only if max mod pwr
|
|
chn.push(ch(obj.status, name, obj.ch[i]));
|
|
}
|
|
mIvHtml.push(
|
|
ml("div", {}, [
|
|
ivHead(obj),
|
|
ml("div", {class: "row mb-2"}, chn),
|
|
tsInfo(obj)
|
|
])
|
|
);
|
|
|
|
|
|
var last = true;
|
|
for(var i = obj.id + 1; i < ivEn.length; i++) {
|
|
if((i != ivEn.length) && ivEn[i]) {
|
|
last = false;
|
|
getAjax("/api/inverter/id/" + i, parseIv);
|
|
break;
|
|
}
|
|
}
|
|
if(last) {
|
|
if(mNum > 1)
|
|
mIvHtml.unshift(totals());
|
|
document.getElementById("live").replaceChildren(...mIvHtml);
|
|
}
|
|
}
|
|
|
|
function parseIvAlarm(obj) {
|
|
var html = [];
|
|
var offs = new Date().getTimezoneOffset() * -60;
|
|
html.push(
|
|
ml("div", {class: "row"}, [
|
|
ml("div", {class: "col"}, ml("strong", {}, "Event")),
|
|
ml("div", {class: "col"}, ml("strong", {}, "ID")),
|
|
ml("div", {class: "col"}, ml("strong", {}, "Start")),
|
|
ml("div", {class: "col"}, ml("strong", {}, "End"))
|
|
])
|
|
);
|
|
|
|
for(a of obj.alarm) {
|
|
if(a.code != 0) {
|
|
html.push(
|
|
ml("div", {class: "row"}, [
|
|
ml("div", {class: "col mt-3"}, String(a.str)),
|
|
ml("div", {class: "col mt-3"}, String(a.code)),
|
|
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))),
|
|
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
|
|
])
|
|
);
|
|
}
|
|
}
|
|
modal("Alarms of inverter " + obj.iv_name, ml("div", {}, html));
|
|
}
|
|
|
|
function parseIvVersion(obj) {
|
|
var model;
|
|
switch(obj.generation) {
|
|
case 0: model = "MI-"; break;
|
|
case 1: model = "HM-"; break;
|
|
case 2: model = "HMS-"; break;
|
|
case 3: model = "HMT-"; break;
|
|
default: model = "???-"; break;
|
|
}
|
|
model += String(obj.max_pwr) + " (Serial: " + obj.serial + ")";
|
|
|
|
|
|
var html = ml("table", {class: "table"}, [
|
|
ml("tbody", {}, [
|
|
tr("Model", model),
|
|
tr("Firmware Version / Build", String(obj.fw_ver) + " (build: " + String(obj.fw_date) + " " + String(obj.fw_time) + ")"),
|
|
tr("Hardware Version / Build", (obj.hw_ver/100).toFixed(2) + " (build: " + String(obj.prod_cw) + "/" + String(obj.prod_year) + ")"),
|
|
tr("Hardware Number", obj.part_num.toString(16)),
|
|
tr("Bootloader Version", (obj.boot_ver/100).toFixed(2))
|
|
])
|
|
]);
|
|
modal("Info for inverter " + obj.name, ml("div", {}, html));
|
|
}
|
|
|
|
function parseIvRadioStats(obj) {
|
|
var html = ml("table", {class: "table"}, [
|
|
ml("tbody", {}, [
|
|
tr("TX count", obj.tx_cnt),
|
|
tr("RX success", obj.rx_success),
|
|
tr("RX fail", obj.rx_fail),
|
|
tr("RX no answer", obj.rx_fail_answer),
|
|
tr("RX fragments", obj.frame_cnt),
|
|
tr("TX retransmits", obj.retransmits)
|
|
])
|
|
]);
|
|
modal("Radio statistics for inverter " + obj.name, ml("div", {}, html));
|
|
}
|
|
|
|
function limitModal(obj) {
|
|
var opt = [["pct", "%"], ["watt", "W"]];
|
|
var html = ml("div", {}, [
|
|
ml("div", {class: "row mb-3"}, [
|
|
ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"),
|
|
ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")),
|
|
ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct"))
|
|
]),
|
|
ml("div", {class: "row mb-3"}, [
|
|
ml("div", {class: "col-8 col-sm-5"}, "Keep limit over inverter restart"),
|
|
ml("div", {class: "col-4 col-sm-7"}, ml("input", {type: "checkbox", name: "keep"}))
|
|
]),
|
|
ml("div", {class: "row my-3"},
|
|
ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "Apply", class: "btn", onclick: function() {
|
|
applyLimit(obj.id);
|
|
}}, null))
|
|
),
|
|
ml("div", {class: "row my-4"}, [
|
|
ml("div", {class: "col-12 col-sm-5 my-2"}, "Control"),
|
|
ml("div", {class: "col col-sm-7 a-r"}, [
|
|
ml("input", {type: "button", value: "restart", class: "btn", onclick: function() {
|
|
applyCtrl(obj.id, "restart");
|
|
}}, null),
|
|
ml("input", {type: "button", value: "turn off", class: "btn mx-1", onclick: function() {
|
|
applyCtrl(obj.id, "power", 0);
|
|
}}, null),
|
|
ml("input", {type: "button", value: "turn on", class: "btn", onclick: function() {
|
|
applyCtrl(obj.id, "power", 1);
|
|
}}, null)
|
|
])
|
|
]),
|
|
ml("div", {class: "row mt-1"}, [
|
|
ml("div", {class: "col-12 col-sm-5 my-2"}, "Result"),
|
|
ml("div", {class: "col-sm-7 my-2"}, ml("span", {name: "pwrres"}, "-"))
|
|
])
|
|
]);
|
|
modal("Active Power Control for inverter " + obj.name, html);
|
|
}
|
|
|
|
function applyLimit(id) {
|
|
var cmd = "limit_";
|
|
if(!document.getElementsByName("keep")[0].checked)
|
|
cmd += "non";
|
|
cmd += "persistent_";
|
|
if(document.getElementsByName("type")[0].value == "pct")
|
|
cmd += "relative";
|
|
else
|
|
cmd += "absolute";
|
|
|
|
var val = document.getElementsByName("limit")[0].value;
|
|
if(isNaN(val))
|
|
val = 100;
|
|
|
|
var obj = new Object();
|
|
obj.id = id;
|
|
obj.cmd = cmd;
|
|
obj.val = val;
|
|
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
|
|
}
|
|
|
|
function applyCtrl(id, cmd, val=0) {
|
|
var obj = new Object();
|
|
obj.id = id;
|
|
obj.cmd = cmd;
|
|
obj.val = val;
|
|
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj));
|
|
}
|
|
|
|
function ctrlCb(obj) {
|
|
var e = document.getElementsByName("pwrres")[0];
|
|
if(obj.success) {
|
|
e.innerHTML = "received command, waiting for inverter acknowledge ...";
|
|
tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000);
|
|
}
|
|
else
|
|
e.innerHTML = "Error: " + obj["error"];
|
|
}
|
|
|
|
function ctrlCb2(obj) {
|
|
var e = document.getElementsByName("pwrres")[0];
|
|
if(obj.success)
|
|
e.innerHTML = "command received";
|
|
else
|
|
e.innerHTML = "Error: " + obj["error"];
|
|
}
|
|
|
|
function updatePwrAck(obj) {
|
|
if(!obj.ack)
|
|
return;
|
|
var e = document.getElementsByName("pwrres")[0];
|
|
clearInterval(tPwrAck);
|
|
if(null == e)
|
|
return;
|
|
e.innerHTML = "inverter acknowledged active power control command";
|
|
}
|
|
|
|
function parse(obj) {
|
|
if(null != obj) {
|
|
parseGeneric(obj["generic"]);
|
|
units = Object.assign({}, obj["fld_units"]);
|
|
ivEn = Object.values(Object.assign({}, obj["iv"]));
|
|
mIvHtml = [];
|
|
mNum = 0;
|
|
total.fill(0);
|
|
for(var i = 0; i < obj.iv.length; i++) {
|
|
if(obj.iv[i]) {
|
|
getAjax("/api/inverter/id/" + i, parseIv);
|
|
break;
|
|
}
|
|
}
|
|
if(obj.refresh < 5)
|
|
obj.refresh = 5;
|
|
document.getElementById("refresh").innerHTML = obj.refresh;
|
|
if(true == exeOnce) {
|
|
window.setInterval("getAjax('/api/live', parse)", obj.refresh * 1000);
|
|
exeOnce = false;
|
|
}
|
|
}
|
|
else
|
|
document.getElementById("refresh").innerHTML = "n/a";
|
|
}
|
|
|
|
getAjax("/api/live", parse);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|