From c3fc01b9562826d68a2977733a8ff8a34195c72c Mon Sep 17 00:00:00 2001 From: lumapu Date: Mon, 26 Dec 2022 23:32:22 +0100 Subject: [PATCH] fix #521 no reconnect at beginning of day added immediate (each minute) report of inverter status MQTT #522 added protection mask to select which pages should be protected --- src/CHANGES.md | 5 ++ src/app.h | 4 ++ src/appInterface.h | 2 + src/config/settings.h | 29 ++++++++++ src/defines.h | 2 +- src/publisher/pubMqtt.h | 116 ++++++++++++++++++++++++---------------- src/utils/helper.h | 3 ++ src/utils/llist.h | 4 +- src/web/RestApi.h | 59 ++++++++++++-------- src/web/html/api.js | 3 +- src/web/html/setup.html | 26 +++++++-- src/web/web.h | 65 ++++++++++++++-------- 12 files changed, 222 insertions(+), 96 deletions(-) diff --git a/src/CHANGES.md b/src/CHANGES.md index 65d938b9..2d3bbb53 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,10 @@ # Changelog +## 0.5.61 +* fix #521 no reconnect at beginning of day +* added immediate (each minute) report of inverter status MQTT #522 +* added protection mask to select which pages should be protected + ## 0.5.60 * added regex to inverter name and MQTT topic (setup.html) * beautified serial.html diff --git a/src/app.h b/src/app.h index 584f20f8..09542e32 100644 --- a/src/app.h +++ b/src/app.h @@ -134,6 +134,10 @@ class app : public IApp, public ah::Scheduler { return mMqtt.getRxCnt(); } + bool getProtection() { + return mWeb.getProtection(); + } + uint8_t getIrqPin(void) { return mConfig->nrf.pinIrq; } diff --git a/src/appInterface.h b/src/appInterface.h index 84090e85..1a71ad61 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -37,6 +37,8 @@ class IApp { virtual bool getMqttIsConnected() = 0; virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0; + + virtual bool getProtection() = 0; }; #endif /*__IAPP_H__*/ diff --git a/src/config/settings.h b/src/config/settings.h index 5eb317ad..47c59a8c 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -18,6 +18,26 @@ * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ + +#define PROT_MASK_INDEX 0x0001 +#define PROT_MASK_LIVE 0x0002 +#define PROT_MASK_SERIAL 0x0004 +#define PROT_MASK_SETUP 0x0008 +#define PROT_MASK_UPDATE 0x0010 +#define PROT_MASK_SYSTEM 0x0020 +#define PROT_MASK_API 0x0040 +#define PROT_MASK_MQTT 0x0080 + +#define DEF_PROT_INDEX 0x0001 +#define DEF_PROT_LIVE 0x0000 +#define DEF_PROT_SERIAL 0x0004 +#define DEF_PROT_SETUP 0x0008 +#define DEF_PROT_UPDATE 0x0010 +#define DEF_PROT_SYSTEM 0x0020 +#define DEF_PROT_API 0x0000 +#define DEF_PROT_MQTT 0x0000 + + typedef struct { uint8_t ip[4]; // ip address uint8_t mask[4]; // sub mask @@ -29,6 +49,7 @@ typedef struct { typedef struct { char deviceName[DEVNAME_LEN]; char adminPwd[PWD_LEN]; + uint16_t protectionMask; // wifi char stationSsid[SSID_LEN]; @@ -240,6 +261,8 @@ class settings { } // erase all settings and reset to default memset(&mCfg, 0, sizeof(settings_t)); + 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; // restore temp settings if(keepWifi) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); @@ -288,6 +311,7 @@ class settings { obj[F("pwd")] = mCfg.sys.stationPwd; obj[F("dev")] = mCfg.sys.deviceName; obj[F("adm")] = mCfg.sys.adminPwd; + obj[F("prot_mask")] = mCfg.sys.protectionMask; 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.dns1, buf); obj[F("dns1")] = String(buf); @@ -298,11 +322,16 @@ class settings { snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as()); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); + mCfg.sys.protectionMask = obj[F("prot_mask")]; ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); + + if(mCfg.sys.protectionMask == 0) + 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; } } diff --git a/src/defines.h b/src/defines.h index 06c9b8ec..35dc142b 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 60 +#define VERSION_PATCH 61 //------------------------------------- typedef struct { diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 8ae2f5a7..ebe1a822 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -36,6 +36,7 @@ class PubMqtt { mSubscriptionCb = NULL; mIsDay = false; mIvAvail = true; + memset(mLastIvState, 0xff, MAX_NUM_INVERTERS); } ~PubMqtt() { } @@ -77,6 +78,7 @@ class PubMqtt { } void tickerMinute() { + processIvStatus(); char val[12]; snprintf(val, 12, "%ld", millis() / 1000); publish("uptime", val); @@ -368,6 +370,72 @@ class PubMqtt { return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; } + bool processIvStatus() { + // returns true if all inverters are available + bool allAvail = true; + bool first = true; + bool changed = false; + char topic[7 + MQTT_TOPIC_LEN], val[40]; + Inverter<> *iv; + record_t<> *rec; + bool totalComplete = true; + + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if(first) + mIvAvail = false; + first = false; + + // inverter status + uint8_t status = MQTT_STATUS_AVAIL_PROD; + if ((!iv->isAvailable(*mUtcTimestamp, rec)) || (!iv->config->enabled)) { + status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; + if(iv->config->enabled) { // only change all-avail if inverter is enabled! + totalComplete = false; + allAvail = false; + } + } + else if (!iv->isProducing(*mUtcTimestamp, rec)) { + mIvAvail = true; + if (MQTT_STATUS_AVAIL_PROD == status) + status = MQTT_STATUS_AVAIL_NOT_PROD; + } + else + mIvAvail = true; + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name); + snprintf(val, 40, "%s%s%s%s", + (status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "", + "available and ", + (status == MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "", + "producing" + ); + publish(topic, val, true); + + if(mLastIvState[id] != status) { + mLastIvState[id] = status; + + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); + snprintf(val, 40, "%d", status); + publish(topic, val, true); + + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); + snprintf(val, 40, "%d", iv->getLastTs(rec)); + publish(topic, val, true); + } + } + + if(changed) { + snprintf(val, 32, "%s", ((allAvail) ? "online" : ((mIvAvail) ? "partial" : "offline"))); + publish("status", val, true); + } + + return totalComplete; + } + void sendIvData(void) { if(mSendList.empty()) return; @@ -375,9 +443,6 @@ class PubMqtt { char topic[7 + MQTT_TOPIC_LEN], val[40]; float total[4]; bool sendTotal = false; - bool totalIncomplete = false; - bool allAvail = true; - bool first = true; while(!mSendList.empty()) { memset(total, 0, sizeof(float) * 4); @@ -388,45 +453,6 @@ class PubMqtt { record_t<> *rec = iv->getRecordStruct(mSendList.front()); - if(mSendList.front() == RealTimeRunData_Debug) { - if(first) - mIvAvail = false; - first = false; - - // inverter status - uint8_t status = MQTT_STATUS_AVAIL_PROD; - if ((!iv->isAvailable(*mUtcTimestamp, rec)) || (!iv->config->enabled)) { - status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; - if(iv->config->enabled) { // only change all-avail if inverter is enabled! - totalIncomplete = true; - allAvail = false; - } - } - else if (!iv->isProducing(*mUtcTimestamp, rec)) { - mIvAvail = true; - if (MQTT_STATUS_AVAIL_PROD == status) - status = MQTT_STATUS_AVAIL_NOT_PROD; - } - else - mIvAvail = true; - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name); - snprintf(val, 40, "%s%s%s%s", - (status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "", - "available and ", - (status == MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "", - "producing" - ); - publish(topic, val, true); - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); - snprintf(val, 40, "%d", status); - publish(topic, val, true); - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); - snprintf(val, 40, "%d", iv->getLastTs(rec)); - publish(topic, val, true); - } - // data if(iv->isAvailable(*mUtcTimestamp, rec)) { for (uint8_t i = 0; i < rec->length; i++) { @@ -471,10 +497,7 @@ class PubMqtt { mSendList.pop(); // remove from list once all inverters were processed - snprintf(val, 32, "%s", ((allAvail) ? "online" : ((mIvAvail) ? "partial" : "offline"))); - publish("status", val, true); - - if ((true == sendTotal) && (false == totalIncomplete)) { + if ((true == sendTotal) && processIvStatus()) { uint8_t fieldId; for (uint8_t i = 0; i < 4; i++) { switch (i) { @@ -514,6 +537,7 @@ class PubMqtt { subscriptionCb mSubscriptionCb; bool mIsDay; bool mIvAvail; // shows if at least one inverter is available + uint8_t mLastIvState[MAX_NUM_INVERTERS]; // last will topic and payload must be available trough lifetime of 'espMqttClient' char mLwtTopic[MQTT_TOPIC_LEN+5]; diff --git a/src/utils/helper.h b/src/utils/helper.h index 460e4691..7e908624 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -13,6 +13,9 @@ #include #include + +#define CHECK_MASK(a,b) ((a & b) == b) + namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Char(uint8_t ip[], char *str); diff --git a/src/utils/llist.h b/src/utils/llist.h index 44011846..69750f19 100644 --- a/src/utils/llist.h +++ b/src/utils/llist.h @@ -70,8 +70,10 @@ class llist { elmType *t = p->nxt; p->nxt->pre = p->pre; p->pre->nxt = p->nxt; - if(root == p) + if((root == p) && (p->nxt == p)) root = NULL; + else + root = p->nxt; p->nxt = NULL; p->pre = NULL; p = NULL; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 0b77fdda..19cb2603 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -165,6 +165,7 @@ class RestApi { obj[F("mac")] = WiFi.macAddress(); obj[F("hostname")] = WiFi.getHostname(); obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); + obj[F("prot_mask")] = mConfig->sys.protectionMask; obj[F("sdk")] = ESP.getSdkVersion(); obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); @@ -324,29 +325,41 @@ class RestApi { } void getMenu(JsonObject obj) { - obj["name"][0] = "Live"; - obj["link"][0] = "/live"; - obj["name"][1] = "Serial / Control"; - obj["link"][1] = "/serial"; - obj["name"][2] = "Settings"; - obj["link"][2] = "/setup"; - obj["name"][3] = "-"; - obj["name"][4] = "REST API"; - obj["link"][4] = "/api"; - obj["trgt"][4] = "_blank"; - obj["name"][5] = "-"; - obj["name"][6] = "Update"; - obj["link"][6] = "/update"; - obj["name"][7] = "System"; - obj["link"][7] = "/system"; - obj["name"][8] = "-"; - obj["name"][9] = "Documentation"; - obj["link"][9] = "https://ahoydtu.de"; - obj["trgt"][9] = "_blank"; - if(strlen(mConfig->sys.adminPwd) > 0) { - obj["name"][10] = "-"; - obj["name"][11] = "Logout"; - obj["link"][11] = "/logout"; + uint8_t i = 0; + uint16_t mask = (mApp->getProtection()) ? mConfig->sys.protectionMask : 0; + if(!CHECK_MASK(mask, PROT_MASK_LIVE)) { + obj[F("name")][i] = "Live"; + obj[F("link")][i++] = "/live"; + } + if(!CHECK_MASK(mask, PROT_MASK_SERIAL)) { + obj[F("name")][i] = "Serial / Control"; + obj[F("link")][i++] = "/serial"; + } + if(!CHECK_MASK(mask, PROT_MASK_SETUP)) { + obj[F("name")][i] = "Settings"; + obj[F("link")][i++] = "/setup"; + } + obj[F("name")][i++] = "-"; + obj[F("name")][i] = "REST API"; + obj[F("link")][i] = "/api"; + obj[F("trgt")][i++] = "_blank"; + obj[F("name")][i++] = "-"; + if(!CHECK_MASK(mask, PROT_MASK_UPDATE)) { + obj[F("name")][i] = "Update"; + obj[F("link")][i++] = "/update"; + } + if(!CHECK_MASK(mask, PROT_MASK_SYSTEM)) { + obj[F("name")][i] = "System"; + obj[F("link")][i++] = "/system"; + } + obj[F("name")][i++] = "-"; + obj[F("name")][i] = "Documentation"; + obj[F("link")][i] = "https://ahoydtu.de"; + obj[F("trgt")][i++] = "_blank"; + if((strlen(mConfig->sys.adminPwd) > 0) && !mApp->getProtection()) { + obj[F("name")][i++] = "-"; + obj[F("name")][i] = "Logout"; + obj[F("link")][i++] = "/logout"; } } diff --git a/src/web/html/api.js b/src/web/html/api.js index cfbddce5..2e1ffb2b 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -129,7 +129,7 @@ function lbl(htmlfor, val, cl=null, id=null) { return e; } -function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, title=null) { +function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, title=null, checked=null) { e = document.createElement('input'); e.classList.add(...cl); e.name = name; @@ -139,6 +139,7 @@ function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, t if(null != type) e.type = type; if(null != pattern) e.pattern = pattern; if(null != title) e.title = title; + if(null != checked) e.checked = checked; return e; } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index a1f425f3..79184089 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -37,9 +37,6 @@ Device Host Name - - - @@ -77,6 +74,18 @@ + +
+
+ Protection + + + +

Select pages which should be protected by password

+
+
+
+
@@ -347,6 +356,17 @@ var e = document.getElementsByName("adminpwd")[0]; if(!obj["pwd_set"]) e.value = ""; + var d = document.getElementById("prot_mask"); + var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"] + for(var i = 0; i < 6; i++) { + var chkd = ((obj["prot_mask"] & (1 << i)) == (1 << i)); + var sp = lbl("protMask" + i, a[i]); + 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()); + } } function parseGeneric(obj) { diff --git a/src/web/web.h b/src/web/web.h index 487ffbfb..d8922747 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -121,6 +121,11 @@ class Web { void setProtection(bool protect) { mProtected = protect; } + + bool getProtection() { + return mProtected; + } + void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if(!index) { Serial.printf("Update Start: %s\n", filename.c_str()); @@ -180,10 +185,12 @@ class Web { void onUpdate(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onUpdate")); - /*if(mProtected) { - request->redirect("/login"); - return; - }*/ + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { + if(mProtected) { + request->redirect("/login"); + return; + } + } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len); response->addHeader(F("Content-Encoding"), "gzip"); @@ -221,9 +228,11 @@ class Web { void onIndex(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onIndex")); - if(mProtected) { - request->redirect("/login"); - return; + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { + if(mProtected) { + request->redirect("/login"); + return; + } } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len); @@ -236,7 +245,7 @@ class Web { if(request->args() > 0) { if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { - mProtected = false; + mProtected = false; request->redirect("/"); } } @@ -346,9 +355,11 @@ class Web { void onSetup(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onSetup")); - if(mProtected) { - request->redirect("/login"); - return; + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { + if(mProtected) { + request->redirect("/login"); + return; + } } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len); @@ -376,11 +387,17 @@ class Web { request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); if(request->arg("device") != "") request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); + + // protection if(request->arg("adminpwd") != "{PWD}") { request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); mProtected = (strlen(mConfig->sys.adminPwd) > 0); } - + mConfig->sys.protectionMask = 0x0000; + for(uint8_t i = 0; i < 6; i++) { + if(request->arg("protMask" + String(i)) == "on") + mConfig->sys.protectionMask |= (1 << i); + } // static ip request->arg("ipAddr").toCharArray(buf, 20); @@ -501,9 +518,11 @@ class Web { void onLive(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onLive")); - if(mProtected) { - request->redirect("/login"); - return; + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { + if(mProtected) { + request->redirect("/login"); + return; + } } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len); @@ -579,9 +598,11 @@ class Web { void onSerial(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onSerial")); - if(mProtected) { - request->redirect("/login"); - return; + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { + if(mProtected) { + request->redirect("/login"); + return; + } } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len); @@ -592,9 +613,11 @@ class Web { void onSystem(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onSystem")); - if(mProtected) { - request->redirect("/login"); - return; + if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { + if(mProtected) { + request->redirect("/login"); + return; + } } AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);