Browse Source

improved web API for `live`

added dark mode option
converted all forms to reponsive design
repaired menu with password protection #720, #716, #709
pull/736/head
lumapu 2 years ago
parent
commit
70cb0dcd45
  1. 6
      src/CHANGES.md
  2. 4
      src/config/settings.h
  3. 2
      src/defines.h
  4. 4
      src/hm/hmDefines.h
  5. 1
      src/platformio.ini
  6. 86
      src/web/RestApi.h
  7. 53
      src/web/html/api.js
  8. 27
      src/web/html/colorBright.css
  9. 27
      src/web/html/colorDark.css
  10. 2
      src/web/html/includes/header.html
  11. 14
      src/web/html/includes/nav.html
  12. 26
      src/web/html/index.html
  13. 10
      src/web/html/login.html
  14. 71
      src/web/html/serial.html
  15. 489
      src/web/html/setup.html
  16. 449
      src/web/html/style.css
  17. 11
      src/web/html/update.html
  18. 247
      src/web/html/visualization.html
  19. 86
      src/web/web.h

6
src/CHANGES.md

@ -2,6 +2,12 @@
(starting from release version `0.5.66`) (starting from release version `0.5.66`)
## 0.5.93
* improved web API for `live`
* added dark mode option
* converted all forms to reponsive design
* repaired menu with password protection #720, #716, #709
## 0.5.92 ## 0.5.92
* fix mobile menu * fix mobile menu
* fix inverters in select `serial.html` #709 * fix inverters in select `serial.html` #709

4
src/config/settings.h

@ -51,6 +51,7 @@ typedef struct {
char deviceName[DEVNAME_LEN]; char deviceName[DEVNAME_LEN];
char adminPwd[PWD_LEN]; char adminPwd[PWD_LEN];
uint16_t protectionMask; uint16_t protectionMask;
bool darkMode;
// wifi // wifi
char stationSsid[SSID_LEN]; char stationSsid[SSID_LEN];
@ -292,6 +293,7 @@ class settings {
memset(&mCfg, 0, sizeof(settings_t)); memset(&mCfg, 0, sizeof(settings_t));
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
mCfg.sys.darkMode = false;
// restore temp settings // restore temp settings
if(keepWifi) if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
@ -354,6 +356,7 @@ class settings {
obj[F("dev")] = mCfg.sys.deviceName; obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd; obj[F("adm")] = mCfg.sys.adminPwd;
obj[F("prot_mask")] = mCfg.sys.protectionMask; obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode;
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@ -365,6 +368,7 @@ class settings {
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>()); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>());
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>());
mCfg.sys.protectionMask = obj[F("prot_mask")]; mCfg.sys.protectionMask = obj[F("prot_mask")];
mCfg.sys.darkMode = obj[F("dark")];
ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());

2
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 5 #define VERSION_MINOR 5
#define VERSION_PATCH 92 #define VERSION_PATCH 93
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

4
src/hm/hmDefines.h

@ -29,6 +29,10 @@ const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "
"active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"};
const char* const notAvail = "n/a"; const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE};
// mqtt discovery device classes // mqtt discovery device classes
enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"};

1
src/platformio.ini

@ -15,6 +15,7 @@ include_dir = .
[env] [env]
framework = arduino framework = arduino
board_build.filesystem = littlefs board_build.filesystem = littlefs
upload_speed = 921600
;build_flags = ;build_flags =
; ;;;;; Possible Debug options ;;;;;; ; ;;;;; Possible Debug options ;;;;;;

86
src/web/RestApi.h

@ -24,6 +24,9 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR};
template<class HMSYSTEM> template<class HMSYSTEM>
class RestApi { class RestApi {
public: public:
@ -91,8 +94,12 @@ class RestApi {
else if(path == "record/alarm") getRecord(root, AlarmData); else if(path == "record/alarm") getRecord(root, AlarmData);
else if(path == "record/config") getRecord(root, SystemConfigPara); else if(path == "record/config") getRecord(root, SystemConfigPara);
else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); else if(path == "record/live") getRecord(root, RealTimeRunData_Debug);
else else {
getNotFound(root, F("http://") + request->host() + F("/api/")); if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
else
getNotFound(root, F("http://") + request->host() + F("/api/"));
}
//DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage()));
response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Origin", "*");
@ -158,7 +165,7 @@ class RestApi {
File fp = LittleFS.open("/settings.json", "r"); File fp = LittleFS.open("/settings.json", "r");
if(!fp) { if(!fp) {
DPRINTLN(DBG_ERROR, F("failed to load settings")); DPRINTLN(DBG_ERROR, F("failed to load settings"));
response = request->beginResponse(200, F("application/json"), "{}"); response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}");
} }
else { else {
String tmp = fp.readString(); String tmp = fp.readString();
@ -171,7 +178,7 @@ class RestApi {
tmp.remove(i, tmp.indexOf("\"", i)-i); tmp.remove(i, tmp.indexOf("\"", i)-i);
} }
} }
response = request->beginResponse(200, F("application/json"), tmp); response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
} }
response->addHeader("Content-Type", "application/octet-stream"); response->addHeader("Content-Type", "application/octet-stream");
@ -182,12 +189,11 @@ class RestApi {
} }
void getGeneric(JsonObject obj) { void getGeneric(JsonObject obj) {
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_uptime")] = mApp->getUptime();
obj[F("menu_prot")] = mApp->getProtection(); obj[F("menu_prot")] = mApp->getProtection();
obj[F("menu_maskH")] = ((mConfig->sys.protectionMask >> 8) & 0xff); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_maskL")] = ((mConfig->sys.protectionMask ) & 0xff); obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
#if defined(ESP32) #if defined(ESP32)
obj[F("esp_type")] = F("ESP32"); obj[F("esp_type")] = F("ESP32");
@ -199,6 +205,7 @@ class RestApi {
void getSysInfo(JsonObject obj) { void getSysInfo(JsonObject obj) {
obj[F("ssid")] = mConfig->sys.stationSsid; obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("device_name")] = mConfig->sys.deviceName; obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode;
obj[F("mac")] = WiFi.macAddress(); obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = mConfig->sys.deviceName; obj[F("hostname")] = mConfig->sys.deviceName;
@ -312,6 +319,41 @@ class RestApi {
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
} }
void getInverter(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
obj[F("id")] = id;
obj[F("enabled")] = (bool)iv->config->enabled;
obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj[F("ts_last_success")] = rec->ts;
JsonArray ch = obj.createNestedArray("ch");
// AC
uint8_t pos;
obj[F("ch_name")][0] = "AC";
JsonArray ch0 = ch.createNestedArray();
for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
pos = (iv->getPosByChFld(CH0, acList[fld], rec));
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
// DC
for(uint8_t j = 0; j < iv->channels; j ++) {
obj[F("ch_name")][j+1] = iv->config->chName[j];
JsonArray cur = ch.createNestedArray();
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) {
pos = (iv->getPosByChFld((j+1), dcList[fld], rec));
cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
}
}
}
void getMqtt(JsonObject obj) { void getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker); obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port); obj[F("port")] = String(mConfig->mqtt.port);
@ -444,12 +486,28 @@ class RestApi {
void getLive(JsonObject obj) { void getLive(JsonObject obj) {
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
JsonArray invArr = obj.createNestedArray(F("inverter")); //JsonArray invArr = obj.createNestedArray(F("inverter"));
obj["refresh_interval"] = mConfig->nrf.sendInterval; obj[F("refresh")] = mConfig->nrf.sendInterval;
uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]);
obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]);
}
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) {
obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]);
obj[F("fld_names")][fld] = String(fields[dcList[fld]]);
}
Inverter<> *iv; Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i);
bool parse = false;
if(NULL != iv)
parse = iv->config->enabled;
obj[F("iv")][i] = parse;
}
/*Inverter<> *iv;
uint8_t pos; uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i); iv = mSys->getInverterByPos(i);
@ -493,7 +551,7 @@ class RestApi {
} }
} }
} }
} }*/
} }
void getRecord(JsonObject obj, uint8_t recType) { void getRecord(JsonObject obj, uint8_t recType) {

53
src/web/html/api.js

@ -33,23 +33,63 @@ iconSuccess = [
/** /**
* GENERIC FUNCTIONS * GENERIC FUNCTIONS
*/ */
function ml(tagName, ...args) {
var el = document.createElement(tagName);
if(args[0]) {
for(var name in args[0]) {
if(name.indexOf("on") === 0) {
el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false)
} else {
el.setAttribute(name, args[0][name]);
}
}
}
if (!args[1]) {
return el;
}
return nester(el, args[1])
}
function nester(el, n) {
if (typeof n === "string") {
var t = document.createTextNode(n);
el.appendChild(t);
} else if (n instanceof Array) {
for(var i = 0; i < n.length; i++) {
if (typeof n[i] === "string") {
var t = document.createTextNode(n[i]);
el.appendChild(t);
} else if (n[i] instanceof Node){
el.appendChild(n[i]);
}
}
} else if (n instanceof Node){
el.appendChild(n)
}
return el;
}
function topnav() { function topnav() {
toggle("topnav", "mobile"); toggle("topnav", "mobile");
} }
function parseNav(obj) { function parseNav(obj) {
for(i = 0; i < 7; i++) { for(i = 0; i < 10; i++) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i); var l = document.getElementById("nav"+i);
if(window.location.pathname == "/" + l.href.split('/').pop()) if(window.location.pathname == "/" + l.href.split('/').pop())
l.classList.add("active"); l.classList.add("active");
if(obj["menu_protEn"]) { if(obj["menu_protEn"]) {
if(obj["menu_prot"]) { if(obj["menu_prot"]) {
if((((obj["menu_mask"] >> i) & 0x01) == 0x01) || (1 == i)) if(0 == i)
l.classList.remove("hide"); l.classList.remove("hide");
else if(i > 2) {
} else if(0 == i) if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00)
l.classList.remove("hide");
}
} else if(0 != i)
l.classList.remove("hide"); l.classList.remove("hide");
} else if(i > 1) } else if(i > 1)
l.classList.remove("hide"); l.classList.remove("hide");
@ -72,7 +112,7 @@ function parseRssi(obj) {
icon = iconWifi1; icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70) else if(obj["wifi_rssi"] <= -70)
icon = iconWifi2; icon = iconWifi2;
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "#fff", null, obj["wifi_rssi"])); document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"]));
} }
function setHide(id, hide) { function setHide(id, hide) {
@ -201,11 +241,10 @@ function link(dst, text, target=null) {
return a; return a;
} }
function svg(data=null, w=24, h=24, color="#000", cl=null, tooltip=null) { function svg(data=null, w=24, h=24, cl=null, tooltip=null) {
var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
s.setAttribute('width', w); s.setAttribute('width', w);
s.setAttribute('height', h); s.setAttribute('height', h);
s.setAttribute('fill', color);
s.setAttribute('viewBox', '0 0 16 16'); s.setAttribute('viewBox', '0 0 16 16');
if(null != cl) s.setAttribute('class', cl); if(null != cl) s.setAttribute('class', cl);
if(null != data) { if(null != data) {

27
src/web/html/colorBright.css

@ -0,0 +1,27 @@
:root {
--bg: #fff;
--fg: #000;
--fg2: #fff;
--info: #0000dd;
--warn: #ff7700;
--success: #009900;
--input-bg: #eee;
--nav-bg: #333;
--primary: #006ec0;
--primary-hover: #044e86;
--secondary: #0072c8;
--nav-active: #555;
--footer-bg: #282828;
--total-head-title: #8e5903;
--total-bg: #b06e04;
--iv-head-title: #1c6800;
--iv-head-bg: #32b004;
--ch-head-title: #003c80;
--ch-head-bg: #006ec0;
--ts-head: #333;
--ts-bg: #555;
}

27
src/web/html/colorDark.css

@ -0,0 +1,27 @@
:root {
--bg: #222;
--fg: #ccc;
--fg2: #fff;
--info: #0072c8;
--warn: #ffaa00;
--success: #00bb00;
--input-bg: #333;
--nav-bg: #333;
--primary: #004d87;
--primary-hover: #023155;
--secondary: #0072c8;
--nav-active: #555;
--footer-bg: #282828;
--total-head-title: #555511;
--total-bg: #666622;
--iv-head-title: #115511;
--iv-head-bg: #226622;
--ch-head-title: #112255;
--ch-head-bg: #223366;
--ts-head: #333;
--ts-bg: #555;
}

2
src/web/html/includes/header.html

@ -1,3 +1,5 @@
<link rel="stylesheet" type="text/css" href="colors.css"/>
<link rel="stylesheet" type="text/css" href="style.css"/> <link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<script type="text/javascript" src="api.js"></script> <script type="text/javascript" src="api.js"></script>

14
src/web/html/includes/nav.html

@ -6,15 +6,15 @@
<span></span> <span></span>
</a> </a>
<div id="topnav" class="mobile"> <div id="topnav" class="mobile">
<a id="nav2" class="hide" href="/live">Live</a> <a id="nav3" class="hide" href="/live">Live</a>
<a id="nav3" class="hide" href="/serial">Serial / Control</a> <a id="nav4" class="hide" href="/serial">Serial / Control</a>
<a id="nav4" class="hide" href="/setup">Settings</a> <a id="nav5" class="hide" href="/setup">Settings</a>
<span class="seperator"></span> <span class="seperator"></span>
<a id="nav5" class="hide" href="/update">Update</a> <a id="nav6" class="hide" href="/update">Update</a>
<a id="nav6" class="hide" href="/system">System</a> <a id="nav7" class="hide" href="/system">System</a>
<span class="seperator"></span> <span class="seperator"></span>
<a id="nav7" href="/api" target="_blank">REST API</a> <a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav8" href="https://ahoydtu.de" target="_blank">Documentation</a> <a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
<span class="seperator"></span> <span class="seperator"></span>
<a id="nav0" class="hide" href="/login">Login</a> <a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a> <a id="nav1" class="hide" href="/logout">Logout</a>

26
src/web/html/index.html

@ -67,12 +67,8 @@
} }
function parseGeneric(obj) { function parseGeneric(obj) {
// Disclaimer if(exeOnce)
//if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction());
/*if(exeOnce){
parseVersion(obj);
parseESP(obj); parseESP(obj);
}*/
parseRssi(obj); parseRssi(obj);
} }
@ -124,14 +120,14 @@
var p = div(["none"]); var p = div(["none"]);
for(var i of obj) { for(var i of obj) {
var icon = iconWarn; var icon = iconWarn;
var color = "#F70"; var cl = "icon-warn";
avail = ""; avail = "";
if(false == i["enabled"]) { if(false == i["enabled"]) {
avail = "disabled"; avail = "disabled";
} }
else if(false == i["is_avail"]) { else if(false == i["is_avail"]) {
icon = iconInfo; icon = iconInfo;
color = "#00d"; cl = "icon-info";
avail = "not yet available"; avail = "not yet available";
} }
else if(0 == i["ts_last_success"]) { else if(0 == i["ts_last_success"]) {
@ -144,12 +140,12 @@
if(false == i["is_producing"]) if(false == i["is_producing"])
avail += "not "; avail += "not ";
else else
color = "#090"; cl = "icon-success";
avail += "producing"; avail += "producing";
} }
p.append( p.append(
svg(icon, 20, 20, color, "icon"), svg(icon, 30, 30, "icon " + cl),
span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail), span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail),
br() br()
); );
@ -167,22 +163,22 @@
function parseWarnInfo(warn, success) { function parseWarnInfo(warn, success) {
var p = div(["none"]); var p = div(["none"]);
for(var w of warn) { for(var w of warn) {
p.append(svg(iconWarn, 20, 20, "#F70", "icon"), span(w), br()); p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br());
} }
for(var i of success) { for(var i of success) {
p.append(svg(iconSuccess, 20, 20, "#090", "icon"), span(i), br()); p.append(svg(iconSuccess, 30, 30, "icon icon-success"), span(i), br());
} }
if(commInfo.length > 0) if(commInfo.length > 0)
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span(commInfo), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br());
if(null != release) { if(null != release) {
if(getVerInt("{#VERSION}") < getVerInt(release)) if(getVerInt("{#VERSION}") < getVerInt(release))
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("Update available, current released version: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br());
else if(getVerInt("{#VERSION}") > getVerInt(release)) else if(getVerInt("{#VERSION}") > getVerInt(release))
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br());
else else
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using the current stable release: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br());
} }
document.getElementById("warn_info").replaceChildren(p); document.getElementById("warn_info").replaceChildren(p);

10
src/web/html/login.html

@ -7,11 +7,13 @@
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div id="login"> <div id="login">
<div class="pad"> <div class="p-4">
<form action="/login" method="post"> <form action="/login" method="post">
<h2>AhoyDTU</h2> <div class="row"><h2>AhoyDTU</h2></div>
<input type="password" name="pwd" value="" autofocus> <div class="row">
<input type="submit" name="login" value="login" class="btn"> <div class="col-8"><input type="password" name="pwd" autofocus></div>
<div class="col-4"><input type="submit" name="login" value="login" class="btn"></div>
</div>
</form> </form>
</div> </div>
</div> </div>

71
src/web/html/serial.html

@ -8,37 +8,56 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div class="serial"> <div class="row">
<textarea id="serial" cols="80" rows="20" readonly></textarea><br/> <textarea id="serial" class="mt-3" cols="80" rows="20" readonly></textarea>
connected: <span class="dot" id="connected"></span> </div>
Uptime: <span id="uptime"></span> <div class="row my-3">
<input type="button" value="clear" class="btn" id="clear"/> <div class="col-3">connected: <span class="dot" id="connected"></span></div>
<input type="button" value="autoscroll" class="btn" id="scroll"/> <div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="hr mt-3 mb-3"></div> <div class="col-6 col-sm-4">
<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> <h3>Commands</h3>
<label for="iv">Select Inverter:</label> </div>
<select name="iv" id="InvID"> <div class="row mb-3">
</select> <div class="col-12 col-sm-3 my-2">Select Inverter</div>
<label for="pwrlimval">Power Limit Value</label> <div class="col-12 col-sm-9"><select name="iv" id="InvID"></select></div>
<input type="number" class="text" name="pwrlimval" maxlength="4"/> </div>
<label for="pwrlimctrl">Power Limit Command</label> <div class="row mb-3">
<select name="pwrlimctrl"> <div class="col-12 col-sm-3 my-2">Power Limit Command</div>
<option value="" selected disabled hidden>select the unit and persistence</option> <div class="col-12 col-sm-9">
<option value="limit_nonpersistent_absolute">absolute non persistent [W]</option> <select name="pwrlimctrl">
<option value="limit_nonpersistent_relative">relative non persistent [%]</option> <option value="" selected disabled hidden>select the unit and persistence</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option> <option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option> <option value="limit_nonpersistent_relative">relative non persistent [%]</option>
</select> <option value="limit_persistent_absolute">absolute persistent [W]</option>
<br/> <option value="limit_persistent_relative">relative persistent [%]</option>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/> </select>
<div class="hr mt-3 mb-3"></div> </div>
<div id="power" class="mt-3"> </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="Restart" class="btn" id="restart"/>
<input type="button" value="Turn Off" class="btn" id="power_off"/> <input type="button" value="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/> <input type="button" value="Turn On" class="btn" id="power_on"/>
</div> </div>
<br/> </div>
<p>Ctrl result: <span id="result">n/a</span></p> <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> </div>
</div> </div>

489
src/web/html/setup.html

@ -20,54 +20,108 @@
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<form method="post" action="/save"> <form method="post" action="/save">
<fieldset> <button type="button" class="s_collapsible mt-4">System Config</button>
<div class="s_content">
<fieldset class="mb-2">
<legend class="des">Device Host Name</legend> <legend class="des">Device Host Name</legend>
<label for="device">Device Name</label> <div class="row mb-3">
<input type="text" name="device" class="text"/> <div class="col-12 col-sm-3">Device Name</div>
<div class="col-12 col-sm-9"><input type="text" name="device"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Dark Mode</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
</div>
</fieldset>
<fieldset class="mb-4">
<legend class="des">System Config</legend>
<p class="des">Pinout</p>
<div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div>
<p class="des">Serial Console</p>
<div class="row mb-3">
<div class="col-8 col-sm-3">print inverter data</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serEn"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Serial Debug</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div>
<div class="col-12 col-sm-9"><input type="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/></div>
</div>
</fieldset> </fieldset>
</div>
<button type="button" class="s_collapsible">Network</button> <button type="button" class="s_collapsible">Network</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-2">
<legend class="des">WiFi</legend> <legend class="des">WiFi</legend>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> <p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p>
<label for="scanbtn">Search Networks</label>
<input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/> <div class="row mb-3">
<label for="networks">Avail Networks</label> <div class="col-12 col-sm-3 my-2">Search Networks</div>
<select name="networks" id="networks" onChange="selNet()"> <div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/></div>
<option value="-1" selected disabled hidden>not scanned</option> </div>
</select>
<label for="ssid">SSID</label> <div class="row mb-2 mb-sm-3">
<input type="text" name="ssid" class="text"/> <div class="col-12 col-sm-3 my-2">Avail Networks</div>
<label for="pwd">Password</label> <div class="col-12 col-sm-9">
<input type="password" class="text" name="pwd" value="{PWD}"/> <select name="networks" id="networks" onChange="selNet()">
<option value="-1" selected disabled hidden>not scanned</option>
</select>
</div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">SSID</div>
<div class="col-12 col-sm-9"><input type="text" name="ssid"/></div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">Password</div>
<div class="col-12 col-sm-9"><input type="password" name="pwd" value="{PWD}"/></div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset class="mb-4">
<legend class="des">Static IP (optional)</legend> <legend class="des">Static IP (optional)</legend>
<p> <p>
Leave fields blank for DHCP<br/> Leave fields blank for DHCP<br/>
The following fields are parsed in this format: 192.168.1.1 The following fields are parsed in this format: 192.168.4.1
</p> </p>
<label for="ipAddr">IP Address</label> <div class="row mb-3">
<input type="text" name="ipAddr" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">IP Address</div>
<label for="ipMask">Submask</label> <div class="col-12 col-sm-9"><input type="text" name="ipAddr" maxlength="15" /></div>
<input type="text" name="ipMask" class="text" maxlength="15" /> </div>
<label for="ipDns1">DNS 1</label> <div class="row mb-3">
<input type="text" name="ipDns1" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">Submask</div>
<label for="ipDns2">DNS 2</label> <div class="col-12 col-sm-9"><input type="text" name="ipMask" maxlength="15" /></div>
<input type="text" name="ipDns2" class="text" maxlength="15" /> </div>
<label for="ipGateway">Gateway</label> <div class="row mb-3">
<input type="text" name="ipGateway" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">DNS 1</div>
<div class="col-12 col-sm-9"><input type="text" name="ipDns1" maxlength="15" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">DNS 2</div>
<div class="col-12 col-sm-9"><input type="text" name="ipDns2" maxlength="15" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Gateway</div>
<div class="col-12 col-sm-9"><input type="text" name="ipGateway" maxlength="15" /></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">Protection</button> <button type="button" class="s_collapsible">Protection</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Protection</legend> <legend class="des mx-2">Protection</legend>
<label for="adminpwd">Admin Password</label> <div class="row mb-3">
<input type="password" name="adminpwd" class="text" value="{PWD}"/> <div class="col-12 col-sm-3 mb-2 mt-2">Admin Password</div>
<input type="hidden" name="disclaimer" value="false" id="disclaimer"> <div class="col-12 col-sm-9"><input type="password" name="adminpwd" value="{PWD}"/></div>
</div>
<p>Select pages which should be protected by password</p> <p>Select pages which should be protected by password</p>
<div id="prot_mask"></div> <div id="prot_mask"></div>
</fieldset> </fieldset>
@ -75,140 +129,178 @@
<button type="button" class="s_collapsible">Inverter</button> <button type="button" class="s_collapsible">Inverter</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Inverter</legend> <legend class="des">Inverter</legend>
<div id="inverter"></div><br/> <div id="inverter"></div>
<input type="button" id="btnAdd" class="btn" value="Add Inverter"/> <div class="row mb-2">
<p class="subdes">General</p> <div class="col-12 col-sm-3"></div>
<label for="invInterval">Interval [s]</label> <div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div>
<input type="text" class="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/> </div>
<label for="invRetry">Max retries per Payload</label> <div class="row mb-2">
<input type="text" class="text" name="invRetry"/> <div class="col-12 col-sm-3"><p class="subdes">General</p></div>
<div class="col-12 col-sm-9"></div>
</div>
<label for="invRstMid">Reset values and YieldDay at midnight</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="invRstMid"/><br/> <div class="col-12 col-sm-3 my-2">Interval [s]</div>
<label for="invRstComStop">Reset values when inverter polling stops at sunset</label> <div class="col-12 col-sm-9"><input type="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/></div>
<input type="checkbox" class="cb" name="invRstComStop"/><br/> </div>
<label for="invRstNotAvail">Reset values when inverter status is 'not available'</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="invRstNotAvail"/><br/> <div class="col-12 col-sm-3 my-2">Max retries per Payload</div>
<div class="col-12 col-sm-9"><input type="text" name="invRetry"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values when inverter polling stops at sunset</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Reset values when inverter status is 'not available'</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstNotAvail"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">NTP Server</button> <button type="button" class="s_collapsible">NTP Server</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">NTP Server</legend> <legend class="des">NTP Server</legend>
<label for="ntpAddr">NTP Server / IP</label> <div class="row mb-3">
<input type="text" class="text" name="ntpAddr"/> <div class="col-12 col-sm-3 my-2">NTP Server / IP</div>
<label for="ntpPort">NTP Port</label> <div class="col-12 col-sm-9"><input type="text" name="ntpAddr"/></div>
<input type="text" class="text" name="ntpPort"/> </div>
<label for="ntpBtn">set system time</label> <div class="row mb-3">
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/> <div class="col-12 col-sm-3 my-2">NTP Port</div>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/> <div class="col-12 col-sm-9"><input type="text" name="ntpPort"/></div>
<span id="apiResultNtp"></span> </div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">set system time</div>
<div class="col-12 col-sm-9">
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/>
<span id="apiResultNtp"></span>
</div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">Sunrise & Sunset</button> <button type="button" class="s_collapsible">Sunrise & Sunset</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Sunrise & Sunset</legend> <legend class="des">Sunrise & Sunset</legend>
<p> <p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p>
Use a decimal separator: '.' (dot) for Latitude and Longitude
</p> <div class="row mb-3">
<label for="sunLat">Latitude (decimal)</label> <div class="col-12 col-sm-3 my-2">Latitude (decimal)</div>
<input type="text" class="text" name="sunLat"/> <div class="col-12 col-sm-9"><input type="text" name="sunLat"/></div>
<label for="sunLon">Longitude (decimal)</label> </div>
<input type="text" class="text" name="sunLon"/> <div class="row mb-3">
<label for="sunOffs">Offset (pre sunrise, post sunset)</label> <div class="col-12 col-sm-3 my-2">Longitude (decimal)</div>
<select name="sunOffs"></select> <div class="col-12 col-sm-9"><input type="text" name="sunLon"/></div>
<br> </div>
<label for="sunDisNightCom">Stop polling inverters during night</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="sunDisNightCom"/><br/> <div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div>
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Stop polling inverters during night</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">MQTT</button> <button type="button" class="s_collapsible">MQTT</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">MQTT</legend> <legend class="des">MQTT</legend>
<label for="mqttAddr">Broker / Server IP</label> <div class="row mb-3">
<input type="text" class="text" name="mqttAddr"/> <div class="col-12 col-sm-3 my-2">Broker / Server IP</div>
<label for="mqttPort">Port</label> <div class="col-12 col-sm-9"><input type="text" name="mqttAddr"/></div>
<input type="text" class="text" name="mqttPort"/> </div>
<label for="mqttUser">Username (optional)</label> <div class="row mb-3">
<input type="text" class="text" name="mqttUser"/> <div class="col-12 col-sm-3 my-2">Port</div>
<label for="mqttPwd">Password (optional)</label> <div class="col-12 col-sm-9"><input type="text" name="mqttPort"/></div>
<input type="password" class="text" name="mqttPwd"/> </div>
<label for="mqttTopic">Topic</label> <div class="row mb-3">
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /> <div class="col-12 col-sm-3 my-2">Username (optional)</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttUser"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Password (optional)</div>
<div class="col-12 col-sm-9"><input type="password" name="mqttPwd"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Topic</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /></div>
</div>
<p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p> <p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p>
<label for="mqttIntvl">Interval [s]</label> <div class="row mb-3">
<input type="text" class="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /> <div class="col-12 col-sm-3 my-2">Interval [s]</div>
<label for="mqttBtn">Discovery Config (homeassistant)</label> <div class="col-12 col-sm-9"><input type="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /></div>
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/> </div>
<span id="apiResultMqtt"></span> <div class="row mb-3">
</fieldset> <div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
</div> <div class="col-12 col-sm-9">
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/>
<button type="button" class="s_collapsible">System Config</button> <span id="apiResultMqtt"></span>
<div class="s_content"> </div>
<fieldset> </div>
<legend class="des">System Config</legend>
<p class="des">Pinout</p>
<div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div>
<p class="des">Serial Console</p>
<label for="serEn">print inverter data</label>
<input type="checkbox" class="cb" name="serEn"/><br/>
<label for="serDbg">Serial Debug</label>
<input type="checkbox" class="cb" name="serDbg"/><br/>
<label for="serIntvl">Interval [s]</label>
<input type="text" class="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">Display Config</button> <button type="button" class="s_collapsible">Display Config</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Display Config</legend> <legend class="des">Display Config</legend>
<div id="dispType"></div> <div id="dispType"></div>
<label for="logoEn">Show Logo</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="logoEn"/><br/> <div class="col-8 col-sm-3">Show Logo</div>
<label for="dispPwr">Turn off while inverters are offline</label> <div class="col-4 col-sm-9"><input type="checkbox" name="logoEn"/></div>
<input type="checkbox" class="cb" name="dispPwr"/><br/> </div>
<label for="dispPxSh">Enable pixel shifting</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="dispPxSh"/><br/> <div class="col-8 col-sm-3">Turn off while inverters are offline</div>
<label for="disp180">Rotate 180 degree</label> <div class="col-4 col-sm-9"><input type="checkbox" name="dispPwr"/></div>
<input type="checkbox" class="cb" name="disp180"/><br/> </div>
<div class="row mb-3">
<label for="dispCont">Contrast</label> <div class="col-8 col-sm-3">Enable pixel shifting</div>
<select name="dispCont" id="contrast"></select> <div class="col-4 col-sm-9"><input type="checkbox" name="dispPxSh"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Rotate 180 degree</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp180"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Contrast</div>
<div class="col-12 col-sm-9"><select name="dispCont" id="contrast"></select></div>
</div>
<label for="dispCont"></label>
<p class="des">Pinout</p> <p class="des">Pinout</p>
<div id="dispPins"></div> <div id="dispPins"></div>
</fieldset> </fieldset>
</div> </div>
<div class="mt-3"> <div class="row mb-4 mt-4">
<label for="reboot">Reboot device after successful save</label> <div class="col-8 col-sm-3">Reboot device after successful save</div>
<input type="checkbox" class="cb" name="reboot" checked /> <div class="col-2 col-md-6">
<input type="submit" value="save" class="btn right"/> <input type="checkbox" name="reboot" checked />
<input type="submit" value="save" class="btn right"/>
</div>
</div> </div>
<div class="hr mb-3 mt-3"></div> <div class="hr mb-3 mt-3"></div>
<div class="mb-4"> <div class="mb-4 mt-4">
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a> <a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
<fieldset> <fieldset class="mb-4">
<legend class="des">Upload / Store JSON Settings</legend> <legend class="des">Upload / Store JSON Settings</legend>
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload"> <input type="file" name="upload">
<input type="button" class="btn" value="Upload" onclick="hide()"> <input type="button" class="btn" value="Upload" onclick="hide()">
</form> </form>
</fieldset> </fieldset>
<a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a> (only saved values, passwords will be removed!) <a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a><span> (only saved values, passwords will be removed!)</span>
</div> </div>
</form> </form>
</div> </div>
@ -343,23 +435,38 @@
document.getElementsByName(id + "Name")[0].value = ""; document.getElementsByName(id + "Name")[0].value = "";
} }
function mlCb(id, des, chk=false) {
var cb = ml("input", {type: "checkbox", id: id, name: id}, "");
if(chk)
cb.checked = true;
return ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3"}, des),
ml("div", {class: "col-4 col-sm-9"}, cb)
]);
}
function mlE(des, e) {
return ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, des),
ml("div", {class: "col-12 col-sm-9"}, e)
]);
}
function ivHtml(obj, id) { function ivHtml(obj, id) {
highestId = id + 1; highestId = id + 1;
if(highestId == maxInv) if(highestId == maxInv)
setHide("btnAdd", true); setHide("btnAdd", true);
iv = document.getElementById("inverter");
var iv = document.getElementById("inverter");
iv.appendChild(des("Inverter " + id)); iv.appendChild(des("Inverter " + id));
id = "inv" + id; id = "inv" + id;
iv.appendChild(lbl(id + "Enable", "Communication Enable"));
var en = inp(id + "Enable", null, null, ["cb"], id + "Enable", "checkbox");
en.checked = obj["enabled"];
iv.appendChild(en);
iv.appendChild(br());
iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*"));
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input"); var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input");
iv.appendChild(addr); iv.append(
mlCb(id + "Enable", "Communication Enable", obj["enabled"]),
mlE("Serial Number (12 digits)*", addr)
);
['keyup', 'change'].forEach(function(evt) { ['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => { addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4); var serial = addr.value.substring(0,4);
@ -369,9 +476,9 @@
setHide(id+"ModName"+i, true); setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true); setHide(id+"YieldCor"+i, true);
} }
setHide("lbl"+id+"ModPwr", true); setHide("row"+id+"ModPwr", true);
setHide("lbl"+id+"ModName", true); setHide("row"+id+"ModName", true);
setHide("lbl"+id+"YieldCor", true); setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) { if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) { if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) {
@ -391,39 +498,44 @@
setHide(id+"ModName"+i, false); setHide(id+"ModName"+i, false);
setHide(id+"YieldCor"+i, false); setHide(id+"YieldCor"+i, false);
} }
setHide("lbl"+id+"ModPwr", false); setHide("row"+id+"ModPwr", false);
setHide("lbl"+id+"ModName", false); setHide("row"+id+"ModName", false);
setHide("lbl"+id+"YieldCor", false); setHide("row"+id+"YieldCor", false);
} }
}) })
}); });
iv.append( iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")));
lbl(id + "Name", "Name*"),
inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")
);
for(var j of [ for(var j of [
["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 16, null], ["ModName", "ch_name", "Module Name", 16, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 16, "[0-9-]+"]]) { ["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 16, "[0-9-]+"]]) {
var cl = (re.test(obj["serial"])) ? null : ["hide"]; var cl = (re.test(obj["serial"])) ? null : ["hide"];
iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0]));
d = div([j[0]]);
i = 0; i = 0;
cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"]; arrIn = [];
for(it of obj[j[1]]) { for(it of obj[j[1]]) {
d.appendChild(inp(id + j[0] + i, it, j[3], cl, id + j[0] + i, "text", j[4], "Invalid input")); 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++; i++;
} }
iv.appendChild(d);
iv.append(
ml("div", {class: "row mb-2 mb-sm-3", 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 = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button");
del.addEventListener("click", delIv); del.addEventListener("click", delIv);
iv.append( iv.append(mlE("Delete", del));
lbl(id + "lbldel", "Delete"),
del
);
} }
function ivGlob(obj) { function ivGlob(obj) {
@ -436,20 +548,18 @@
function parseSys(obj) { function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"]]) for(var i of [["device", "device_name"], ["ssid", "ssid"]])
document.getElementsByName(i[0])[0].value = obj[i[1]]; document.getElementsByName(i[0])[0].value = obj[i[1]];
var e = document.getElementsByName("adminpwd")[0]; document.getElementsByName("darkMode")[0].checked = obj["dark_mode"];
e = document.getElementsByName("adminpwd")[0];
if(!obj["pwd_set"]) if(!obj["pwd_set"])
e.value = ""; e.value = "";
var d = document.getElementById("prot_mask"); var d = document.getElementById("prot_mask");
var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"] var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"];
var el = [];
for(var i = 0; i < 6; i++) { for(var i = 0; i < 6; i++) {
var chkd = ((obj["prot_mask"] & (1 << i)) == (1 << i)); var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));
var sp = lbl("protMask" + i, a[i]); el.push(mlCb("protMask" + i, a[i], chk))
var cb = inp("protMask" + i, null, null, ["cb"], "protMask" + i, "checkbox", null, null, chkd);
if(0 == i)
d.replaceChildren(sp, cb, br());
else
d.append(sp, cb, br());
} }
d.append(...el);
} }
function parseGeneric(obj) { function parseGeneric(obj) {
@ -495,20 +605,31 @@
var e = document.getElementById("pinout"); var e = document.getElementById("pinout");
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']]; pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['led0', 'pinLed0'], ['led1', 'pinLed1']];
for(p of pins) { for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase())); e.append(
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])); ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])
)
])
);
} }
} }
function parseRadio(obj) { function parseRadio(obj) {
var e = document.getElementById("rf24"); var e = document.getElementById("rf24").append(
e.appendChild(lbl("rf24Power", "Amplifier Power Level")); ml("div", {class: "row mb-3"}, [
e.appendChild(sel("rf24Power", [ ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
[0, "MIN"], ml("div", {class: "col-12 col-sm-9"},
[1, "LOW"], sel("rf24Power", [
[2, "HIGH"], [0, "MIN"],
[3, "MAX"] [1, "LOW"],
], obj["power_level"])); [2, "HIGH"],
[3, "MAX"]
], obj["power_level"])
)
])
);
} }
function parseSerial(obj) { function parseSerial(obj) {
@ -524,14 +645,22 @@
var e = document.getElementById("dispPins"); var e = document.getElementById("dispPins");
pins = [['SCL / CS', 'pinDisp0'], ['SDA / DC', 'pinDisp1']]; pins = [['SCL / CS', 'pinDisp0'], ['SDA / DC', 'pinDisp1']];
for(p of pins) { for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase())); e.append(
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])); ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])
)
])
);
} }
var opts = [[0, "None"], [1, "Nokia5110"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]]; var opts = [[0, "None"], [1, "Nokia5110"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]];
document.getElementById("dispType").append( document.getElementById("dispType").append(
lbl("dispType", "Type"), ml("div", {class: "row mb-3"}, [
sel("dispType", opts, obj["disp_type"]) ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"),
ml("div", {class: "col-12 col-sm-9"}, sel("dispType", opts, obj["disp_type"]))
])
); );
e = document.getElementById("contrast"); e = document.getElementById("contrast");
@ -576,11 +705,7 @@
e.value = s.value; e.value = s.value;
} }
hiddenInput = document.getElementById("disclaimer")
hiddenInput.value = sessionStorage.getItem("gDisclaimer");
getAjax("/api/setup", parse); getAjax("/api/setup", parse);
</script> </script>
</body> </body>
</html> </html>

449
src/web/html/style.css

@ -4,26 +4,39 @@ html, body {
padding: 0; padding: 0;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
background-color: var(--bg);
color: var(--fg);
} }
h2 { h2 {
padding-left: 10px; padding-left: 10px;
} }
span, li, h3, label, fieldset {
color: var(--fg);
}
fieldset, input[type=submit], .btn {
border-radius: 4px;
}
#live span {
color: var(--fg2);
}
.topnav { .topnav {
background-color: #333; background-color: var(--nav-bg);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
} }
.topnav a { .topnav a {
color: #fff; color: var(--fg2);
padding: 14px 14px; padding: 14px 14px;
text-decoration: none; text-decoration: none;
font-size: 17px; font-size: 17px;
display: block; display: block;
height: 20px;
} }
#topnav a { #topnav a {
@ -33,18 +46,17 @@ h2 {
.topnav a.icon { .topnav a.icon {
top: 0; top: 0;
left: 0; left: 0;
background: #333; background: var(--nav-bg);
display: block; display: block;
position: absolute; position: absolute;
} }
.topnav a:hover { .topnav a:hover {
background-color: #044e86 !important; background-color: var(--primary-hover) !important;
color: #000;
} }
.topnav .info { .topnav .info {
color: #fff; color: var(--fg2);
position: absolute; position: absolute;
right: 24px; right: 24px;
top: 5px; top: 5px;
@ -61,8 +73,24 @@ svg.icon {
padding: 5px 7px 5px 0px; padding: 5px 7px 5px 0px;
} }
.icon-info {
fill: var(--info);
}
.icon-warn {
fill: var(--warn);
}
.icon-success {
fill: var(--success);
}
.wifi {
fill: var(--fg2);
}
.title { .title {
background-color: #006ec0; background-color: var(--primary);
color: #fff !important; color: #fff !important;
padding-left: 80px !important padding-left: 80px !important
} }
@ -78,7 +106,7 @@ svg.icon {
} }
.topnav .active { .topnav .active {
background-color: #555; background-color: var(--nav-active);
} }
span.seperator { span.seperator {
@ -89,6 +117,197 @@ span.seperator {
display: block; display: block;
} }
#content {
max-width: 1140px;
}
.total-h {
background-color: var(--total-head-title);
color: var(--fg2);
}
.total-bg {
background-color: var(--total-bg);
color: var(--fg2);
}
.iv-h {
background-color: var(--iv-head-title);
color: var(--fg2);
}
.iv-bg {
background-color: var(--iv-head-bg);
color: var(--fg2);
}
.ch-h {
background-color: var(--ch-head-title);
color: var(--fg2);
}
.ch-bg {
background-color: var(--ch-head-bg);
color: var(--fg2);
}
.ts-h {
background-color: var(--ts-head);
color: var(--fg2);
}
.ts-bg {
background-color: var(--ts-bg);
color: var(--fg2);
}
.hr {
border-top: 1px solid var(--iv-head-title);
margin: 1rem 0 1rem;
}
p {
text-align: justify;
font-size: 13pt;
color: var(--fg);
}
#footer {
background-color: var(--footer-bg);
}
.row { display: flex; max-width: 100%; flex-wrap: wrap; }
.col { flex: 1 0 0%; }
.col-1, .col-2, .col-3, .col-4,
.col-5, .col-6, .col-7, .col-8,
.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; }
.col-1 { width: 8.333333333%; }
.col-2 { width: 16.66666667%; }
.col-3 { width: 25%; }
.col-4 { width: 33.33333333%; }
.col-5 { width: 41.66666667%; }
.col-6 { width: 50%; }
.col-7 { width: 58.33333333%; }
.col-8 { width: 66.66666667%; }
.col-9 { width: 75%; }
.col-10 { width: 83.33333333%; }
.col-11 { width: 91.66666667%; }
.col-12 { width: 100%; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }
.px-1 { padding: 0 0.25rem 0 0.25rem; }
.px-2 { padding: 0 0.5rem 0 0.5rem; }
.px-3 { padding: 0 1rem 0 1rem; }
.px-4 { padding: 0 1.5rem 0 1.5rem; }
.px-5 { padding: 0 3rem 0 3rem; }
.py-1 { padding: 0.25rem 0 0.25rem; }
.py-2 { padding: 0.5rem 0 0.5rem; }
.py-3 { padding: 1rem 0 1rem; }
.py-4 { padding: 1.5rem 0 1.5rem; }
.py-5 { padding: 3rem 0 3rem; }
.mx-1 { margin: 0 0.25rem 0 0.25rem; }
.mx-2 { margin: 0 0.5rem 0 0.5rem; }
.mx-3 { margin: 0 1rem 0 1rem; }
.mx-4 { margin: 0 1.5rem 0 1.5rem; }
.mx-5 { margin: 0 3rem 0 3rem; }
.my-1 { margin: 0.25rem 0 0.25rem; }
.my-2 { margin: 0.5rem 0 0.5rem; }
.my-3 { margin: 1rem 0 1rem; }
.my-4 { margin: 1.5rem 0 1.5rem; }
.my-5 { margin: 3rem 0 3rem; }
.mt-1 { margin-top: 0.25rem }
.mt-2 { margin-top: 0.5rem }
.mt-3 { margin-top: 1rem }
.mt-4 { margin-top: 1.5rem }
.mt-5 { margin-top: 3rem }
.mb-1 { margin-bottom: 0.25rem }
.mb-2 { margin-bottom: 0.5rem }
.mb-3 { margin-bottom: 1rem }
.mb-4 { margin-bottom: 1.5rem }
.mb-5 { margin-bottom: 3rem }
.fs-1 { font-size: 3.5rem; }
.fs-2 { font-size: 3rem; }
.fs-3 { font-size: 2.5rem; }
.fs-4 { font-size: 2rem; }
.fs-5 { font-size: 1.75rem; }
.fs-6 { font-size: 1.5rem; }
.fs-7 { font-size: 1.25rem; }
.fs-8 { font-size: 1rem; }
.fs-9 { font-size: 0.75rem; }
.fs-10 { font-size: 0.5rem; }
.a-r { text-align: right; }
.a-c { text-align: center; }
.row > * {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
*, ::after, ::before {
box-sizing: border-box;
}
/* sm */
@media(min-width: 768px) {
.col-sm-1 { width: 8.333333333%; }
.col-sm-2 { width: 16.66666667%; }
.col-sm-3 { width: 25%; }
.col-sm-4 { width: 33.33333333%; }
.col-sm-5 { width: 41.66666667%; }
.col-sm-6 { width: 50%; }
.col-sm-7 { width: 58.33333333%; }
.col-sm-8 { width: 66.66666667%; }
.col-sm-9 { width: 75%; }
.col-sm-10 { width: 83.33333333%; }
.col-sm-11 { width: 91.66666667%; }
.col-sm-12 { width: 100%; }
.mb-sm-1 { margin-bottom: 0.25rem }
.mb-sm-2 { margin-bottom: 0.5rem }
.mb-sm-3 { margin-bottom: 1rem }
.mb-sm-4 { margin-bottom: 1.5rem }
.mb-sm-5 { margin-bottom: 3rem }
.fs-sm-1 { font-size: 3.5rem; }
.fs-sm-2 { font-size: 3rem; }
.fs-sm-3 { font-size: 2.5rem; }
.fs-sm-4 { font-size: 2rem; }
.fs-sm-5 { font-size: 1.75rem; }
.fs-sm-6 { font-size: 1.5rem; }
.fs-sm-7 { font-size: 1.25rem; }
.fs-sm-8 { font-size: 1rem; }
}
/* md */
@media(min-width: 992px) {
.col-md-1 { width: 8.333333333%; }
.col-md-2 { width: 16.66666667%; }
.col-md-3 { width: 25%; }
.col-md-4 { width: 33.33333333%; }
.col-md-5 { width: 41.66666667%; }
.col-md-6 { width: 50%; }
.col-md-7 { width: 58.33333333%; }
.col-md-8 { width: 66.66666667%; }
.col-md-9 { width: 75%; }
.col-md-10 { width: 83.33333333%; }
.col-md-11 { width: 91.66666667%; }
.col-md-12 { width: 100%; }
}
#wrapper { #wrapper {
min-height: 100%; min-height: 100%;
} }
@ -101,7 +320,6 @@ span.seperator {
#footer { #footer {
height: 121px; height: 121px;
margin-top: -121px; margin-top: -121px;
background-color: #555;
width: 100%; width: 100%;
font-size: 13px; font-size: 13px;
} }
@ -176,13 +394,6 @@ span.seperator {
} }
} }
/** old CSS below **/
p {
text-align: justify;
font-size: 13pt;
}
p.lic, p.lic a { p.lic, p.lic a {
font-size: 8pt; font-size: 8pt;
color: #999; color: #999;
@ -191,11 +402,11 @@ p.lic, p.lic a {
.des { .des {
margin-top: 20px; margin-top: 20px;
font-size: 13pt; font-size: 13pt;
color: #006ec0; color: var(--secondary);
} }
.s_active, .s_collapsible:hover { .s_active, .s_collapsible:hover {
background-color: #044e86; background-color: var(--primary-hover);
color: #fff; color: #fff;
} }
@ -205,34 +416,34 @@ p.lic, p.lic a {
} }
.s_collapsible { .s_collapsible {
background-color: #006ec0; background-color: var(--primary);
color: white; color: white;
cursor: pointer; cursor: pointer;
padding: 18px; padding: 12px;
width: 100%; width: 100%;
border: none; border: none;
text-align: left; text-align: left;
outline: none; outline: none;
font-size: 15px; font-size: 15px;
margin-bottom: 4px; margin-bottom: 5px;
} }
.subdes { .subdes {
font-size: 12pt; font-size: 12pt;
color: #006ec0; color: var(--secondary);
margin-left: 7px; margin-left: 7px;
} }
.subsubdes { .subsubdes {
font-size:12pt; font-size:12pt;
color:#006ec0; color:var(--secondary);
margin: 0 0 7px 12px; margin: 0 0 7px 12px;
} }
a:link, a:visited { a:link, a:visited {
text-decoration: none; text-decoration: none;
font-size: 13pt; font-size: 13pt;
color: #006ec0; color: var(--secondary);
} }
a:hover, a:focus { a:hover, a:focus {
@ -240,14 +451,14 @@ a:hover, a:focus {
} }
a.btn { a.btn {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
padding: 7px 15px 7px 15px; padding: 7px 15px 7px 15px;
display: inline-block; display: inline-block;
} }
a.btn:hover { a.btn:hover {
background-color: #044e86 !important; background-color: var(--primary-hover) !important;
} }
input, select { input, select {
@ -255,11 +466,13 @@ input, select {
font-size: 13pt; font-size: 13pt;
} }
input.text, select { input[type=text], input[type=password], select, input[type=number] {
width: 70%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 10px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px;
background-color: var(--input-bg);
color: var(--fg);
} }
input.sh { input.sh {
@ -272,7 +485,7 @@ input.btnDel {
} }
input.btn { input.btn {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
border: 0px; border: 0px;
padding: 7px 20px 7px 20px; padding: 7px 20px 7px 20px;
@ -303,10 +516,6 @@ pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
fieldset {
margin-bottom: 15px;
}
.left { .left {
float: left; float: left;
} }
@ -315,88 +524,11 @@ fieldset {
float: right; float: right;
} }
div.ch-iv {
width: 100%;
background-color: #32b004;
display: inline-block;
margin-bottom: 15px;
padding-bottom: 20px;
overflow: auto;
}
div.ch {
width: 220px;
min-height: 350px;
background-color: #006ec0;
display: inline-block;
margin: 0 20px 10px 0px;
overflow: auto;
padding-bottom: 20px;
}
div.ch-all {
width: 100%;
background-color: #b06e04;
display: inline-block;
margin-bottom: 15px;
padding-bottom: 20px;
overflow: auto;
}
div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head {
color: #fff;
display: block;
width: 100%;
text-align: center;
}
.subgrp { .subgrp {
float: left; float: left;
width: 220px; width: 220px;
} }
div.ch .unit, div.ch-iv .unit, div.ch-all .unit {
font-size: 19px;
margin-left: 10px;
}
div.ch .value, div.ch-iv .value, div.ch-all .value {
margin-top: 20px;
font-size: 24px;
}
div.ch .info, div.ch-iv .info, div.ch-all .info {
margin-top: 3px;
font-size: 10px;
}
div.ch .head {
background-color: #003c80;
padding: 10px 0 10px 0;
}
div.ch-all .head {
background-color: #8e5903;
padding: 10px 0 10px 0;
}
div.ch-iv .head {
background-color: #1c6800;
padding: 10px 0 10px 0;
}
div.iv {
max-width: 960px;
margin-bottom: 40px;
}
div.ts {
font-size: 13px;
background-color: #ddd;
border-top: 7px solid #999;
padding: 7px;
}
div.ModPwr, div.ModName, div.YieldCor { div.ModPwr, div.ModName, div.YieldCor {
width:70%; width:70%;
display: inline-block; display: inline-block;
@ -447,104 +579,19 @@ div.hr {
} }
#login { #login {
width: 300px; width: 450px;
height: 200px; height: 200px;
border: 1px solid #ccc; border: 1px solid #ccc;
background-color: #eee; background-color: var(--ts-head);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -160px; margin-top: -160px;
margin-left: -150px; margin-left: -225px;
}
#login .pad {
padding: 20px;
}
#login .pad input {
width: 100%;
padding: 7px 0 7px 0;
border: 0px;
margin-bottom: 10px;
} }
.head { .head {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
} }
.row { display: flex; max-width: 100%; flex-wrap: wrap; }
.col { flex: 1 0 0%; }
.col-1, .col-2, .col-3, .col-4,
.col-5, .col-6, .col-7, .col-8,
.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; }
.col-1 { width: 8.333333333%; }
.col-2 { width: 16.66666667%; }
.col-3 { width: 25%; }
.col-4 { width: 33.33333333%; }
.col-5 { width: 41.66666667%; }
.col-6 { width: 50%; }
.col-7 { width: 58.33333333%; }
.col-8 { width: 66.66666667%; }
.col-9 { width: 75%; }
.col-10 { width: 83.33333333%; }
.col-11 { width: 91.66666667%; }
.col-12 { width: 100%; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }
.mt-1 { margin-top: 0.25rem }
.mt-2 { margin-top: 0.5rem }
.mt-3 { margin-top: 1rem }
.mt-4 { margin-top: 1.5rem }
.mt-5 { margin-top: 3rem }
.mb-1 { margin-bottom: 0.25rem }
.mb-2 { margin-bottom: 0.5rem }
.mb-3 { margin-bottom: 1rem }
.mb-4 { margin-bottom: 1.5rem }
.mb-5 { margin-bottom: 3rem }
.a-r { text-align: right; }
.a-c { text-align: center; }
/* sm */
@media(min-width: 768px) {
.col-sm-1 { width: 8.333333333%; }
.col-sm-2 { width: 16.66666667%; }
.col-sm-3 { width: 25%; }
.col-sm-4 { width: 33.33333333%; }
.col-sm-5 { width: 41.66666667%; }
.col-sm-6 { width: 50%; }
.col-sm-7 { width: 58.33333333%; }
.col-sm-8 { width: 66.66666667%; }
.col-sm-9 { width: 75%; }
.col-sm-10 { width: 83.33333333%; }
.col-sm-11 { width: 91.66666667%; }
.col-sm-12 { width: 100%; }
}
/* md */
@media(min-width: 992px) {
.col-md-1 { width: 8.333333333%; }
.col-md-2 { width: 16.66666667%; }
.col-md-3 { width: 25%; }
.col-md-4 { width: 33.33333333%; }
.col-md-5 { width: 41.66666667%; }
.col-md-6 { width: 50%; }
.col-md-7 { width: 58.33333333%; }
.col-md-8 { width: 66.66666667%; }
.col-md-9 { width: 75%; }
.col-md-10 { width: 83.33333333%; }
.col-md-11 { width: 91.66666667%; }
.col-md-12 { width: 100%; }
}

11
src/web/html/update.html

@ -8,10 +8,13 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> <fieldset>
<input type="file" name="update"> <legend class="des">Select firmware file (*.bin)</legend>
<input type="button" class="btn" value="Update" onclick="hide()"> <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
</form> <input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()">
</form>
</fieldset>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}

247
src/web/html/visualization.html

@ -16,6 +16,11 @@
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
var exeOnce = true; var exeOnce = true;
var units, ivEn;
var mIvHtml = [];
var mNum = 0;
var names = ["Voltage", "Current", "Power", "Yield Day", "Yield Total", "Irradiation"];
var total = Array(5).fill(0);
function parseGeneric(obj) { function parseGeneric(obj) {
if(true == exeOnce){ if(true == exeOnce){
@ -25,93 +30,193 @@
parseRssi(obj); parseRssi(obj);
} }
function parseIv(obj, root) { function numBig(val, unit, des) {
var ivHtml = []; return ml("div", {class: "col-6 col-sm-4 a-c"}, [
ml("div", {class: "row"},
var tDiv = div(["ch-all", "iv"]); ml("div", {class: "col"}, [
tDiv.appendChild(span("Total", ["head"])); ml("span", {class: "fs-5 fs-md-4"}, String(val)),
var total = new Array(root.ch0_fld_names.length).fill(0); ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit)
if(obj.length > 1) ])),
ivHtml.push(tDiv); ml("div", {class: "row"},
ml("div", {class: "col"},
for(var iv of obj) { ml("span", {class: "fs-9 px-1"}, des)
if(iv["enabled"]) { )
main = div(["iv"]); )
var ch0 = div(["ch-iv"]); ]);
var limit = iv["power_limit_read"] + "%"; }
if(limit == "65535%")
limit = "n/a";
ch0.appendChild(span(iv["name"] + " Limit " + limit, ["head"]));
for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = Math.round(iv["ch"][0][j] * 100) / 100;
var sub = div(["subgrp"]);
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
ch0.appendChild(sub);
switch(j) { function numMid(val, unit, des) {
case 2: total[j] += val; break; // P_AC return ml("div", {class: "col-6 col-sm-4 col-md-3"}, [
case 6: total[j] += val; break; // YieldTotal ml("div", {class: "row"},
case 7: total[j] += val; break; // YieldDay ml("div", {class: "col"}, [
case 8: total[j] += val; break; // P_DC ml("span", {class: "fs-6"}, String(val)),
case 10: total[j] += val; break; // Q_AC ml("span", {class: "fs-8 mx-1"}, unit)
} ])),
} ml("div", {class: "row"},
main.appendChild(ch0); ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des)
)
)
]);
}
function totals() {
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) {
total[0] += obj.ch[0][2]; // P_AC
total[1] += obj.ch[0][7]/1000; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal
total[3] += obj.ch[0][8]; // P_DC
total[4] += obj.ch[0][10]; // Q_AC
return ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("div", {class: "p-2 iv-h"},
ml("div", {class: "row"}, [
ml("div", {class: "col mx-2 mx-md-1"}, obj.name),
ml("div", {class: "col a-c"}, "Power limit " + obj.power_limit_read + " %"),
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5]) + " C")
])
),
ml("div", {class: "p-2 iv-bg"}, [
ml("div", {class: "row"},[
numBig(obj.ch[0][2], "W", "AC Power"),
numBig(obj.ch[0][7]/1000, "kWh", "Yield Day"),
numBig(obj.ch[0][6], "kWh", "Yield Total")
]),
ml("div", {class: "hr"}),
ml("div", {class: "row mt-2"},[
numMid(obj.ch[0][8], "W", "DC Power"),
numMid(obj.ch[0][0], "V", "Voltage"),
numMid(obj.ch[0][1], "A", "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")
])
])
])
);
}
for(var i = 1; i < (iv["channels"] + 1); i++) { function numCh(val, unit, des) {
var ch = div(["ch"]); return ml("div", {class: "col-12 col-sm-6 col-md-12 mb-2"}, [
ch.appendChild(span(("" == iv["ch_names"][i]) ? ("CHANNEL " + i) : iv["ch_names"][i], ["head"])); ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", {class: "fs-6 fs-md-7"}, String(val)),
ml("span", {class: "fs-8 mx-2"}, unit)
])),
ml("div", {class: "row"},
ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des)
)
)
]);
}
for(var j = 0; j < root.fld_names.length; j++) { function ch(name, vals) {
var val = Math.round(iv["ch"][i][j] * 100) / 100; return ml("div", {class: "col-6 col-md-3 mt-2"}, [
ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"])); ml("div", {class: "ch-h p-2 a-c"}, name),
ch.appendChild(span(root["fld_names"][j], ["info"])); ml("div", {class: "p-2 ch-bg"}, [
} ml("div", {class: "row"}, [
main.appendChild(ch); numCh(vals[2], units[2], "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], "Voltage"),
numCh(vals[1], units[1], "Current")
])
])
]);
}
var ts = div(["ts"]); function tsInfo(ts) {
var ageInfo = "Last received data requested at: "; var ageInfo = "Last received data requested at: ";
if(iv["ts_last_success"] > 0) { if(ts > 0) {
var date = new Date(iv["ts_last_success"] * 1000); var date = new Date(ts * 1000);
ageInfo += date.toLocaleString('de-DE'); ageInfo += date.toLocaleString('de-DE');
} }
else else
ageInfo += "nothing received"; ageInfo += "nothing received";
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: "col mx-2"}, ageInfo)
)
]);
}
ts.innerHTML = ageInfo; function parseIv(obj) {
mNum++;
main.appendChild(ts); var chn = [];
ivHtml.push(main); for(var i = 1; i < obj.ch.length; i++) {
} var name = obj.ch_name[i];
if(name.length == 0)
name = "CHANNEL " + i;
chn.push(ch(name, obj.ch[i]));
} }
mIvHtml.push(
// total ml("div", {}, [
if(obj.length > 1) { ivHead(obj),
for(var j = 0; j < root.ch0_fld_names.length; j++) { ml("div", {class: "row mb-2"}, chn),
var val = Math.round(total[j] * 100) / 100; tsInfo(obj.ts_last_succcess)
if((j == 2) || (j == 6) || (j == 7) || (j == 8) || (j == 10)) { ])
var sub = div(["subgrp"]); );
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"])); var last = true;
tDiv.appendChild(sub); for(var i = obj.id + 1; i < ivEn.length; i++) {
} if((i != ivEn.length) && ivEn[i]) {
last = false;
getAjax("http://10.20.3.44/api/inverter/id/" + i, parseIv);
break;
} }
} }
if(last) {
document.getElementById("live").replaceChildren(...ivHtml); if(mNum > 1)
mIvHtml.unshift(totals());
document.getElementById("live").replaceChildren(...mIvHtml);
}
} }
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
parseIv(obj["inverter"], obj); units = Object.assign({}, obj["fld_units"]);
document.getElementById("refresh").innerHTML = obj["refresh_interval"]; ivEn = Object.values(Object.assign({}, obj["iv"]));
mIvHtml = [];
mNum = 0;
for(var i = 0; i < obj.iv.length; i++) {
if(obj.iv[i])
getAjax("/api/inverter/id/" + i, parseIv);
break;
}
document.getElementById("refresh").innerHTML = obj["refresh"];
if(true == exeOnce) { if(true == exeOnce) {
window.setInterval("getAjax('/api/live', parse)", obj["refresh_interval"] * 1000); window.setInterval("getAjax('/api/live', parse)", obj["refresh"] * 1000);
exeOnce = false; exeOnce = false;
} }
} }

86
src/web/web.h

@ -23,6 +23,8 @@
#include "html/h/index_html.h" #include "html/h/index_html.h"
#include "html/h/login_html.h" #include "html/h/login_html.h"
#include "html/h/style_css.h" #include "html/h/style_css.h"
#include "html/h/colorDark_css.h"
#include "html/h/colorBright_css.h"
#include "html/h/api_js.h" #include "html/h/api_js.h"
#include "html/h/favicon_ico.h" #include "html/h/favicon_ico.h"
#include "html/h/setup_html.h" #include "html/h/setup_html.h"
@ -57,6 +59,7 @@ class Web {
mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1));
mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1));
mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1));
mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1));
mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1));
mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1));
mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1));
@ -219,17 +222,30 @@ class Web {
} }
private: private:
void checkRedirect(AsyncWebServerRequest *request) {
if((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX)
request->redirect(F("/index"));
else if((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE)
request->redirect(F("/live"));
else if((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL)
request->redirect(F("/serial"));
else if((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM)
request->redirect(F("/system"));
else
request->redirect(F("/login"));
}
void onUpdate(AsyncWebServerRequest *request) { void onUpdate(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onUpdate")); DPRINTLN(DBG_VERBOSE, F("onUpdate"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -244,11 +260,10 @@ class Web {
html += "failed"; html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
//if(reboot) mApp->setRebootFlag();
mApp->setRebootFlag();
} }
void onUpload(AsyncWebServerRequest *request) { void onUpload(AsyncWebServerRequest *request) {
@ -261,11 +276,10 @@ class Web {
html += "failed"; html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
//if(reboot) mApp->setRebootFlag();
mApp->setRebootFlag();
} }
void onConnect(AsyncEventSourceClient *client) { void onConnect(AsyncEventSourceClient *client) {
@ -284,12 +298,12 @@ class Web {
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -304,7 +318,7 @@ class Web {
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), login_html, login_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -313,13 +327,24 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("onLogout")); DPRINTLN(DBG_VERBOSE, F("onLogout"));
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
mProtected = true; mProtected = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onColor(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onColor"));
AsyncWebServerResponse *response;
if(mConfig->sys.darkMode)
response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len);
else
response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -349,21 +374,21 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) { void showNotFound(AsyncWebServerRequest *request) {
if(mProtected) if(mProtected)
request->redirect("/login"); checkRedirect(request);
else else
request->redirect("/setup"); request->redirect("/setup");
} }
void onReboot(AsyncWebServerRequest *request) { void onReboot(AsyncWebServerRequest *request) {
mApp->setRebootFlag(); mApp->setRebootFlag();
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
void showErase(AsyncWebServerRequest *request) { void showErase(AsyncWebServerRequest *request) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -374,7 +399,7 @@ class Web {
void showFactoryRst(AsyncWebServerRequest *request) { void showFactoryRst(AsyncWebServerRequest *request) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -399,7 +424,7 @@ class Web {
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>"); "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120; refresh = 120;
} }
request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if(refresh == 10) { if(refresh == 10) {
delay(1000); delay(1000);
ESP.restart(); ESP.restart();
@ -411,12 +436,12 @@ class Web {
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -425,7 +450,7 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("showSave")); DPRINTLN(DBG_VERBOSE, F("showSave"));
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -441,6 +466,7 @@ class Web {
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
if(request->arg("device") != "") if(request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
// protection // protection
if(request->arg("adminpwd") != "{PWD}") { if(request->arg("adminpwd") != "{PWD}") {
@ -580,7 +606,7 @@ class Web {
if(request->arg("reboot") == "on") if(request->arg("reboot") == "on")
onReboot(request); onReboot(request);
else { else {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -591,13 +617,15 @@ class Web {
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
response->addHeader(F("content-type"), "text/html; charset=UTF-8");
request->send(response); request->send(response);
} }
@ -668,7 +696,7 @@ class Web {
void onDebug(AsyncWebServerRequest *request) { void onDebug(AsyncWebServerRequest *request) {
mApp->getSchedulerNames(); mApp->getSchedulerNames();
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), "ok"); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
request->send(response); request->send(response);
} }
@ -677,12 +705,12 @@ class Web {
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -692,12 +720,12 @@ class Web {
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) {
if(mProtected) { if(mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }

Loading…
Cancel
Save