1
0
Fork 0
mirror of https://github.com/lumapu/ahoy.git synced 2025-07-15 15:35:14 +02:00

fix no reconnect at beginning of day

added immediate (each minute) report of inverter status MQTT 
added protection mask to select which pages should be protected
This commit is contained in:
lumapu 2022-12-26 23:32:22 +01:00
parent 6bb8a4e448
commit c3fc01b956
12 changed files with 222 additions and 96 deletions

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,27 +370,22 @@ class PubMqtt {
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId];
}
void sendIvData(void) {
if(mSendList.empty())
return;
char topic[7 + MQTT_TOPIC_LEN], val[40];
float total[4];
bool sendTotal = false;
bool totalIncomplete = false;
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;
while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
record_t<> *rec = iv->getRecordStruct(mSendList.front());
if(mSendList.front() == RealTimeRunData_Debug) {
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(first)
mIvAvail = false;
first = false;
@ -398,7 +395,7 @@ class PubMqtt {
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;
totalComplete = false;
allAvail = false;
}
}
@ -418,6 +415,9 @@ class PubMqtt {
);
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);
@ -426,6 +426,32 @@ class PubMqtt {
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;
char topic[7 + MQTT_TOPIC_LEN], val[40];
float total[4];
bool sendTotal = false;
while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
record_t<> *rec = iv->getRecordStruct(mSendList.front());
// data
if(iv->isAvailable(*mUtcTimestamp, rec)) {
@ -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];

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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) {
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,10 +228,12 @@ class Web {
void onIndex(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onIndex"));
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);
response->addHeader(F("Content-Encoding"), "gzip");
@ -346,10 +355,12 @@ class Web {
void onSetup(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSetup"));
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);
response->addHeader(F("Content-Encoding"), "gzip");
@ -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,10 +518,12 @@ class Web {
void onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLive"));
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);
response->addHeader(F("Content-Encoding"), "gzip");
@ -579,10 +598,12 @@ class Web {
void onSerial(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSerial"));
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);
response->addHeader(F("Content-Encoding"), "gzip");
@ -592,10 +613,12 @@ class Web {
void onSystem(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSystem"));
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);
response->addHeader(F("Content-Encoding"), "gzip");