Browse Source

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
pull/591/head
lumapu 2 years ago
committed by Mo Demman
parent
commit
6e41a83a26
  1. 8
      src/CHANGES.md
  2. 14
      src/app.cpp
  3. 7
      src/app.h
  4. 1
      src/appInterface.h
  5. 31
      src/config/settings.h
  6. 2
      src/defines.h
  7. 18
      src/hm/hmInverter.h
  8. 143
      src/publisher/pubMqtt.h
  9. 2
      src/utils/ahoyTimer.h
  10. 110
      src/utils/llist.h
  11. 1
      src/utils/scheduler.h
  12. 43
      src/web/RestApi.h
  13. 45
      src/web/html/setup.html
  14. 1
      src/web/html/update.html
  15. 129
      src/web/web.h
  16. 3
      src/wifi/ahoywifi.h

8
src/CHANGES.md

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

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

7
src/app.h

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

1
src/appInterface.h

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

31
src/config/settings.h

@ -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*>());
@ -438,9 +455,10 @@ class settings {
void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) {
if(set) {
obj[F("en")] = (bool)cfg->enabled;
obj[F("name")] = cfg->name;
obj[F("sn")] = cfg->serial.u64;
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*>());

2
src/defines.h

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

18
src/hm/hmInverter.h

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

143
src/publisher/pubMqtt.h

@ -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 (MQTT_STATUS_AVAIL_PROD == status)
status = MQTT_STATUS_AVAIL_NOT_PROD;
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,49 +467,52 @@ class PubMqtt {
record_t<> *rec = iv->getRecordStruct(mSendList.front());
// data
if(iv->isAvailable(*mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (mSendList.front() == RealTimeRunData_Debug) {
//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) {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
retained = true;
break;
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
retained = true;
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
}
}
sendTotal = true;
}
yield();
sendTotal = true;
}
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)

2
src/utils/ahoyTimer.h

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

110
src/utils/llist.h

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

1
src/utils/scheduler.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();
}

43
src/web/RestApi.h

@ -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];
@ -276,12 +294,15 @@ class RestApi {
}
void getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
obj[F("user")] = String(mConfig->mqtt.user);
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("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
obj[F("user")] = String(mConfig->mqtt.user);
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) {

45
src/web/html/setup.html

@ -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>
@ -148,6 +154,12 @@
<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" />
<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) {

1
src/web/html/update.html

@ -23,7 +23,6 @@
<input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()">
</form>
</div>
</div>
<div id="footer">

129
src/web/web.h

@ -82,7 +82,9 @@ 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));
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);
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));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(iv->ts) + F("\"\n\t},\n");
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 < 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(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";
Inverter<> *iv;
record_t<> *rec;
char type[60], topic[60], val[25];
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char type[60], topic[60], val[25];
for(uint8_t i = 0; i < iv->listLen; i++) {
uint8_t channel = iv->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));
metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n";
}
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, 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__*/

3
src/wifi/ahoywifi.h

@ -27,8 +27,7 @@ class ahoywifi {
void getAvailNetworks(JsonObject obj);
private:
typedef enum WiFiStatus
{
typedef enum WiFiStatus {
DISCONNECTED = 0,
CONNECTING,
CONNECTED,

Loading…
Cancel
Save