Browse Source

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
pull/558/head
lumapu 2 years ago
parent
commit
c3fc01b956
  1. 5
      src/CHANGES.md
  2. 4
      src/app.h
  3. 2
      src/appInterface.h
  4. 29
      src/config/settings.h
  5. 2
      src/defines.h
  6. 116
      src/publisher/pubMqtt.h
  7. 3
      src/utils/helper.h
  8. 4
      src/utils/llist.h
  9. 59
      src/web/RestApi.h
  10. 3
      src/web/html/api.js
  11. 26
      src/web/html/setup.html
  12. 65
      src/web/web.h

5
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

4
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;
}

2
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__*/

29
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<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*>());
mCfg.sys.protectionMask = obj[F("prot_mask")];
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.dns1, obj[F("dns1")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>());
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;
}
}

2
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 {

116
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];

3
src/utils/helper.h

@ -13,6 +13,9 @@
#include <stdlib.h>
#include <TimeLib.h>
#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);

4
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;

59
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";
}
}

3
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;
}

26
src/web/html/setup.html

@ -37,9 +37,6 @@
<legend class="des">Device Host Name</legend>
<label for="device">Device Name</label>
<input type="text" name="device" class="text"/>
<label for="adminpwd">Admin Password</label>
<input type="password" name="adminpwd" class="text" value="{PWD}"/>
<input type="hidden" name="disclaimer" value="false" id="disclaimer">
</fieldset>
<button type="button" class="s_collapsible">Network</button>
@ -77,6 +74,18 @@
</fieldset>
</div>
<button type="button" class="s_collapsible">Protection</button>
<div class="s_content">
<fieldset>
<legend class="des">Protection</legend>
<label for="adminpwd">Admin Password</label>
<input type="password" name="adminpwd" class="text" value="{PWD}"/>
<input type="hidden" name="disclaimer" value="false" id="disclaimer">
<p>Select pages which should be protected by password</p>
<div id="prot_mask"></div>
</fieldset>
</div>
<button type="button" class="s_collapsible">Inverter</button>
<div class="s_content">
<fieldset>
@ -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) {

65
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);

Loading…
Cancel
Save