Browse Source

0.8.2

* beautified inverter settings in `setup` (preperation for future, settings become more inverter dependent)
pull/1225/head
lumapu 1 year ago
parent
commit
975f6923d3
  1. 3
      src/CHANGES.md
  2. 2
      src/config/config.h
  3. 2
      src/defines.h
  4. 3
      src/hm/hmInverter.h
  5. 2
      src/hm/hmSystem.h
  6. 31
      src/web/RestApi.h
  7. 19
      src/web/html/api.js
  8. 2
      src/web/html/colorDark.css
  9. 277
      src/web/html/setup.html
  10. 6
      src/web/html/style.css
  11. 4
      src/web/html/system.html
  12. 38
      src/web/web.h

3
src/CHANGES.md

@ -1,5 +1,8 @@
# Development Changes
## 0.8.2 - 2023-11-08
* beautified inverter settings in `setup` (preperation for future, settings become more inverter dependent)
## 0.8.1 - 2023-11-05
* added tx channel heuristics (per inverter)
* fix statistics counter

2
src/config/config.h

@ -158,7 +158,7 @@
#define MAX_RF_PAYLOAD_SIZE 32
// maximum total payload buffers (must be greater than the number of received frame fragments)
#define MAX_PAYLOAD_ENTRIES 10
#define MAX_PAYLOAD_ENTRIES 20
// number of seconds since last successful response, before inverter is marked inactive
#define INVERTER_INACT_THRES_SEC 5*60

2
src/defines.h

@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
#define VERSION_PATCH 1
#define VERSION_PATCH 2
//-------------------------------------
typedef struct {

3
src/hm/hmInverter.h

@ -118,7 +118,6 @@ class Inverter {
record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null)
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
@ -142,7 +141,6 @@ class Inverter {
actPowerLimit = 0xffff; // init feedback from inverter to -1
mDevControlRequest = false;
devControlCmd = InitDataState;
initialized = false;
alarmMesIndex = 0;
isConnected = false;
status = InverterStatus::OFF;
@ -193,7 +191,6 @@ class Inverter {
initAssignment(&recordConfig, SystemConfigPara);
initAssignment(&recordAlarm, AlarmData);
toRadioId();
initialized = true;
}
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {

2
src/hm/hmSystem.h

@ -114,7 +114,7 @@ class HmSystem {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER)
return NULL;
else if((mInverter[pos].initialized && mInverter[pos].config->serial.u64 != 0ULL) || false == check)
else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos];
else
return NULL;

31
src/web/RestApi.h

@ -124,7 +124,7 @@ class RestApi {
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody");
DynamicJsonDocument json(200);
DynamicJsonDocument json(800);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot();
@ -708,8 +708,35 @@ class RestApi {
mApp->setTimestamp(0); // 0: update ntp flag
else if(F("serial_utc_offset") == jsonIn[F("cmd")])
mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) {
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->setMqttDiscoveryFlag(); // for homeassistant
else if(F("save_iv") == jsonIn[F("cmd")]) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false);
iv->config->enabled = jsonIn[F("en")];
iv->config->serial.u64 = jsonIn[F("ser")];
snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>());
for(uint8_t i = 0; i < 6; i++) {
iv->config->chMaxPwr[i] = jsonIn[F("ch")][i][F("pwr")];
iv->config->yieldCor[i] = jsonIn[F("ch")][i][F("yld")];
snprintf(iv->config->chName[i], MAX_NAME_LENGTH, "%s", jsonIn[F("ch")][i][F("name")].as<const char*>());
}
switch(iv->config->serial.b[4]) {
case 0x24:
case 0x22:
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x44:
case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x64:
case 0x62:
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
mApp->saveSettings(false); // without reboot
} else {
jsonOut[F("error")] = F("unknown cmd");
return false;

19
src/web/html/api.js

@ -1,6 +1,4 @@
/**
* SVG ICONS
*/
/* SVG ICONS - https://icons.getbootstrap.com */
iconWifi1 = [
"M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z"
@ -34,6 +32,15 @@ iconSuccessFull = [
"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"
];
iconGear = [
"M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"
];
iconDel = [
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z",
"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"
];
/**
* GENERIC FUNCTIONS
*/
@ -119,7 +126,7 @@ function parseRssi(obj) {
icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70)
icon = iconWifi2;
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"]));
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg", obj["wifi_rssi"]));
}
function toIsoDateStr(d) {
@ -181,6 +188,10 @@ function tr(val1, val2) {
]);
}
function badge(success, text, second="error") {
return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text);
}
function des(val) {
e = document.createElement('p');
e.classList.add("subdes");

2
src/web/html/colorDark.css

@ -16,7 +16,7 @@
--secondary: #0072c8;
--nav-active: #555;
--footer-bg: #282828;
--modal-bg: #666;
--modal-bg: #282828;
--invalid-bg: #400;

277
src/web/html/setup.html

@ -140,14 +140,7 @@
<fieldset class="mb-4">
<legend class="des">Inverter</legend>
<div id="inverter"></div>
<div class="row mb-2">
<div class="col-12 col-sm-3"></div>
<div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div>
</div>
<div class="row mb-2">
<div class="col-12 col-sm-3"><p class="subdes">Note</p></div>
<div class="col-12 col-sm-9"><p>A 'max module power' value of '0' disables the channel in 'live' view</p></div>
</div>
<div class="row mb-2">
<div class="col-12 col-sm-3"><p class="subdes">General</p></div>
<div class="col-12 col-sm-9"></div>
@ -451,8 +444,6 @@
[1, "high active"],
];
const re = /1[0,1,3][2,4,6,8][1,2,4].*/;
window.onload = function() {
for(it of document.getElementsByClassName("s_collapsible")) {
it.addEventListener("click", function() {
@ -471,10 +462,6 @@
});
}
document.getElementById("btnAdd").addEventListener("click", function() {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":6,"ch_max_pwr":[0,0,0,0,0,0],"ch_name":["","","","","",""],"ch_yield_cor":[0,0,0,0,0,0]}'));
});
function apiCbWifi(obj) {
var e = document.getElementById("networks");
selDelAllOpt(e);
@ -573,98 +560,6 @@
return null;
}
function ivHtml(obj) {
var id = getFreeId();
if(null == id) {
setHide("btnAdd", true);
return;
}
var iv = ml("div", {id: "inv" + id}, null);
document.getElementById("inverter").appendChild(iv);
iv.appendChild(des("Inverter " + id));
id = "inv" + id;
var addr = ml("input", {name: id + "Addr", class: "text", type: "number", max: 138999999999, value: obj["serial"]}, null);
iv.append(
mlCb(id + "Enable", "Communication Enable", obj["enabled"]),
mlE("Serial Number (12 digits)*", addr)
);
['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4);
var max = 0;
for(var i=0;i<6;i++) {
setHide(id+"ModPwr"+i, true);
setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true);
}
setHide("row"+id+"ModPwr", true);
setHide("row"+id+"ModName", true);
setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1) || (serial.charAt(1) == 3)) {
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2) || (serial.charAt(3) == 4)) {
switch(serial.charAt(2)) {
default:
case "2": max = 1; break;
case "4": max = 2; break;
case "6": max = 4; break;
case "8": max = 6; break;
}
}
}
}
if(max != 0) {
for(var i=0;i<max;i++) {
setHide(id+"ModPwr"+i, false);
setHide(id+"ModName"+i, false);
setHide(id+"YieldCor"+i, false);
}
setHide("row"+id+"ModPwr", false);
setHide("row"+id+"ModName", false);
setHide("row"+id+"YieldCor", false);
}
})
});
iv.append(mlE("Name*", inp(id + "Name", obj["name"], 15, ["text"], null, "text", "[\\-\\+A-Za-z0-9.\\/#$%&=_]+", "Invalid input")));
for(var j of [
["ModPwr", "ch_max_pwr", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 15, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[\\-0-9\.]+"]]) {
var cl = (re.test(obj["serial"])) ? "" : " hide";
i = 0;
arrIn = [];
for(it of obj[j[1]]) {
arrIn.push(ml("div", {class: "col-3 "},
inp(id + j[0] + i, it, j[3], [], id + j[0] + i, "text", j[4], "Invalid input")
));
i++;
}
iv.append(
ml("div", {class: "row mb-2 mb-sm-3" + cl, id: "row" + id + j[0]}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, j[2]),
ml("div", {class: "col-12 col-sm-9"},
ml("div", {class: "row"}, arrIn)
)
])
);
}
var del = ml("input", {class: "btn btnDel", type: "button", id: id+"del", value: "X"}, null);
del.addEventListener("click", delIv);
iv.append(mlE("Delete", del));
}
function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["invRetry", "retries"], ["yldEff", "yldEff"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
@ -709,11 +604,177 @@
function parseIv(obj) {
maxInv = obj["max_num_inverters"];
for(var i = 0; i < obj.inverter.length; i++)
ivHtml(obj.inverter[i]);
var lines = [];
lines.push(ml("tr", {}, [
ml("th", {style: "width: 10%; text-align: center;"}, ""),
ml("th", {}, "Name"),
ml("th", {}, "Serial"),
ml("th", {style: "width: 10%; text-align: center;"}, "Edit"),
ml("th", {style: "width: 10%; text-align: center;"}, "Delete")
]));
for(let i = 0; i < obj.inverter.length; i++) {
lines.push(ml("tr", {}, [
ml("td", {}, badge(obj.inverter[i].enabled, (obj.inverter[i].enabled) ? "enabled" : "disabled")),
ml("td", {}, obj.inverter[i].name),
ml("td", {}, String(obj.inverter[i].serial)),
ml("td", {style: "text-align: center;", onclick: function() {ivModal(obj.inverter[i]);}}, svg(iconGear, 25, 25, "icon icon-fg pointer")),
ml("td", {style: "text-align: center; ", onclick: function() {ivDel(obj.inverter[i]);}}, svg(iconDel, 25, 25, "icon icon-fg pointer"))
]));
}
var add = new Object();
add.id = obj.inverter.length;
add.name = "";
add.enabled = true;
add.ch_max_pwr = [];
add.ch_name = [];
add.ch_yield_cor = [];
var e = document.getElementById("inverter");
e.innerHTML = ""; // remove all childs
e.append(ml("table", {class: "table"}, ml("tbody", {}, lines)));
e.append(ml("div", {class: "row my-3"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "add Inverter", class: "btn", onclick: function() { ivModal(add); }}, null))));
ivGlob(obj);
}
function ivModal(obj) {
var lines = [];
lines.push(ml("tr", {}, [
ml("th", {style: "width: 10%;"}, "Input"),
ml("th", {}, "Max Module Power [Wp]"),
ml("th", {}, "Name (optional)"),
ml("th", {}, "Yield Correction [kWh] (optional)")
]));
for(let i = 0; i < 6; i++) {
lines.push(ml("tr", {id: "ch"+i}, [
ml("td", {}, String(i+1)),
ml("td", {}, ml("input", {name: "ch_p"+i, class: "text", type: "number", max: 999, value: obj.ch_max_pwr[i]}, null)),
ml("td", {}, ml("input", {name: "ch_n"+i, class: "text", type: "text", maxlength: 15, value: (undefined === obj.ch_name[i]) ? "" : obj.ch_name[i]}, null)),
ml("td", {}, ml("input", {name: "yld_c"+i, class: "text", type: "number", max: 999999, value: obj.ch_yield_cor[i]}, null))
]));
}
var cbEn = ml("input", {name: "enable", type: "checkbox"}, null);
if(obj.enabled)
cbEn.checked = true;
var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null);
var html = ml("div", {}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-4"}, "Serial"),
ml("div", {class: "col-8"}, ser)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-4"}, "Name"),
ml("div", {class: "col-8"}, ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-4"}, "Enable"),
ml("div", {class: "col-8"}, cbEn)
]),
ml("div", {class: "row mb-3"},
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8", id: "res"}, ""),
ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "save", class: "btn", onclick: function() { ivSave(); }}, null))
])
]);
['keyup', 'change'].forEach(function(evt) {
ser.addEventListener(evt, (e) => {
var serial = ser.value.substring(0,4);
var max = 1;
for(var i = 0; i < 6; i++) {
setHide("ch"+i, true);
}
if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1) || (serial.charAt(1) == 3)) {
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2) || (serial.charAt(3) == 4)) {
switch(serial.charAt(2)) {
default:
case "2": max = 1; break;
case "4": max = 2; break;
case "6": max = 4; break;
case "8": max = 6; break;
}
}
}
}
for(var i = 0; i < max; i++) {
setHide("ch"+i, false);
}
})
});
modal("Edit inverter", html);
ser.dispatchEvent(new Event('change'));
function ivSave() {
var o = new Object();
o.cmd = "save_iv";
o.id = obj.id;
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked;
o.ch = [];
for(let i = 0; i < 6; i++) {
var q = new Object();
q.pwr = document.getElementsByName("ch_p"+i)[0].value;
q.name = document.getElementsByName("ch_n"+i)[0].value;
q.yld = document.getElementsByName("yld_c"+i)[0].value;
o.ch.push(q);
}
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
function cb(obj) {
var e = document.getElementById("res");
if(!obj.success)
e.innerHTML = "error while saving";
else {
modalClose();
getAjax("/api/inverter/list", parseIv);
}
}
}
function ivDel(obj) {
var html = ml("div", {class: "row"}, [
ml("div", {class: "col-9"}, "do you realy want to delete inverter " + obj.name + "?"),
ml("div", {class: "col-3 a-r"}, ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "yes", class: "btn", onclick: function() { del(); }}, null)))
]);
modal("Delete inverter " + obj.name, html);
function del() {
var o = new Object();
o.cmd = "save_iv";
o.id = obj.id;
o.ser = 0;
o.name = "";
o.en = false;
o.ch = [];
for(let i = 0; i < 6; i++) {
var q = new Object();
q.pwr = 0;
q.name = "";
q.yld = 0;
o.ch.push(q);
}
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
function cb(obj) {
if(obj.success) {
modalClose();
getAjax("/api/inverter/list", parseIv);
}
}
}
function parseMqtt(obj) {
for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]])
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];

6
src/web/html/style.css

@ -94,8 +94,8 @@ svg.icon {
fill: var(--success);
}
.wifi {
fill: var(--fg2);
.icon-fg {
fill: var(--fg);
}
.title {
@ -708,7 +708,7 @@ div.hr {
width: 100%;
background-color: var(--modal-bg);
background-clip: padding-box;
border: 1px solid rgba(0,0,0,.2);
border: 1px solid var(--fg);
flex-direction: column;
}

4
src/web/html/system.html

@ -40,10 +40,6 @@
);
}
function badge(success, text, second="error") {
return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text);
}
function headline(text) {
return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text)))
}

38
src/web/web.h

@ -487,44 +487,6 @@ class Web {
request->arg("ipGateway").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.gateway, buf);
// inverter
Inverter<> *iv;
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i, false);
// enable communication
iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on");
// address
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if (strlen(buf) == 0)
memset(buf, 0, 20);
iv->config->serial.u64 = ah::Serial2u64(buf);
switch(iv->config->serial.b[4]) {
case 0x24:
case 0x22:
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x44:
case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x64:
case 0x62:
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
// name
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for (uint8_t j = 0; j < 6; j++) {
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toDouble();
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
}
iv->initialized = true;
}
if (request->arg("invInterval") != "")
mConfig->nrf.sendInterval = request->arg("invInterval").toInt();
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");

Loading…
Cancel
Save