//----------------------------------------------------------------------------- // 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- #if defined(ESP32) && defined(F) #undef F #define F(sl) (sl) #endif #include "web.h" #include "html/h/index_html.h" #include "html/h/style_css.h" #include "favicon.h" #include "html/h/setup_html.h" #include "html/h/visualization_html.h" const uint16_t pwrLimitOptionValues[] { NoPowerLimit, AbsolutNonPersistent, AbsolutPersistent, RelativNonPersistent, RelativPersistent }; const char* const pwrLimitOptions[] { "no power limit", "absolute in Watt non persistent", "absolute in Watt persistent", "relativ in percent non persistent", "relativ in percent persistent" }; //----------------------------------------------------------------------------- web::web(app *main, sysConfig_t *sysCfg, config_t *config, char version[]) { mMain = main; mSysCfg = sysCfg; mConfig = config; mVersion = version; #ifdef ESP8266 mWeb = new ESP8266WebServer(80); mUpdater = new ESP8266HTTPUpdateServer(); #elif defined(ESP32) mWeb = new WebServer(80); mUpdater = new HTTPUpdateServer(); #endif mUpdater->setup(mWeb); } //----------------------------------------------------------------------------- void web::setup(void) { DPRINTLN(DBG_VERBOSE, F("app::setup-begin")); mWeb->begin(); DPRINTLN(DBG_VERBOSE, F("app::setup-on")); mWeb->on("/", std::bind(&web::showIndex, this)); mWeb->on("/style.css", std::bind(&web::showCss, this)); mWeb->on("/favicon.ico", std::bind(&web::showFavicon, this)); mWeb->onNotFound ( std::bind(&web::showNotFound, this)); mWeb->on("/uptime", std::bind(&web::showUptime, this)); mWeb->on("/reboot", std::bind(&web::showReboot, this)); mWeb->on("/erase", std::bind(&web::showErase, this)); mWeb->on("/factory", std::bind(&web::showFactoryRst, this)); mWeb->on("/setup", std::bind(&web::showSetup, this)); mWeb->on("/save", std::bind(&web::showSave, this)); mWeb->on("/cmdstat", std::bind(&web::showStatistics, this)); mWeb->on("/visualization", std::bind(&web::showVisualization, this)); mWeb->on("/livedata", std::bind(&web::showLiveData, this)); mWeb->on("/json", std::bind(&web::showJson, this)); mWeb->on("/api", HTTP_POST, std::bind(&web::showWebApi, this)); } //----------------------------------------------------------------------------- void web::loop(void) { mWeb->handleClient(); } //----------------------------------------------------------------------------- void web::showIndex(void) { DPRINTLN(DBG_VERBOSE, F("showIndex")); String html = FPSTR(index_html); html.replace(F("{DEVICE}"), mSysCfg->deviceName); html.replace(F("{VERSION}"), mVersion); html.replace(F("{TS}"), String(mConfig->sendInterval) + " "); html.replace(F("{JS_TS}"), String(mConfig->sendInterval * 1000)); html.replace(F("{BUILD}"), String(AUTO_GIT_HASH)); mWeb->send(200, "text/html", html); } //----------------------------------------------------------------------------- void web::showCss(void) { mWeb->send(200, "text/css", FPSTR(style_css)); } //----------------------------------------------------------------------------- void web::showFavicon(void) { static const char favicon_type[] PROGMEM = "image/x-icon"; static const char favicon_content[] PROGMEM = FAVICON_PANEL_16; mWeb->send_P(200, favicon_type, favicon_content, sizeof(favicon_content)); } //----------------------------------------------------------------------------- void web::showNotFound(void) { DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + mWeb->uri()); String msg = F("File Not Found\n\nURI: "); msg += mWeb->uri(); mWeb->send(404, F("text/plain"), msg); } //----------------------------------------------------------------------------- void web::showUptime(void) { char time[21] = {0}; uint32_t uptime = mMain->getUptime(); uint32_t upTimeSc = uint32_t((uptime) % 60); uint32_t upTimeMn = uint32_t((uptime / (60)) % 60); uint32_t upTimeHr = uint32_t((uptime / (60 * 60)) % 24); uint32_t upTimeDy = uint32_t((uptime / (60 * 60 * 24)) % 365); snprintf(time, 20, "%d Days, %02d:%02d:%02d", upTimeDy, upTimeHr, upTimeMn, upTimeSc); mWeb->send(200, "text/plain", String(time) + "; now: " + mMain->getDateTimeStr(mMain->getTimestamp())); } //----------------------------------------------------------------------------- void web::showReboot(void) { mWeb->send(200, F("text/html"), F("Rebooting ...rebooting ... auto reload after 10s")); delay(1000); ESP.restart(); } //----------------------------------------------------------------------------- void web::showErase() { DPRINTLN(DBG_VERBOSE, F("showErase")); mMain->eraseSettings(); showReboot(); } //----------------------------------------------------------------------------- void web::showFactoryRst(void) { DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); String content = ""; int refresh = 3; if(mWeb->args() > 0) { if(mWeb->arg("reset").toInt() == 1) { mMain->eraseSettings(true); content = F("factory reset: success\n\nrebooting ... "); refresh = 10; } else { content = F("factory reset: aborted"); refresh = 3; } } else { content = F("

Factory Reset

" "

RESET

CANCEL

"); refresh = 120; } mWeb->send(200, F("text/html"), F("Factory Reset") + content + F("")); if(refresh == 10) { delay(1000); ESP.restart(); } } //----------------------------------------------------------------------------- void web::showSetup(void) { DPRINTLN(DBG_VERBOSE, F("showSetup")); String html = FPSTR(setup_html); html.replace(F("{SSID}"), mSysCfg->stationSsid); // PWD will be left at the default value (for protection) // -> the PWD will only be changed if it does not match the default "{PWD}" html.replace(F("{DEVICE}"), String(mSysCfg->deviceName)); html.replace(F("{VERSION}"), String(mVersion)); String inv = ""; Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mMain->mSys->getInverterByPos(i); inv += F("

Inverter ") + String(i) + "

"; inv += F(""); inv += F("serial.u64, HEX); inv += F("\"/ maxlength=\"12\">"); inv += F(""); inv += F("name); inv += F("\"/ maxlength=\"") + String(MAX_NAME_LENGTH) + "\">"; inv += F(""); inv += F("powerLimit[0]); inv += F("\"/ maxlength=\"") + String(6) + "\">"; inv += F(""); inv += F(""); inv += F("
"); for(uint8_t j = 0; j < 4; j++) { inv += F("chMaxPwr[j]); inv += F("\"/ maxlength=\"4\">"); } inv += F("

"); for(uint8_t j = 0; j < 4; j++) { inv += F("chName[j]); inv += F("\"/ maxlength=\"") + String(MAX_NAME_LENGTH) + "\">"; } inv += F("
"); } html.replace(F("{INVERTERS}"), String(inv)); // pinout String pinout; for(uint8_t i = 0; i < 3; i++) { pinout += F(""); pinout += F(""); } html.replace(F("{PINOUT}"), String(pinout)); // nrf24l01+ String rf24; for(uint8_t i = 0; i <= 3; i++) { rf24 += F(""); } html.replace(F("{RF24}"), String(rf24)); html.replace(F("{INV_INTVL}"), String(mConfig->sendInterval)); html.replace(F("{INV_RETRIES}"), String(mConfig->maxRetransPerPyld)); html.replace(F("{SER_INTVL}"), String(mConfig->serialInterval)); html.replace(F("{SER_VAL_CB}"), (mConfig->serialShowIv) ? "checked" : ""); html.replace(F("{SER_DBG_CB}"), (mConfig->serialDebug) ? "checked" : ""); html.replace(F("{NTP_ADDR}"), String(mConfig->ntpAddr)); html.replace(F("{NTP_PORT}"), String(mConfig->ntpPort)); html.replace(F("{MQTT_ADDR}"), String(mConfig->mqtt.broker)); html.replace(F("{MQTT_PORT}"), String(mConfig->mqtt.port)); html.replace(F("{MQTT_USER}"), String(mConfig->mqtt.user)); html.replace(F("{MQTT_PWD}"), String(mConfig->mqtt.pwd)); html.replace(F("{MQTT_TOPIC}"), String(mConfig->mqtt.topic)); mWeb->send(200, F("text/html"), html); } //----------------------------------------------------------------------------- void web::showSave(void) { DPRINTLN(DBG_VERBOSE, F("showSave")); if(mWeb->args() > 0) { char buf[20] = {0}; // general if(mWeb->arg("ssid") != "") mWeb->arg("ssid").toCharArray(mSysCfg->stationSsid, SSID_LEN); if(mWeb->arg("pwd") != "{PWD}") mWeb->arg("pwd").toCharArray(mSysCfg->stationPwd, PWD_LEN); if(mWeb->arg("device") != "") mWeb->arg("device").toCharArray(mSysCfg->deviceName, DEVNAME_LEN); // inverter Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mMain->mSys->getInverterByPos(i, false); // address mWeb->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); if(strlen(buf) == 0) memset(buf, 0, 20); iv->serial.u64 = mMain->Serial2u64(buf); // active power limit uint16_t actPwrLimit = mWeb->arg("inv" + String(i) + "ActivePowerLimit").toInt(); uint16_t actPwrLimitControl = mWeb->arg("inv" + String(i) + "PowerLimitControl").toInt(); if (actPwrLimit != 0xffff && actPwrLimit > 0){ iv->powerLimit[0] = actPwrLimit; iv->powerLimit[1] = actPwrLimitControl; iv->devControlCmd = ActivePowerContr; iv->devControlRequest = true; if ((iv->powerLimit[1] & 0x0001) == 0x0001) DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%") ); else { DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W") ); DPRINTLN(DBG_INFO, F("Power Limit Control Setting ") + String(iv->powerLimit[1])); } } if (actPwrLimit == 0xffff) { // set to 100% iv->powerLimit[0] = 100; iv->powerLimit[1] = RelativPersistent; iv->devControlCmd = ActivePowerContr; iv->devControlRequest = true; DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to unlimted")); } // name mWeb->arg("inv" + String(i) + "Name").toCharArray(iv->name, MAX_NAME_LENGTH); // max channel power / name for(uint8_t j = 0; j < 4; j++) { iv->chMaxPwr[j] = mWeb->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; mWeb->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->chName[j], MAX_NAME_LENGTH); } iv->initialized = true; } if(mWeb->arg("invInterval") != "") mConfig->sendInterval = mWeb->arg("invInterval").toInt(); if(mWeb->arg("invRetry") != "") mConfig->maxRetransPerPyld = mWeb->arg("invRetry").toInt(); // pinout uint8_t pin; for(uint8_t i = 0; i < 3; i ++) { pin = mWeb->arg(String(pinArgNames[i])).toInt(); switch(i) { default: mConfig->pinCs = pin; break; case 1: mConfig->pinCe = pin; break; case 2: mConfig->pinIrq = pin; break; } } // nrf24 amplifier power mConfig->amplifierPower = mWeb->arg("rf24Power").toInt() & 0x03; // ntp if(mWeb->arg("ntpAddr") != "") { mWeb->arg("ntpAddr").toCharArray(mConfig->ntpAddr, NTP_ADDR_LEN); mConfig->ntpPort = mWeb->arg("ntpPort").toInt() & 0xffff; } // mqtt if(mWeb->arg("mqttAddr") != "") { String addr = mWeb->arg("mqttAddr"); addr.trim(); addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); mWeb->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); mWeb->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); mWeb->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); mConfig->mqtt.port = mWeb->arg("mqttPort").toInt(); } // serial console if(mWeb->arg("serIntvl") != "") { mConfig->serialInterval = mWeb->arg("serIntvl").toInt() & 0xffff; mConfig->serialDebug = (mWeb->arg("serDbg") == "on"); mConfig->serialShowIv = (mWeb->arg("serEn") == "on"); // Needed to log TX buffers to serial console mMain->mSys->Radio.mSerialDebug = mConfig->serialDebug; } mMain->saveValues(); if(mWeb->arg("reboot") == "on") showReboot(); else mWeb->send(200, F("text/html"), F("Setup saved" "

saved

")); } } //----------------------------------------------------------------------------- void web::showStatistics(void) { DPRINTLN(DBG_VERBOSE, F("web::showStatistics")); mWeb->send(200, F("text/plain"), mMain->getStatistics()); } //----------------------------------------------------------------------------- void web::showVisualization(void) { DPRINTLN(DBG_VERBOSE, F("web::showVisualization")); String html = FPSTR(visualization_html); html.replace(F("{DEVICE}"), mSysCfg->deviceName); html.replace(F("{VERSION}"), mVersion); html.replace(F("{TS}"), String(mConfig->sendInterval) + " "); html.replace(F("{JS_TS}"), String(mConfig->sendInterval * 1000)); mWeb->send(200, F("text/html"), html); } //----------------------------------------------------------------------------- void web::showLiveData(void) { DPRINTLN(DBG_VERBOSE, F("web::showLiveData")); String modHtml; for (uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) { Inverter<> *iv = mMain->mSys->getInverterByPos(id); if (NULL != iv) { #ifdef LIVEDATA_VISUALIZED uint8_t modNum, pos; switch (iv->type) { default: case INV_TYPE_1CH: modNum = 1; break; case INV_TYPE_2CH: modNum = 2; break; case INV_TYPE_4CH: modNum = 4; break; } modHtml += F("
" "
") + String(iv->name) + F(" Limit ") + String(iv->actPowerLimit) + F("%"); if(NoPowerLimit == iv->powerLimit[1]) modHtml += F(" (not controlled)"); modHtml += F(" | last Alarm: ") + iv->lastAlarmMsg + F(""); uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PCT, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_PRA, FLD_ALARM_MES_ID}; for (uint8_t fld = 0; fld < 11; fld++) { pos = (iv->getPosByChFld(CH0, list[fld])); if (0xff != pos) { modHtml += F("
"); modHtml += F("") + String(iv->getValue(pos)); modHtml += F("") + String(iv->getUnit(pos)) + F(""); modHtml += F("") + String(iv->getFieldName(pos)) + F(""); modHtml += F("
"); } } modHtml += "
"; for (uint8_t ch = 1; ch <= modNum; ch++) { modHtml += F("
"); if (iv->chName[ch - 1][0] == 0) modHtml += F("CHANNEL ") + String(ch); else modHtml += String(iv->chName[ch - 1]); modHtml += F(""); for (uint8_t j = 0; j < 6; j++) { switch (j) { default: pos = (iv->getPosByChFld(ch, FLD_UDC)); break; case 1: pos = (iv->getPosByChFld(ch, FLD_IDC)); break; case 2: pos = (iv->getPosByChFld(ch, FLD_PDC)); break; case 3: pos = (iv->getPosByChFld(ch, FLD_YD)); break; case 4: pos = (iv->getPosByChFld(ch, FLD_YT)); break; case 5: pos = (iv->getPosByChFld(ch, FLD_IRR)); break; } if (0xff != pos) { modHtml += F("") + String(iv->getValue(pos)); modHtml += F("") + String(iv->getUnit(pos)) + F(""); modHtml += F("") + String(iv->getFieldName(pos)) + F(""); } } modHtml += "
"; yield(); } modHtml += F("
Last received data requested at: ") + mMain->getDateTimeStr(iv->ts) + F("
"); modHtml += F("
"); #else // dump all data to web frontend modHtml = F("
");
            char topic[30], val[10];
            for (uint8_t i = 0; i < iv->listLen; i++) {
                snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
                snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
                modHtml += String(topic) + ": " + String(val) + "\n";
            }
            modHtml += F("
"); #endif } } mWeb->send(200, F("text/html"), modHtml); } //----------------------------------------------------------------------------- void web::showJson(void) { DPRINTLN(DBG_VERBOSE, F("web::showJson")); mWeb->send(200, F("application/json"), mMain->getJson()); } //----------------------------------------------------------------------------- void web::showWebApi(void) { DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); DPRINTLN(DBG_DEBUG, mWeb->arg("plain")); const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. DynamicJsonDocument response(capacity); // Parse JSON object deserializeJson(response, mWeb->arg("plain")); // ToDo: error handling for payload uint8_t iv_id = response["inverter"]; uint8_t cmd = response["cmd"]; Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id); if (NULL != iv) { if (response["tx_request"] == (uint8_t)TX_REQ_INFO) { // if the AlarmData is requested set the Alarm Index to the requested one if (cmd == AlarmData || cmd == AlarmUpdate){ // set the AlarmMesIndex for the request from user input iv->alarmMesIndex = response["payload"]; } DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"])); // process payload from web request corresponding to the cmd iv->enqueCommand(cmd); } if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) { if (response["cmd"] == (uint8_t)ActivePowerContr) { uint16_t webapiPayload = response["payload"]; uint16_t webapiPayload2 = response["payload2"]; if (webapiPayload > 0 && webapiPayload < 10000) { iv->devControlCmd = ActivePowerContr; iv->powerLimit[0] = webapiPayload; if (webapiPayload2 > 0) { iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check } else { // if not set, set it to 0x0000 default iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporay in Watt absolut } if (iv->powerLimit[1] & 0x0001) { DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); } else { DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); } iv->devControlRequest = true; // queue it in the request loop } } if (response["cmd"] == (uint8_t)TurnOff){ iv->devControlCmd = TurnOff; iv->devControlRequest = true; // queue it in the request loop } if (response["cmd"] == (uint8_t)TurnOn){ iv->devControlCmd = TurnOn; iv->devControlRequest = true; // queue it in the request loop } } } mWeb->send(200, "text/json", "{success:true}"); }