//todo: // - more accurate TX/RX indication (for security concern), e.g. favicon is served static, thus not display TX/RX arrow. // - Login and apikey access for next version #include "wifi.h" #include "upsCfg.h" #ifndef UPSIO_H #include "upsIo.h" #endif //#include //for esp32 #include //for esp8266 #include const char* http_username = "admin"; const char* http_password = "admin123"; String base64Decode(const String& input) { const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; String out; int buf = 0, bits = 0; for (size_t i = 0; i < input.length(); i++) { char c = input[i]; if (c == '=') break; const char* p = strchr(b64, c); if (!p) continue; buf = (buf << 6) | (p - b64); bits += 6; if (bits >= 8) { bits -= 8; out += (char)((buf >> bits) & 0xFF); buf &= (1 << bits) - 1; } } return out; } bool checkAuth(AsyncWebServerRequest *request) { if (!request->hasHeader("Authorization")) return false; String auth = request->header("Authorization"); if (!auth.startsWith("Basic ")) return false; String b64 = auth.substring(6); b64.trim(); String decoded = base64Decode(b64); int sep = decoded.indexOf(':'); if (sep < 0) return false; String user = decoded.substring(0, sep); String pass = decoded.substring(sep + 1); return user.equals(http_username) && pass.equals(http_password); } int8_t wifiStatePrev = 99; bool configChanged = false; AsyncWebServer server(80); void applyConfigs() { autoPowerOn = (cfgAutoOn >= AUTO_POWER_ON); //auto power on //cfgChargeMode: charge mode: switch (cfgChargeMode) { case CHARGE_MODE_ALWAYS: { chargeAlwaysActive = true; } break; case CHARGE_MODE_RANGE_90_100: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 100; chargePercentageForResume = 90; } break; case CHARGE_MODE_RANGE_85_100: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 100; chargePercentageForResume = 85; } break; case CHARGE_MODE_RANGE_80_100: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 100; chargePercentageForResume = 80; } break; case CHARGE_MODE_RANGE_90_95: { chargeAlwaysActive = false; chargeToFull = false; chargePercentageForStop = 95; chargePercentageForResume = 90; } break; case CHARGE_MODE_RANGE_85_95: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 95; chargePercentageForResume = 85; } break; case CHARGE_MODE_RANGE_80_95: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 95; chargePercentageForResume = 80; } break; case CHARGE_MODE_RANGE_85_90: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 90; chargePercentageForResume = 85; } break; case CHARGE_MODE_RANGE_80_90: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 90; chargePercentageForResume = 80; } break; case CHARGE_MODE_RANGE_80_85: { chargeAlwaysActive = false; chargeToFull = true; chargePercentageForStop = 85; chargePercentageForResume = 80; } break; } //cfgInputLostTriggerChargeMode: switch (cfgInputLostTriggerChargeMode) { case INPUT_LOST_TRIGGER_CHARGE_NONE: { chargeAfterInputLost = false; } break; case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_ALWAYS: { chargeAfterInputLost = true; fullChargeAfterInputLost = true; } break; case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_95: { chargeAfterInputLost = true; fullChargeAfterInputLost = false; triggerFullChargeAfterInputLostAtPercentage = 95; } break; case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_90: { chargeAfterInputLost = true; fullChargeAfterInputLost = false; triggerFullChargeAfterInputLostAtPercentage = 90; } break; case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_85: { chargeAfterInputLost = true; fullChargeAfterInputLost = false; triggerFullChargeAfterInputLostAtPercentage = 85; } break; case INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80: { chargeAfterInputLost = true; fullChargeAfterInputLost = false; triggerFullChargeAfterInputLostAtPercentage = 80; } break; } //cfgChargeOvervoltageProtection: chargeOvervoltageProtection = (cfgChargeOvervoltageProtection > 0); //cfgChargeAutomaticReducer: chargeReduceAtBatteryFull = (cfgChargeAutomaticReducer > 0); } bool getShutdownSuggestionFlag() { if (cfgShutdownSuggestion == SHUTDOWN_SUGGESTION_DISABLED) return false; if (!startupReady) return false; switch (cfgShutdownSuggestion) { case SHUTDOWN_SUGGESTION_ON_BATT: { return powerOutputActive && !inputVoltageKeepup; } break; case SHUTDOWN_SUGGESTION_ON_BATT_75: { return powerOutputActive && !inputVoltageKeepup && (batteryLevel <= 3); } break; case SHUTDOWN_SUGGESTION_ON_BATT_50: { return powerOutputActive && !inputVoltageKeepup && (batteryLevel <= 2); } break; case SHUTDOWN_SUGGESTION_ON_BATT_25: { return powerOutputActive && !inputVoltageKeepup && (batteryLevel <= 1); } break; case SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL: { return powerOutputActive && !inputVoltageKeepup && (batteryLevel <= 1) && isUpsBatteryCritical(); } break; default: { return false; } } return false; } void handleGetState(AsyncWebServerRequest *request) { if (!checkAuth(request)) { setNetworkIndicatorInTransmission(true, false); AsyncWebServerResponse *response = request->beginResponse(401, "application/json", "{\"success\":0}"); // response->addHeader("WWW-Authenticate", "Basic realm=\"UPS Login\""); // Removed to prevent native browser prompt request->send(response); setNetworkIndicatorInTransmission(true, true); return; } setNetworkIndicatorInTransmission(true, false); bool isRefresh = true; String v = ""; uint8_t i = 0; if (request->hasParam("on")) { isRefresh = false; v = request->getParam("on")->value(); bool tgtState = v != "0"; Serial.print("Switch request: "); Serial.println(tgtState); if (tgtState != powerOutputActive) { setOutputPowerState(tgtState); Serial.println("Switching..."); } } if (request->hasParam("auto")) { isRefresh = false; v = request->getParam("auto")->value(); uint8_t tmpAutoOnMode = v.toInt(); if ((tmpAutoOnMode != cfgAutoOn) && (tmpAutoOnMode <= AUTO_POWER_PERSIST)) { cfgAutoOn = tmpAutoOnMode; configChanged = true; } } if (request->hasParam("shutdown")) { isRefresh = false; v = request->getParam("shutdown")->value(); uint16_t tSec = v.toInt(); if (tSec > 999) tSec = 999; uint32_t nanoT = ((uint32_t)tSec) * 1000000UL; if (nanoT < 999999) nanoT = 999999; bool ok = setPersistentShutdown(nanoT); if ((cfgShutdownInSeconds != tSec && ok)) { cfgShutdownInSeconds = tSec; configChanged = true; } Serial.print("Timed Shutdown request: "); Serial.println(tSec); //Serial.println(ok); } if (request->hasParam("shutlv")) { isRefresh = false; v = request->getParam("shutlv")->value(); int16_t lv = v.toInt(); if (lv >= SHUTDOWN_SUGGESTION_DISABLED && lv <= SHUTDOWN_SUGGESTION_ON_BATT_CRITICAL) { if (lv != cfgShutdownSuggestion) { cfgShutdownSuggestion = lv; configChanged = true; Serial.print("Shutdown level changed: "); Serial.println(lv); } } else { Serial.print("Invalid shutdown level: "); Serial.println(v); } } //battery management params: //cfgChargeMode (charge mode): if (request->hasParam("cm")) { isRefresh = false; v = request->getParam("cm")->value(); i = v.toInt(); if (i <= CHARGE_MODE_RANGE_80_85) { cfgChargeMode = i; configChanged = true; Serial.print("CM: "); Serial.println(i); } } //cfgInputLostTriggerChargeMode (charge on input lost): if (request->hasParam("coil")) { isRefresh = false; v = request->getParam("coil")->value(); i = v.toInt(); if (i <= INPUT_LOST_TRIGGER_CHARGE_ACTIVE_FULL_80) { cfgInputLostTriggerChargeMode = i; configChanged = true; Serial.print("COIL: "); Serial.println(i); } } //cfgChargeOvervoltageProtection (charge overvoltage protection): if (request->hasParam("covp")) { isRefresh = false; v = request->getParam("covp")->value(); i = v.toInt(); if (i > 1) i = 1; if (i != cfgChargeOvervoltageProtection) { cfgChargeOvervoltageProtection = i; configChanged = true; Serial.print("COVP: "); Serial.println(i); } } //reset "On-Going Full Charge" triggered after input lost (reset ongoing full charge): if (request->hasParam("rofc")) { isRefresh = false; v = request->getParam("rofc")->value(); i = v.toInt(); if (i > 0 && needFullChargeOnce) { resetOnGoingFullChargeAfterLineLost(); Serial.print("ROFC: "); Serial.println(i); } } //cfgChargeAutomaticReducer (charge automatic reducer) if (request->hasParam("car")) { isRefresh = false; v = request->getParam("car")->value(); i = v.toInt(); if (i > 1) i = 1; if (i != cfgChargeAutomaticReducer) { cfgChargeAutomaticReducer = i; configChanged = true; Serial.print("CAR: "); Serial.println(i); } } DynamicJsonDocument jsonDoc(1536); String jsonResponse; //String stateOn = isUpsOn()?"1":"0"; //String autoOn = String(cfgAutoOn); //String voltageValue = String(batteryVoltage, 2); //2 battery, 1 Input line or AC, 0 OFF //String lineActive = isUpsPoweredFromBattery()?"2":isInputLinePowered()?"1":isUpsOn()?"2":"0"; uint8_t lineActiveState = isUpsPoweredFromBattery() ? 2 : isInputLinePowered() ? 1 : isUpsOn() ? 2 : 0; //String batt = String(batteryLevel); //String battMax = "4"; // String battBlink = IsUpsChargingOrBlink()?"1":"0"; // String chargeLv = IsUpsChargingOrBlink()?(String(UpsBattMinimalLevelUnderChargingOrBlink())):(String(UpsBatteryLevel())); // String battCritical = IsUpsBatteryCritical()?"1":"0"; // String isTimedShut = timedShutdownFlag?"1":"0"; // String onShutRemaining = String(GetShutdownCountdown()); // String shutdownSuggest = GetShutdownSuggestionFlag()?"1":"0"; // String shutdownSuggestMode = String(cfgShutdownSuggestion); // String shutdownLastPeriod = String(timeShutdownLastPeriod); //json vars: jsonDoc["success"] = isRefresh ? 2 : 1; jsonDoc["on"] = powerOutputActive ? 1 : 0; ; jsonDoc["state"] = isUpsOn() ? 1 : 0; jsonDoc["v"] = String(batteryVoltage, 2); jsonDoc["batt"] = batteryLevel; jsonDoc["battP"] = batteryPercentage; jsonDoc["line"] = lineActiveState; jsonDoc["auto"] = cfgAutoOn; jsonDoc["battBlink"] = isUpsBatteryIndicatorChargingOrBlink() ? 1 : 0; jsonDoc["battCritical"] = isUpsBatteryCritical() ? 1 : 0; jsonDoc["chargeLv"] = getRawBatteryLevelFromPercentage(batteryPercentage); //real level jsonDoc["batt"] = batteryLevel; //staged level jsonDoc["battMax"] = 4; jsonDoc["timedShutdown"] = (persistentShutdown && powerOutputActive) ? 1 : 0; jsonDoc["shutdownRemain"] = getPersistentShutdownTick() / 1000000; jsonDoc["shutdownSuggestMode"] = cfgShutdownSuggestion; jsonDoc["shutdownSuggest"] = getShutdownSuggestionFlag() ? 1 : 0; //internal flag raised if cfgShutdownSuggestion fullfill the condition jsonDoc["shutLastTimer"] = cfgShutdownInSeconds; //charge configuration: jsonDoc["chgError"] = chargeError; //error state jsonDoc["charging"] = getChargingState(); //on charging state jsonDoc["chgMode"] = cfgChargeMode; jsonDoc["chgOVP"] = cfgChargeOvervoltageProtection; //over voltage protection jsonDoc["chgReducer"] = cfgChargeAutomaticReducer; jsonDoc["chgFullTrig"] = cfgInputLostTriggerChargeMode; jsonDoc["chgOFC"] = needFullChargeOnce ? 1 : 0; //when input lost and "Ongoing Full Charge" flag triggered jsonDoc["vSub"] = String(batteryVoltageUnfiltered, 2); jsonDoc["chgSwitch"] = isChargeActive ? 1 : 0; jsonDoc["inputUV"] = inputUnderVoltage ? 1 : 0; jsonDoc["inputVExist"] = inputVoltageKeepup ? 1 : 0; jsonDoc["btn"] = buttonState ? 1 : 0; jsonDoc["btnShort"] = buttonShortPress ? 1 : 0; jsonDoc["btnLong"] = buttonLongPress ? 1 : 0; jsonDoc["keepPower"] = keepPowerState ? 1 : 0; jsonDoc["display"] = displayActive ? 1 : 0; jsonDoc["rssi"] = rssi; jsonDoc["chargeAllowed"] = chargeAllowed ? 1 : 0; serializeJson(jsonDoc, jsonResponse); request->send(200, "application/json", jsonResponse); setNetworkIndicatorInTransmission(true, true); } void onRootRequest(AsyncWebServerRequest *request) { setNetworkIndicatorInTransmission(true, false); Serial.println("onRootRequest"); bool isOK = false; if (spiffsOk) { if (LittleFS.exists("/index.html")) { isOK = true; request->send(LittleFS, "/index.html", "text/html"); setNetworkIndicatorInTransmission(true, true); } } if (!isOK) { request->send(404, "text/text", "404: Not Found"); setNetworkIndicatorInTransmission(true, true); } } void upsServerInit() { bool b = initConfigs(); if (b) { loadCfg(); applyConfigs(); } //transfer configuration to ups library: if (cfgAutoOn >= AUTO_POWER_ON) { autoPowerOn = true; restartAutoPowerOn(); } else { autoPowerOn = false; } //for favicon server.serveStatic("/favicon.ico", LittleFS, "/favicon.ico"); server.serveStatic("*/favicon.ico", LittleFS, "/favicon.ico"); //login endpoint: validate credentials server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request) { setNetworkIndicatorInTransmission(true, false); if (!checkAuth(request)) { request->send(401, "application/json", "{\"success\":0}"); } else { request->send(200, "application/json", "{\"success\":1}"); } setNetworkIndicatorInTransmission(true, true); }); //logout endpoint: clear basic auth by returning 401 server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request) { setNetworkIndicatorInTransmission(true, false); AsyncWebServerResponse *response = request->beginResponse(401, "application/json", "{\"success\":0,\"message\":\"Logged out\"}"); request->send(response); setNetworkIndicatorInTransmission(true, true); }); //for RESTapi server.on("/state", HTTP_GET, handleGetState); //root server.on("/", onRootRequest); server.on("/*", onRootRequest); //Redirect all not-found requests to the root server.onNotFound(onRootRequest); server.begin(); Serial.println("Server is running!"); } void upsServerRoutine() { WiFiManagerRoutine(); //updating wifi state: if (wifiCurrentState != wifiStatePrev) { wifiStatePrev = wifiCurrentState; setNetworkState(wifiStatePrev); } //update signal: indicatorNetworkSignalLevel = wifiSignalLevel; //manage auto on mode: if (cfgAutoOn == AUTO_POWER_PERSIST) { if (inputVoltageKeepup && inputVoltageOK) { if (restartAutoPowerOn()) Serial.println("Automatic ON restarted"); } } //if button long pressed ------------------------------------------- if (buttonLongPressIgnore && buttonLongPress) { buttonLongPressIgnore = true; Serial.println("Long Pressed Button Action is Reserved!"); //(not used) } //------------------------------------------------------------------ //saving configuration: if (configChanged) { configChanged = false; saveCfg(); applyConfigs(); } }