Browse Source

0.7.64

* moved active power control to modal in `live` view (per inverter) by click on current APC state
pull/1219/head
lumapu 1 year ago
parent
commit
1bbe979bcd
  1. 3
      src/CHANGES.md
  2. 2
      src/defines.h
  3. 14
      src/web/RestApi.h
  4. 2
      src/web/html/includes/nav.html
  5. 114
      src/web/html/serial.html
  6. 2
      src/web/html/setup.html
  7. 103
      src/web/html/visualization.html

3
src/CHANGES.md

@ -1,5 +1,8 @@
# Development Changes
## 0.7.64 - 2023-10-02
* moved active power control to modal in `live` view (per inverter) by click on current APC state
## 0.7.63 - 2023-10-01
* fix NRF24 communication #1200

2
src/defines.h

@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 7
#define VERSION_PATCH 63
#define VERSION_PATCH 64
//-------------------------------------
typedef struct {

14
src/web/RestApi.h

@ -105,6 +105,8 @@ class RestApi {
getIvVersion(root, request->url().substring(22).toInt());
else if(path.substring(0, 19) == "inverter/radiostat/")
getIvStatistis(root, request->url().substring(24).toInt());
else if(path.substring(0, 16) == "inverter/pwrack/")
getIvPowerLimitAck(root, request->url().substring(21).toInt());
else
getNotFound(root, F("http://") + request->host() + F("/api/"));
}
@ -276,7 +278,7 @@ class RestApi {
void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
getSysInfo(request, obj.createNestedObject(F("system")));
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">AhoyFactory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
}
void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) {
@ -322,6 +324,15 @@ class RestApi {
obj[F("retransmits")] = iv->radioStatistics.retransmits;
}
void getIvPowerLimitAck(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
return;
}
obj["ack"] = (bool)iv->powerLimitAck;
}
void getInverterList(JsonObject obj) {
JsonArray invArr = obj.createNestedArray(F("inverter"));
@ -652,6 +663,7 @@ class RestApi {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
return false;
}
jsonOut[F("id")] = jsonIn[F("id")];
if(F("power") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff);

2
src/web/html/includes/nav.html

@ -7,7 +7,7 @@
</a>
<div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live?v={#VERSION}">Live</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">Serial / Control</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">Webserial</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">Settings</a>
<span class="seperator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>

114
src/web/html/serial.html

@ -12,53 +12,13 @@
<textarea id="serial" class="mt-3" cols="80" rows="20" readonly></textarea>
</div>
<div class="row my-3">
<div class="col-3">connected: <span class="dot" id="connected"></span></div>
<div class="col-3">console active: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="col-6 col-sm-4">
<div class="col-6 col-sm-4 a-r">
<input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/>
</div>
</div>
<div class="hr my-3"></div>
<div class="row mb-3">
<h3>Commands</h3>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Select Inverter</div>
<div class="col-12 col-sm-9"><select name="iv" id="InvID"></select></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Power Limit Command</div>
<div class="col-12 col-sm-9">
<select name="pwrlimctrl">
<option value="" selected disabled hidden>select the unit and persistence</option>
<option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
<option value="limit_nonpersistent_relative">relative non persistent [%]</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Power Limit Value</div>
<div class="col-12 col-sm-9"><input type="number" name="pwrlimval" maxlength="4"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3"></div>
<div class="col-12 col-sm-9"><input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Control Inverter</div>
<div class="col-12 col-sm-9" id="power">
<input type="button" value="Restart" class="btn" id="restart"/>
<input type="button" value="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/>
</div>
</div>
<div class="row mb-5">
<div class="col-3 my-2">Ctrl result</div>
<div class="col-9 my-2"><span id="result">n/a</span></div>
</div>
</div>
</div>
{#HTML_FOOTER}
@ -84,23 +44,11 @@
parseESP(obj);
window.setInterval("getAjax('/api/generic', parseGeneric)", 10000);
exeOnce = false;
getAjax("/api/inverter/list", parse);
}
setTimeOffset();
}
function parse(root) {
select = document.getElementById('InvID');
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.length; i++) {
inv = root[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;
select.appendChild(opt);
}
function setTimeOffset() {
// set time offset for serial console
var obj = new Object();
obj.cmd = "serial_utc_offset";
@ -119,12 +67,12 @@
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
document.getElementById("connected").style.backgroundColor = "#0c0";
document.getElementById("active").style.backgroundColor = "#0c0";
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
document.getElementById("connected").style.backgroundColor = "#f00";
document.getElementById("active").style.backgroundColor = "#f00";
}
}, false);
@ -135,56 +83,6 @@
}, false);
}
function ctrlCb(obj) {
var e = document.getElementById("result");
if(obj["success"])
e.innerHTML = "ok";
else
e.innerHTML = "Error: " + obj["error"];
}
function get_selected_iv() {
var e = document.getElementById("InvID");
return parseInt(e.value);
}
const wrapper = document.getElementById('power');
wrapper.addEventListener('click', (event) => {
var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = "power";
switch (event.target.value) {
default:
case "Turn On":
obj.val = 1;
break;
case "Turn Off":
obj.val = 0;
break;
}
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
document.getElementById("sendpwrlim").addEventListener("click", function() {
var val = parseInt(document.getElementsByName('pwrlimval')[0].value);
var cmd = document.getElementsByName('pwrlimctrl')[0].value;
if(isNaN(val)) {
document.getElementById("result").textContent = "value is missing";
return;
}
var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
getAjax("/api/generic", parseGeneric);
</script>
</body>

2
src/web/html/setup.html

@ -683,7 +683,7 @@
if(!obj["pwd_set"])
e.value = "";
var d = document.getElementById("prot_mask");
var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"];
var a = ["Index", "Live", "Webserial", "Settings", "Update", "System"];
var el = [];
for(var i = 0; i < 6; i++) {
var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));

103
src/web/html/visualization.html

@ -20,6 +20,7 @@
var mIvHtml = [];
var mNum = 0;
var total = Array(5).fill(0);
var tPwrAck;
function parseGeneric(obj) {
if(true == exeOnce){
@ -113,8 +114,10 @@
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 d-none d-sm-block"}, "Active Power Control: " + pwrLimit),
ml("div", {class: "col a-c d-block d-sm-none"}, "APC: " + pwrLimit),
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))),
@ -305,6 +308,102 @@
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"]);

Loading…
Cancel
Save