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.
 
 
 
 
 
 

541 lines
25 KiB

<!doctype html>
<html>
<head>
<title>{#NAV_LIVE}</title>
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div id="live"></div>
<p>{#EVERY} <span id="refresh"></span> {#UPDATE_SECS}</p>
<div id="zero_export"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
var exeOnce = true;
var units, ivEn;
var mIvHtml = [];
var mNum = 0;
var total = Array(6).fill(0);
var tPwrAck;
function getErrStr(code) {
if("ERR_AUTH") return "{#ERR_AUTH}"
if("ERR_INDEX") return "{#ERR_INDEX}"
if("ERR_UNKNOWN_CMD") return "{#ERR_UNKNOWN_CMD}"
if("ERR_LIMIT_NOT_ACCEPT") return "{#ERR_LIMIT_NOT_ACCEPT}"
if("ERR_UNKNOWN_CMD") return "{#ERR_AUTH}"
return "n/a"
}
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, opt={class: "fs-6"}) {
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", opt, 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 < 6; 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", "{#MAX_POWER}"),
numMid(total[4], "W", "{#DC_POWER}"),
numMid(total[5], "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[4] += obj.ch[0][8]; // P_DC
total[5] += obj.ch[0][10]; // Q_AC
}
total[3] += obj.ch[0][11]; // MAX P_AC
total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal
var t = span("&nbsp;&deg;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 + "&nbsp;%";
if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W";
}
var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000));
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].toFixed(1)) + 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}", {class: "fs-6 tooltip", data: maxAcPwr}),
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}: ";
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) ? "&gt;=" : "&lt;") + " -64&nbsp;dBm)";
else
ageInfo += " (RSSI: " + obj.rssi + "&nbsp;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_MODAL}: " + 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("{#HW_NUMBER}", obj.part_num.toString(16)),
tr("Bootloader Version", (obj.boot_ver/100).toFixed(2)),
tr("Grid Profile", ml("input", {type: "button", value: "{#BTN_SHOW}", class: "btn", onclick: function() {
modalClose();
getAjax("/api/inverter/grid/" + obj.id, showGridProfile);
}}, null))
])
])
modal("{#INV_INFO}: " + obj.name, ml("div", {}, html))
}
function getGridValue(g) {
var val = (parseInt(g.grid.substring(g.offs*3, g.offs*3+2), 16) * 256)
+ parseInt(g.grid.substring(g.offs*3+3, g.offs*3+5), 16)
g.offs += 2
return val
}
function getGridIdentifier(g) {
return "0x" + getGridValue(g).toString(16).padStart(4, '0')
}
function getGridType(t, id) {
for(e of t) {
if(undefined !== e[id])
return e[id]
}
return null
}
function parseGridGroup(g) {
var id = getGridIdentifier(g)
var type = getGridType(g.info.grp_codes, id.substring(0, 4))
var content = []
content.push(ml("div", {class: "row"},
ml("div", {class: "col head p-2 mt-3"},
ml("div", {class: "col a-c"}, type + " (Code " + id + ")")
)
))
content.push(ml("div", {class: "row my-2"}, [
ml("div", {class: "col-4"}, ml("b", {}, "Name")),
ml("div", {class: "col-3"}, ml("b", {}, "{#VALUE}")),
ml("div", {class: "col-3"}, ml("b", {}, "{#RANGE}")),
ml("div", {class: "col-2"}, ml("b", {}, "{#DEFAULT}"))
]))
for(e of g.info.group) {
if(Array.isArray(e[id])) {
for(e of e[id]) {
var v = String(getGridValue(g) / e.div);
var vt = (v !== String(e.def)) ? "b" : "span";
content.push(ml("div", {class: "row mt-2"}, [
ml("div", {class: "col-4"}, e.name),
ml("div", {class: "col-3"}, ml(vt, {}, v + ((undefined !== e.unit) ? " [" + e.unit + "]" : ""))),
ml("div", {class: "col-3"}, (undefined !== e.min) ? (e.min + " - " + e.max) : "n/a"),
ml("div", {class: "col-2"}, String(e.def))
]))
}
}
}
return ml("div", {class: "col"}, [...content])
}
function showGridProfile(obj) {
getJSON("/grid_info.json").then(data => {
var glob = {offs:0, grid:obj.grid, info: data}
var content = [];
var g = getGridType(glob.info.type, getGridIdentifier(glob))
var v = getGridValue(glob);
if(null === g) {
if(0 == obj.grid.length) {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}"))))
} else {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}"))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}."))))
}
} else {
content.push(ml("div", {class: "row"},
ml("div", {class: "col my-3"}, ml("h5", {}, g + " (Version " + (Math.round(v / 0x1000)) + "." + (Math.round((v & 0x0ff0) / 0x10)) + "." + (v & 0x0F) + ")"))
))
while((glob.offs*3) < glob.grid.length) {
content.push(parseGridGroup(glob))
}
}
if(0 != obj.grid.length)
content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid))))
modal("{#PROFILE_MODAL}: " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content])))
})
}
function parseIvRadioStats(obj) {
var html = ml("table", {class: "table"}, [
ml("tbody", {}, [
tr2(["{#TX_COUNT}", obj.tx_cnt, ""]),
tr2(["{#RX_SUCCESS}", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_FAIL}", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_NO_ANSWER}", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_FRAGMENTS}", obj.frame_cnt, ""]),
tr2(["{#TX_RETRANSMITS}", obj.retransmits, ""]),
tr2(["{#INV_LOSS_RATE}", "{#LOST_1} " + obj.ivLoss + " {#LOST_2} " + obj.ivSent + " {#LOST_3}", String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + "&nbsp;%"]),
tr2(["{#DTU_LOSS_RATE}", "{#LOST_1} " + obj.dtuLoss + " {#LOST_2} " + obj.dtuSent + " {#LOST_3}", String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + "&nbsp;%"])
])
])
modal("{#RADIO_STAT_MODAL}: " + 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", step: "0.1", min: 1}, "")),
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}"),
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: "{#BTN_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("{#POWER_LIMIT_MODAL}: " + 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.token = "*"
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.token = "*"
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 = "{#CMD_RECEIVED_WAIT_ACK}";
tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000);
}
else
e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
}
function ctrlCb2(obj) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success)
e.innerHTML = "{#COMMAND_RECEIVED}";
else
e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
}
function updatePwrAck(obj) {
if(!obj.ack)
return;
var e = document.getElementsByName("pwrres")[0];
clearInterval(tPwrAck);
if(null == e)
return;
e.innerHTML = "{#INV_ACK}";
}
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>