mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-22 05:16:11 +02:00
corrected MQTT comm_disabled
#529
fix Prometheus and JSON endpoints (`config_override.h`) #561 publish MQTT with fixed interval even if inverter is not available #542 added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551 MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman improved potential issue with `checkTicker`, thx @cbscpe MQTT option for reset values on midnight / not avail / communication stop #539 small fix in `tickIVCommunication` #534 add `YieldTotal` correction, eg. to have the option to zero at year start #512
This commit is contained in:
parent
4265856814
commit
d8e255ddc2
16 changed files with 346 additions and 218 deletions
|
@ -4,6 +4,14 @@
|
|||
|
||||
## 0.5.70
|
||||
* corrected MQTT `comm_disabled` #529
|
||||
* fix Prometheus and JSON endpoints (`config_override.h`) #561
|
||||
* publish MQTT with fixed interval even if inverter is not available #542
|
||||
* added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551
|
||||
* MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman
|
||||
* improved potential issue with `checkTicker`, thx @cbscpe
|
||||
* MQTT option for reset values on midnight / not avail / communication stop #539
|
||||
* small fix in `tickIVCommunication` #534
|
||||
* add `YieldTotal` correction, eg. to have the option to zero at year start #512
|
||||
|
||||
## 0.5.69
|
||||
* merged SH1106 1.3" Display, thx @dAjaY85
|
||||
|
|
14
src/app.cpp
14
src/app.cpp
|
@ -62,6 +62,9 @@ void app::setup() {
|
|||
if (mConfig->mqtt.broker[0] > 0) {
|
||||
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt));
|
||||
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt));
|
||||
uint32_t nxtTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight
|
||||
if(mConfig->mqtt.rstYieldMidNight)
|
||||
onceAt(std::bind(&app::tickMidnight, this), nxtTrig);
|
||||
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
|
||||
}
|
||||
#endif
|
||||
|
@ -161,7 +164,7 @@ void app::tickIVCommunication(void) {
|
|||
if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start
|
||||
nxtTrig = mSunrise - mConfig->sun.offsetSec;
|
||||
} else {
|
||||
if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
|
||||
if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
|
||||
nxtTrig = 0;
|
||||
} else { // current time lies within communication start/stop time, set next trigger to communication stop
|
||||
mIVCommunicationOn = true;
|
||||
|
@ -207,6 +210,15 @@ void app::tickSend(void) {
|
|||
updateLed();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void app::tickMidnight(void) {
|
||||
// only used and enabled by MQTT (see setup())
|
||||
uint32_t nxtTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight
|
||||
onceAt(std::bind(&app::tickMidnight, this), nxtTrig);
|
||||
|
||||
mMqtt.tickerMidnight();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void app::handleIntr(void) {
|
||||
DPRINTLN(DBG_VERBOSE, F("app::handleIntr"));
|
||||
|
|
|
@ -78,6 +78,10 @@ class app : public IApp, public ah::Scheduler {
|
|||
return mSettings.saveSettings();
|
||||
}
|
||||
|
||||
bool readSettings(const char *path) {
|
||||
return mSettings.readSettings(path);
|
||||
}
|
||||
|
||||
bool eraseSettings(bool eraseWifi = false) {
|
||||
return mSettings.eraseSettings(eraseWifi);
|
||||
}
|
||||
|
@ -95,7 +99,7 @@ class app : public IApp, public ah::Scheduler {
|
|||
}
|
||||
|
||||
void setRebootFlag() {
|
||||
once(std::bind(&app::tickReboot, this), 1);
|
||||
once(std::bind(&app::tickReboot, this), 3);
|
||||
}
|
||||
|
||||
const char *getVersion() {
|
||||
|
@ -203,6 +207,7 @@ class app : public IApp, public ah::Scheduler {
|
|||
void tickCalcSunrise(void);
|
||||
void tickIVCommunication(void);
|
||||
void tickSend(void);
|
||||
void tickMidnight(void);
|
||||
/*void tickSerial(void) {
|
||||
if(Serial.available() == 0)
|
||||
return;
|
||||
|
|
|
@ -15,6 +15,7 @@ class IApp {
|
|||
public:
|
||||
virtual ~IApp() {}
|
||||
virtual bool saveSettings() = 0;
|
||||
virtual bool readSettings(const char *path) = 0;
|
||||
virtual bool eraseSettings(bool eraseWifi) = 0;
|
||||
virtual void setRebootFlag() = 0;
|
||||
virtual const char *getVersion() = 0;
|
||||
|
|
|
@ -97,6 +97,9 @@ typedef struct {
|
|||
char pwd[MQTT_PWD_LEN];
|
||||
char topic[MQTT_TOPIC_LEN];
|
||||
uint16_t interval;
|
||||
bool rstYieldMidNight;
|
||||
bool rstValsNotAvail;
|
||||
bool rstValsCommStop;
|
||||
} cfgMqtt_t;
|
||||
|
||||
typedef struct {
|
||||
|
@ -105,6 +108,7 @@ typedef struct {
|
|||
serial_u serial;
|
||||
uint16_t chMaxPwr[4];
|
||||
char chName[4][MAX_NAME_LENGTH];
|
||||
uint32_t yieldCor; // YieldTotal correction value
|
||||
} cfgIv_t;
|
||||
|
||||
typedef struct {
|
||||
|
@ -155,7 +159,7 @@ class settings {
|
|||
else
|
||||
DPRINTLN(DBG_INFO, F(" .. done"));
|
||||
|
||||
readSettings();
|
||||
readSettings("/settings.json");
|
||||
}
|
||||
|
||||
// should be used before OTA
|
||||
|
@ -186,9 +190,10 @@ class settings {
|
|||
#endif
|
||||
}
|
||||
|
||||
void readSettings(void) {
|
||||
bool readSettings(const char* path) {
|
||||
bool success = false;
|
||||
loadDefaults();
|
||||
File fp = LittleFS.open("/settings.json", "r");
|
||||
File fp = LittleFS.open(path, "r");
|
||||
if(!fp)
|
||||
DPRINTLN(DBG_WARN, F("failed to load json, using default config"));
|
||||
else {
|
||||
|
@ -206,6 +211,7 @@ class settings {
|
|||
jsonMqtt(root["mqtt"]);
|
||||
jsonLed(root["led"]);
|
||||
jsonInst(root["inst"]);
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
Serial.println(F("failed to parse json, using default config"));
|
||||
|
@ -213,6 +219,7 @@ class settings {
|
|||
|
||||
fp.close();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool saveSettings(void) {
|
||||
|
@ -299,6 +306,9 @@ class settings {
|
|||
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD);
|
||||
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
|
||||
mCfg.mqtt.interval = 0; // off
|
||||
mCfg.mqtt.rstYieldMidNight = false;
|
||||
mCfg.mqtt.rstValsNotAvail = false;
|
||||
mCfg.mqtt.rstValsCommStop = false;
|
||||
|
||||
mCfg.led.led0 = DEF_LED0_PIN;
|
||||
mCfg.led.led1 = DEF_LED1_PIN;
|
||||
|
@ -399,9 +409,16 @@ class settings {
|
|||
obj[F("pwd")] = mCfg.mqtt.pwd;
|
||||
obj[F("topic")] = mCfg.mqtt.topic;
|
||||
obj[F("intvl")] = mCfg.mqtt.interval;
|
||||
obj[F("rstMidNight")] = (bool)mCfg.mqtt.rstYieldMidNight;
|
||||
obj[F("rstNotAvail")] = (bool)mCfg.mqtt.rstValsNotAvail;
|
||||
obj[F("rstComStop")] = (bool)mCfg.mqtt.rstValsCommStop;
|
||||
|
||||
} else {
|
||||
mCfg.mqtt.port = obj[F("port")];
|
||||
mCfg.mqtt.interval = obj[F("intvl")];
|
||||
mCfg.mqtt.rstYieldMidNight = (bool)obj["rstMidNight"];
|
||||
mCfg.mqtt.rstValsNotAvail = (bool)obj["rstNotAvail"];
|
||||
mCfg.mqtt.rstValsCommStop = (bool)obj["rstComStop"];
|
||||
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as<const char*>());
|
||||
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as<const char*>());
|
||||
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as<const char*>());
|
||||
|
@ -441,6 +458,7 @@ class settings {
|
|||
obj[F("en")] = (bool)cfg->enabled;
|
||||
obj[F("name")] = cfg->name;
|
||||
obj[F("sn")] = cfg->serial.u64;
|
||||
obj[F("yield")] = cfg->yieldCor;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
obj[F("pwr")][i] = cfg->chMaxPwr[i];
|
||||
obj[F("chName")][i] = cfg->chName[i];
|
||||
|
@ -449,6 +467,7 @@ class settings {
|
|||
cfg->enabled = (bool)obj[F("en")];
|
||||
snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as<const char*>());
|
||||
cfg->serial.u64 = obj[F("sn")];
|
||||
cfg->yieldCor = obj[F("yield")];
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
cfg->chMaxPwr[i] = obj[F("pwr")][i];
|
||||
snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>());
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
//-------------------------------------
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 5
|
||||
#define VERSION_PATCH 69
|
||||
#define VERSION_PATCH 70
|
||||
|
||||
//-------------------------------------
|
||||
typedef struct {
|
||||
|
|
|
@ -233,11 +233,13 @@ class Inverter {
|
|||
val <<= 8;
|
||||
val |= buf[ptr];
|
||||
} while(++ptr != end);
|
||||
if(FLD_T == rec->assign[pos].fieldId) {
|
||||
if (FLD_T == rec->assign[pos].fieldId) {
|
||||
// temperature is a signed value!
|
||||
rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div);
|
||||
}
|
||||
else {
|
||||
} else if ((FLD_YT == rec->assign[pos].fieldId)
|
||||
&& (config->yieldCor != 0)) {
|
||||
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div) - (REC_TYP)config->yieldCor;
|
||||
} else {
|
||||
if ((REC_TYP)(div) > 1)
|
||||
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
|
||||
else
|
||||
|
@ -286,6 +288,16 @@ class Inverter {
|
|||
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
|
||||
}
|
||||
|
||||
bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) {
|
||||
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue"));
|
||||
if(NULL == rec)
|
||||
return false;
|
||||
if(pos > rec->length)
|
||||
return false;
|
||||
rec->record[pos] = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
REC_TYP getValue(uint8_t pos, record_t<> *rec) {
|
||||
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue"));
|
||||
if(NULL == rec)
|
||||
|
|
|
@ -112,6 +112,27 @@ class PubMqtt {
|
|||
void tickerComm(bool disabled) {
|
||||
publish("comm_disabled", ((disabled) ? "true" : "false"), true);
|
||||
publish("comm_dis_ts", String(*mUtcTimestamp).c_str(), true);
|
||||
|
||||
if(disabled && (mCfgMqtt->rstValsCommStop))
|
||||
zeroAllInverters();
|
||||
}
|
||||
|
||||
void tickerMidnight() {
|
||||
Inverter<> *iv;
|
||||
record_t<> *rec;
|
||||
|
||||
// set YieldDay to zero
|
||||
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);
|
||||
uint8_t pos = iv->getPosByChFld(CH0, FLD_YD, rec);
|
||||
iv->setValue(pos, rec, 0.0f);
|
||||
}
|
||||
|
||||
mSendList.push(RealTimeRunData_Debug);
|
||||
sendIvData();
|
||||
}
|
||||
|
||||
void payloadEventListener(uint8_t cmd) {
|
||||
|
@ -394,18 +415,21 @@ class PubMqtt {
|
|||
allAvail = false;
|
||||
}
|
||||
}
|
||||
else if (!iv->isProducing(*mUtcTimestamp, rec)) {
|
||||
else {
|
||||
mIvAvail = true;
|
||||
if (!iv->isProducing(*mUtcTimestamp, rec)) {
|
||||
if (MQTT_STATUS_AVAIL_PROD == status)
|
||||
status = MQTT_STATUS_AVAIL_NOT_PROD;
|
||||
}
|
||||
else
|
||||
mIvAvail = true;
|
||||
}
|
||||
|
||||
if(mLastIvState[id] != status) {
|
||||
mLastIvState[id] = status;
|
||||
changed = true;
|
||||
|
||||
if(mCfgMqtt->rstValsNotAvail)
|
||||
zeroValues(iv);
|
||||
|
||||
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
|
||||
snprintf(val, 40, "%d", status);
|
||||
publish(topic, val, true);
|
||||
|
@ -419,12 +443,13 @@ class PubMqtt {
|
|||
if(changed) {
|
||||
snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((mIvAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
|
||||
publish("status", val, true);
|
||||
sendIvData(false); // false prevents loop of same function
|
||||
}
|
||||
|
||||
return totalComplete;
|
||||
}
|
||||
|
||||
void sendIvData(void) {
|
||||
void sendIvData(bool sendTotals = true) {
|
||||
if(mSendList.empty())
|
||||
return;
|
||||
|
||||
|
@ -442,7 +467,7 @@ class PubMqtt {
|
|||
record_t<> *rec = iv->getRecordStruct(mSendList.front());
|
||||
|
||||
// data
|
||||
if(iv->isAvailable(*mUtcTimestamp, rec)) {
|
||||
//if(iv->isAvailable(*mUtcTimestamp, rec) || (0 != mCfgMqtt->interval)) { // is avail or fixed pulish interval was set
|
||||
for (uint8_t i = 0; i < rec->length; i++) {
|
||||
bool retained = false;
|
||||
if (mSendList.front() == RealTimeRunData_Debug) {
|
||||
|
@ -480,11 +505,14 @@ class PubMqtt {
|
|||
}
|
||||
yield();
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
mSendList.pop(); // remove from list once all inverters were processed
|
||||
|
||||
if(!sendTotals) // skip total value calculation
|
||||
continue;
|
||||
|
||||
if ((true == sendTotal) && processIvStatus()) {
|
||||
uint8_t fieldId;
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
|
@ -511,6 +539,47 @@ class PubMqtt {
|
|||
}
|
||||
}
|
||||
|
||||
void zeroAllInverters() {
|
||||
Inverter<> *iv;
|
||||
|
||||
// set values to zero, exept yields
|
||||
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
||||
iv = mSys->getInverterByPos(id);
|
||||
if (NULL == iv)
|
||||
continue; // skip to next inverter
|
||||
|
||||
zeroValues(iv);
|
||||
}
|
||||
sendIvData();
|
||||
}
|
||||
|
||||
void zeroValues(Inverter<> *iv) {
|
||||
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
||||
for(uint8_t ch = 0; ch <= iv->channels; ch++) {
|
||||
uint8_t pos = 0;
|
||||
uint8_t fld = 0;
|
||||
while(0xff != pos) {
|
||||
switch(fld) {
|
||||
case FLD_YD:
|
||||
case FLD_YT:
|
||||
case FLD_FW_VERSION:
|
||||
case FLD_FW_BUILD_YEAR:
|
||||
case FLD_FW_BUILD_MONTH_DAY:
|
||||
case FLD_FW_BUILD_HOUR_MINUTE:
|
||||
case FLD_HW_ID:
|
||||
case FLD_ACT_ACTIVE_PWR_LIMIT:
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
pos = iv->getPosByChFld(ch, fld, rec);
|
||||
iv->setValue(pos, rec, 0.0f);
|
||||
fld++;
|
||||
}
|
||||
}
|
||||
|
||||
mSendList.push(RealTimeRunData_Debug);
|
||||
}
|
||||
|
||||
espMqttClient mClient;
|
||||
cfgMqtt_t *mCfgMqtt;
|
||||
#if defined(ESP8266)
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace ah {
|
|||
*ticker = mil + interval;
|
||||
return true;
|
||||
}
|
||||
else if(mil < (*ticker - interval)) {
|
||||
else if((mil + interval) < (*ticker)) {
|
||||
*ticker = mil + interval;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// 2022 Ahoy, https://ahoydtu.de
|
||||
// Lukas Pusch, lukas@lpusch.de
|
||||
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef __LIST_H__
|
||||
#define __LIST_H__
|
||||
|
||||
template<class T, class... Args>
|
||||
struct node_s {
|
||||
typedef T dT;
|
||||
node_s *pre;
|
||||
node_s *nxt;
|
||||
uint8_t id;
|
||||
dT d;
|
||||
node_s() : pre(NULL), nxt(NULL), d() {}
|
||||
node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {}
|
||||
};
|
||||
|
||||
template<int MAX_NUM, class T, class... Args>
|
||||
class llist {
|
||||
typedef node_s<T, Args...> elmType;
|
||||
typedef T dataType;
|
||||
public:
|
||||
llist() : root(mPool) {
|
||||
root = NULL;
|
||||
elmType *p = mPool;
|
||||
for(uint32_t i = 0; i < MAX_NUM; i++) {
|
||||
p->id = i;
|
||||
p++;
|
||||
}
|
||||
mFill = mMax = 0;
|
||||
}
|
||||
|
||||
elmType *add(Args... args) {
|
||||
elmType *p = root, *t;
|
||||
if(NULL == (t = getFreeNode()))
|
||||
return NULL;
|
||||
if(++mFill > mMax)
|
||||
mMax = mFill;
|
||||
|
||||
if(NULL == root) {
|
||||
p = root = t;
|
||||
p->pre = p;
|
||||
p->nxt = p;
|
||||
}
|
||||
else {
|
||||
p = root->pre;
|
||||
t->pre = p;
|
||||
p->nxt->pre = t;
|
||||
t->nxt = p->nxt;
|
||||
p->nxt = t;
|
||||
}
|
||||
t->d = dataType(args...);
|
||||
return p;
|
||||
}
|
||||
|
||||
elmType *getFront() {
|
||||
return root;
|
||||
}
|
||||
|
||||
elmType *get(elmType *p) {
|
||||
p = p->nxt;
|
||||
return (p == root) ? NULL : p;
|
||||
}
|
||||
|
||||
elmType *rem(elmType *p) {
|
||||
if(NULL == p)
|
||||
return NULL;
|
||||
elmType *t = p->nxt;
|
||||
p->nxt->pre = p->pre;
|
||||
p->pre->nxt = p->nxt;
|
||||
if((root == p) && (p->nxt == p))
|
||||
root = NULL;
|
||||
else
|
||||
root = p->nxt;
|
||||
p->nxt = NULL;
|
||||
p->pre = NULL;
|
||||
p = NULL;
|
||||
mFill--;
|
||||
return (NULL == root) ? NULL : ((t == root) ? NULL : t);
|
||||
}
|
||||
|
||||
uint16_t getFill(void) {
|
||||
return mFill;
|
||||
}
|
||||
|
||||
uint16_t getMaxFill(void) {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
protected:
|
||||
elmType *root;
|
||||
|
||||
private:
|
||||
elmType *getFreeNode(void) {
|
||||
elmType *n = mPool;
|
||||
for(uint32_t i = 0; i < MAX_NUM; i++) {
|
||||
if(NULL == n->nxt)
|
||||
return n;
|
||||
n++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
elmType mPool[MAX_NUM];
|
||||
uint16_t mFill, mMax;
|
||||
};
|
||||
|
||||
#endif /*__LIST_H__*/
|
|
@ -129,6 +129,7 @@ namespace ah {
|
|||
mTickerInUse[i] = false;
|
||||
else
|
||||
mTicker[i].timeout = mTicker[i].reload;
|
||||
//DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout));
|
||||
(mTicker[i].c)();
|
||||
yield();
|
||||
}
|
||||
|
|
|
@ -134,17 +134,34 @@ class RestApi {
|
|||
ep[F("record/config")] = url + F("record/config");
|
||||
ep[F("record/live")] = url + F("record/live");
|
||||
}
|
||||
|
||||
void onDwnldSetup(AsyncWebServerRequest *request) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
|
||||
JsonObject root = response->getRoot();
|
||||
AsyncWebServerResponse *response;
|
||||
|
||||
getSetup(root);
|
||||
File fp = LittleFS.open("/settings.json", "r");
|
||||
if(!fp) {
|
||||
DPRINTLN(DBG_ERROR, F("failed to load settings"));
|
||||
response = request->beginResponse(200, F("application/json"), "{}");
|
||||
}
|
||||
else {
|
||||
String tmp = fp.readString();
|
||||
int i = 0;
|
||||
// remove all passwords
|
||||
while (i != -1) {
|
||||
i = tmp.indexOf("\"pwd\":", i);
|
||||
if(-1 != i) {
|
||||
i+=7;
|
||||
tmp.remove(i, tmp.indexOf("\"", i)-i);
|
||||
}
|
||||
}
|
||||
response = request->beginResponse(200, F("application/json"), tmp);
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
response->addHeader("Content-Type", "application/octet-stream");
|
||||
response->addHeader("Content-Description", "File Transfer");
|
||||
response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json");
|
||||
request->send(response);
|
||||
fp.close();
|
||||
}
|
||||
|
||||
void getGeneric(JsonObject obj) {
|
||||
|
@ -165,7 +182,7 @@ class RestApi {
|
|||
obj[F("device_name")] = mConfig->sys.deviceName;
|
||||
|
||||
obj[F("mac")] = WiFi.macAddress();
|
||||
obj[F("hostname")] = WiFi.getHostname();
|
||||
obj[F("hostname")] = mConfig->sys.deviceName;
|
||||
obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0);
|
||||
obj[F("prot_mask")] = mConfig->sys.protectionMask;
|
||||
|
||||
|
@ -263,6 +280,7 @@ class RestApi {
|
|||
obj2[F("serial")] = String(iv->config->serial.u64, HEX);
|
||||
obj2[F("channels")] = iv->channels;
|
||||
obj2[F("version")] = String(iv->getFwVersion());
|
||||
obj2[F("yieldCor")] = iv->config->yieldCor;
|
||||
|
||||
for(uint8_t j = 0; j < iv->channels; j ++) {
|
||||
obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j];
|
||||
|
@ -282,6 +300,9 @@ class RestApi {
|
|||
obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
|
||||
obj[F("topic")] = String(mConfig->mqtt.topic);
|
||||
obj[F("interval")] = String(mConfig->mqtt.interval);
|
||||
obj[F("rstMid")] = (bool)mConfig->mqtt.rstYieldMidNight;
|
||||
obj[F("rstNAvail")] = (bool)mConfig->mqtt.rstValsNotAvail;
|
||||
obj[F("rstComStop")] = (bool)mConfig->mqtt.rstValsCommStop;
|
||||
}
|
||||
|
||||
void getNtp(JsonObject obj) {
|
||||
|
|
|
@ -31,7 +31,13 @@
|
|||
<div id="wrapper">
|
||||
<div id="content">
|
||||
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
|
||||
|
||||
<fieldset>
|
||||
<legend class="des">Upload JSON Settings</legend>
|
||||
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
|
||||
<input type="file" name="upload">
|
||||
<input type="button" class="btn" value="Upload" onclick="hide()">
|
||||
</form>
|
||||
</fieldset>
|
||||
<form method="post" action="/save">
|
||||
<fieldset>
|
||||
<legend class="des">Device Host Name</legend>
|
||||
|
@ -147,7 +153,13 @@
|
|||
<label for="mqttPwd">Password (optional)</label>
|
||||
<input type="password" class="text" name="mqttPwd"/>
|
||||
<label for="mqttTopic">Topic</label>
|
||||
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9.\-_\+\/]+" title="Invalid input" />
|
||||
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" />
|
||||
<label for="mqttRstMid">Reset YieldDay at Midnight</label>
|
||||
<input type="checkbox" class="cb" name="mqttRstMid"/><br/>
|
||||
<label for="mqttRstComStop">Reset Values at Communication stop</label>
|
||||
<input type="checkbox" class="cb" name="mqttRstComStop"/><br/>
|
||||
<label for="mqttRstNotAvail">Reset Values on 'not available'</label>
|
||||
<input type="checkbox" class="cb" name="mqttRstNotAvail"/><br/>
|
||||
<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>
|
||||
<input type="text" class="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" />
|
||||
|
@ -184,7 +196,7 @@
|
|||
</div>
|
||||
<div class="hr mb-3 mt-3"></div>
|
||||
<div class="mb-4">
|
||||
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values)
|
||||
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values, passwords will be removed!)
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -212,8 +224,9 @@
|
|||
const re = /11[2,4,6]1.*/;
|
||||
|
||||
document.getElementById("btnAdd").addEventListener("click", function() {
|
||||
if(highestId <= (maxInv-1))
|
||||
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId + 1);
|
||||
if(highestId <= (maxInv-1)) {
|
||||
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId);
|
||||
}
|
||||
});
|
||||
|
||||
function apiCbWifi(obj) {
|
||||
|
@ -268,6 +281,12 @@
|
|||
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function hide() {
|
||||
document.getElementById("form").submit();
|
||||
var e = document.getElementById("content");
|
||||
e.replaceChildren(span("upload started"));
|
||||
}
|
||||
|
||||
function delIv() {
|
||||
var id = this.id.substring(0,4);
|
||||
var e = document.getElementsByName(id + "Addr")[0];
|
||||
|
@ -278,8 +297,8 @@
|
|||
}
|
||||
|
||||
function ivHtml(obj, id) {
|
||||
highestId = id;
|
||||
if(highestId == (maxInv - 1))
|
||||
highestId = id + 1;
|
||||
if(highestId == maxInv)
|
||||
setHide("btnAdd", true);
|
||||
iv = document.getElementById("inverter");
|
||||
iv.appendChild(des("Inverter " + id));
|
||||
|
@ -292,7 +311,7 @@
|
|||
iv.appendChild(br());
|
||||
|
||||
iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*"));
|
||||
var addr = inp(id + "Addr", obj["serial"], 12);
|
||||
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input");
|
||||
iv.appendChild(addr);
|
||||
['keyup', 'change'].forEach(function(evt) {
|
||||
addr.addEventListener(evt, (e) => {
|
||||
|
@ -323,7 +342,7 @@
|
|||
|
||||
iv.append(
|
||||
lbl(id + "Name", "Name*"),
|
||||
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9.\\-_\\+\\/]+", "Invalid input")
|
||||
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")
|
||||
);
|
||||
|
||||
for(var j of [["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModName", "ch_name", "Module Name", 16, null]]) {
|
||||
|
@ -339,10 +358,15 @@
|
|||
iv.appendChild(d);
|
||||
}
|
||||
|
||||
iv.append(
|
||||
br(),
|
||||
lbl(id + "YieldCor", "Yield Total Correction (will be subtracted) [kWh]"),
|
||||
inp(id + "YieldCor", obj["yieldCor"], 32, ["text"], null, "text", "[0-9]+", "Invalid input")
|
||||
);
|
||||
|
||||
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button");
|
||||
del.addEventListener("click", delIv);
|
||||
iv.append(
|
||||
br(),
|
||||
lbl(id + "lbldel", "Delete"),
|
||||
del
|
||||
);
|
||||
|
@ -394,6 +418,9 @@
|
|||
function parseMqtt(obj) {
|
||||
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]])
|
||||
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];
|
||||
|
||||
for(var i of [["Mid", "rstMid"], ["ComStop", "rstNAvail"], ["NotAvail", "rstComStop"]])
|
||||
document.getElementsByName("mqttRst"+i[0])[0].checked = obj[i[1]];
|
||||
}
|
||||
|
||||
function parseNtp(obj) {
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
<input type="file" name="update">
|
||||
<input type="button" class="btn" value="Update" onclick="hide()">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
|
|
117
src/web/web.h
117
src/web/web.h
|
@ -83,6 +83,8 @@ class Web {
|
|||
mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1));
|
||||
mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1),
|
||||
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
|
||||
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1),
|
||||
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
|
||||
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
|
||||
|
||||
|
||||
|
@ -92,6 +94,8 @@ class Web {
|
|||
mWeb.begin();
|
||||
|
||||
registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h
|
||||
|
||||
mUploadFail = false;
|
||||
}
|
||||
|
||||
void tickSecond() {
|
||||
|
@ -150,6 +154,34 @@ class Web {
|
|||
}
|
||||
}
|
||||
|
||||
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if(!index) {
|
||||
mUploadFail = false;
|
||||
mUploadFp = LittleFS.open("/tmp.json", "w");
|
||||
if(!mUploadFp) {
|
||||
DPRINTLN(DBG_ERROR, F("can't open file!"));
|
||||
mUploadFail = true;
|
||||
mUploadFp.close();
|
||||
}
|
||||
}
|
||||
mUploadFp.write(data, len);
|
||||
if(final) {
|
||||
mUploadFp.close();
|
||||
File fp = LittleFS.open("/tmp.json", "r");
|
||||
if(!fp)
|
||||
mUploadFail = true;
|
||||
else {
|
||||
if(!mApp->readSettings("tmp.json")) {
|
||||
mUploadFail = true;
|
||||
DPRINTLN(DBG_ERROR, F("upload JSON error!"));
|
||||
}
|
||||
else
|
||||
mApp->saveSettings();
|
||||
}
|
||||
DPRINTLN(DBG_INFO, F("upload finished!"));
|
||||
}
|
||||
}
|
||||
|
||||
void serialCb(String msg) {
|
||||
if(!mSerialClientConnnected)
|
||||
return;
|
||||
|
@ -214,6 +246,23 @@ class Web {
|
|||
mApp->setRebootFlag();
|
||||
}
|
||||
|
||||
void onUpload(AsyncWebServerRequest *request) {
|
||||
bool reboot = !mUploadFail;
|
||||
|
||||
String html = F("<!doctype html><html><head><title>Upload</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Upload: ");
|
||||
if(reboot)
|
||||
html += "success";
|
||||
else
|
||||
html += "failed";
|
||||
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
|
||||
|
||||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
if(reboot)
|
||||
mApp->setRebootFlag();
|
||||
}
|
||||
|
||||
void onConnect(AsyncEventSourceClient *client) {
|
||||
DPRINTLN(DBG_VERBOSE, "onConnect");
|
||||
|
||||
|
@ -429,6 +478,7 @@ class Web {
|
|||
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
|
||||
default: break;
|
||||
}
|
||||
iv->config->yieldCor = request->arg("inv" + String(i) + "YieldCor").toInt();
|
||||
|
||||
// name
|
||||
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
|
||||
|
@ -495,6 +545,9 @@ class Web {
|
|||
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
|
||||
mConfig->mqtt.port = request->arg("mqttPort").toInt();
|
||||
mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
|
||||
mConfig->mqtt.rstYieldMidNight = (request->arg("mqttRstMid") == "on");
|
||||
mConfig->mqtt.rstValsNotAvail = (request->arg("mqttRstComStop") == "on");
|
||||
mConfig->mqtt.rstValsCommStop = (request->arg("mqttRstNotAvail") == "on");
|
||||
|
||||
// serial console
|
||||
if(request->arg("serIntvl") != "") {
|
||||
|
@ -627,59 +680,68 @@ class Web {
|
|||
}
|
||||
|
||||
#ifdef ENABLE_JSON_EP
|
||||
void showJson(void) {
|
||||
void showJson(AsyncWebServerRequest *request) {
|
||||
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
|
||||
String modJson;
|
||||
Inverter<> *iv;
|
||||
record_t<> *rec;
|
||||
char topic[40], val[25];
|
||||
|
||||
modJson = F("{\n");
|
||||
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
||||
Inverter<> *iv = mSys->getInverterByPos(id);
|
||||
if(NULL != iv) {
|
||||
char topic[40], val[25];
|
||||
snprintf(topic, 30, "\"%s\": {\n", iv->name);
|
||||
iv = mSys->getInverterByPos(id);
|
||||
if(NULL == iv)
|
||||
continue;
|
||||
|
||||
rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
||||
snprintf(topic, 30, "\"%s\": {\n", iv->config->name);
|
||||
modJson += String(topic);
|
||||
for(uint8_t i = 0; i < iv->listLen; i++) {
|
||||
snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i));
|
||||
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i));
|
||||
for(uint8_t i = 0; i < rec->length; i++) {
|
||||
snprintf(topic, 40, "\t\"ch%d/%s\"", rec->assign[i].ch, iv->getFieldName(i, rec));
|
||||
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i, rec), iv->getUnit(i, rec));
|
||||
modJson += String(topic) + ": " + String(val) + F(",\n");
|
||||
}
|
||||
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(iv->ts) + F("\"\n\t},\n");
|
||||
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(rec->ts) + F("\"\n\t},\n");
|
||||
}
|
||||
}
|
||||
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
|
||||
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mApp->getTimestamp())) + F("\"\n}\n");
|
||||
|
||||
mWeb.send(200, F("application/json"), modJson);
|
||||
AsyncWebServerResponse *response = request->beginResponse(200, F("application/json"), modJson);
|
||||
request->send(response);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_PROMETHEUS_EP
|
||||
void showMetrics(void) {
|
||||
void showMetrics(AsyncWebServerRequest *request) {
|
||||
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
|
||||
String metrics;
|
||||
char headline[80];
|
||||
|
||||
snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mApp->getVersion(), mconfig->sys.deviceName);
|
||||
snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mApp->getVersion(), mConfig->sys.deviceName);
|
||||
metrics += "# TYPE ahoy_solar_info gauge\n" + String(headline) + "\n";
|
||||
|
||||
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
||||
Inverter<> *iv = mSys->getInverterByPos(id);
|
||||
if(NULL != iv) {
|
||||
Inverter<> *iv;
|
||||
record_t<> *rec;
|
||||
char type[60], topic[60], val[25];
|
||||
for(uint8_t i = 0; i < iv->listLen; i++) {
|
||||
uint8_t channel = iv->assign[i].ch;
|
||||
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
||||
iv = mSys->getInverterByPos(id);
|
||||
if(NULL == iv)
|
||||
continue;
|
||||
|
||||
rec = iv->getRecordStruct(RealTimeRunData_Debug);
|
||||
for(uint8_t i = 0; i < rec->length; i++) {
|
||||
uint8_t channel = rec->assign[i].ch;
|
||||
if(channel == 0) {
|
||||
String promUnit, promType;
|
||||
std::tie(promUnit, promType) = convertToPromUnits( iv->getUnit(i) );
|
||||
snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i), promUnit.c_str(), promType.c_str());
|
||||
snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i), promUnit.c_str(), iv->name);
|
||||
snprintf(val, 25, "%.3f", iv->getValue(i));
|
||||
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(i, rec));
|
||||
snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i, rec), promUnit.c_str(), promType.c_str());
|
||||
snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i, rec), promUnit.c_str(), iv->config->name);
|
||||
snprintf(val, 25, "%.3f", iv->getValue(i, rec));
|
||||
metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mWeb.send(200, F("text/plain"), metrics);
|
||||
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), metrics);
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
std::pair<String, String> convertToPromUnits(String shortUnit) {
|
||||
|
@ -708,6 +770,9 @@ class Web {
|
|||
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
|
||||
uint16_t mSerialBufFill;
|
||||
bool mSerialClientConnnected;
|
||||
|
||||
File mUploadFp;
|
||||
bool mUploadFail;
|
||||
};
|
||||
|
||||
#endif /*__WEB_H__*/
|
||||
|
|
|
@ -27,8 +27,7 @@ class ahoywifi {
|
|||
void getAvailNetworks(JsonObject obj);
|
||||
|
||||
private:
|
||||
typedef enum WiFiStatus
|
||||
{
|
||||
typedef enum WiFiStatus {
|
||||
DISCONNECTED = 0,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue