538 lines
16 KiB
C
538 lines
16 KiB
C
//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 <AsyncTCP.h> //for esp32
|
|
#include <ESPAsyncTCP.h> //for esp8266
|
|
#include <ESPAsyncWebServer.h>
|
|
|
|
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();
|
|
}
|
|
}
|