#include "webserver.h" #include #include "../../datalayer/datalayer.h" #include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h" #include "../utils/events.h" #include "../utils/led_handler.h" #include "../utils/timer.h" // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Measure OTA progress unsigned long ota_progress_millis = 0; #include "advanced_battery_html.h" #include "cellmonitor_html.h" #include "events_html.h" #include "index_html.cpp" #include "settings_html.h" MyTimer ota_timeout_timer = MyTimer(15000); bool ota_active = false; const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral"; void init_webserver() { String content = index_html; server.on("/logout", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(401); }); // Route for firmware info from ota update page server.on("/GetFirmwareInfo", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send_P(200, "application/json", get_firmware_info_html, get_firmware_info_processor); }); // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send_P(200, "text/html", index_html, processor); }); // Route for going to settings web page server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send_P(200, "text/html", index_html, settings_processor); }); // Route for going to advanced battery info web page server.on("/advanced", HTTP_GET, [](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, advanced_battery_processor); }); // Route for going to cellmonitor web page server.on("/cellmonitor", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send_P(200, "text/html", index_html, cellmonitor_processor); }); // Route for going to event log web page server.on("/events", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send_P(200, "text/html", index_html, events_processor); }); // Route for clearing all events server.on("/clearevents", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); reset_all_events(); // Send back a response that includes an instant redirect to /events String response = ""; response += ""; // Instant redirect response += ""; request->send(200, "text/html", response); }); // Route for editing SSID server.on("/updateSSID", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); if (value.length() <= 63) { // Check if SSID is within the allowable length ssid = value.c_str(); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "SSID must be 63 characters or less"); } } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing Password server.on("/updatePassword", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); if (value.length() > 8) { // Check if password is within the allowable length password = value.c_str(); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Password must be atleast 8 characters"); } } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing Wh server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.info.total_capacity_Wh = value.toInt(); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing USE_SCALED_SOC server.on("/updateUseScaledSOC", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.settings.soc_scaling_active = value.toInt(); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing SOCMax server.on("/updateSocMax", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.settings.max_percentage = static_cast(value.toFloat() * 100); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for pause/resume Battery emulator server.on("/pause", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("p")) { String valueStr = request->getParam("p")->value(); setBatteryPause(valueStr == "true" || valueStr == "1", false); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for equipment stop/resume server.on("/equipmentStop", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("stop")) { String valueStr = request->getParam("stop")->value(); if (valueStr == "true" || valueStr == "1") { setBatteryPause(true, false, true); } else { setBatteryPause(false, false, false); } request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing SOCMin server.on("/updateSocMin", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.settings.min_percentage = static_cast(value.toFloat() * 100); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing MaxChargeA server.on("/updateMaxChargeA", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.info.max_charge_amp_dA = static_cast(value.toFloat() * 10); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for editing MaxDischargeA server.on("/updateMaxDischargeA", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); datalayer.battery.info.max_discharge_amp_dA = static_cast(value.toFloat() * 10); storeSettings(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); #ifdef TEST_FAKE_BATTERY // Route for editing FakeBatteryVoltage server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (!request->hasParam("value")) { request->send(400, "text/plain", "Bad Request"); } String value = request->getParam("value")->value(); float val = value.toFloat(); datalayer.battery.status.voltage_dV = val * 10; request->send(200, "text/plain", "Updated successfully"); }); #endif // TEST_FAKE_BATTERY #if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Route for editing ChargerTargetV server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (!request->hasParam("value")) { request->send(400, "text/plain", "Bad Request"); } String value = request->getParam("value")->value(); float val = value.toFloat(); if (!(val <= CHARGER_MAX_HV && val >= CHARGER_MIN_HV)) { request->send(400, "text/plain", "Bad Request"); } if (!(val * charger_setpoint_HV_IDC <= CHARGER_MAX_POWER)) { request->send(400, "text/plain", "Bad Request"); } charger_setpoint_HV_VDC = val; request->send(200, "text/plain", "Updated successfully"); }); // Route for editing ChargerTargetA server.on("/updateChargeSetpointA", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (!request->hasParam("value")) { request->send(400, "text/plain", "Bad Request"); } String value = request->getParam("value")->value(); float val = value.toFloat(); if (!(val <= datalayer.battery.info.max_charge_amp_dA && val <= CHARGER_MAX_A)) { request->send(400, "text/plain", "Bad Request"); } if (!(val * charger_setpoint_HV_VDC <= CHARGER_MAX_POWER)) { request->send(400, "text/plain", "Bad Request"); } charger_setpoint_HV_IDC = value.toFloat(); request->send(200, "text/plain", "Updated successfully"); }); // Route for editing ChargerEndA server.on("/updateChargeEndA", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); charger_setpoint_HV_IDC_END = value.toFloat(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for enabling/disabling HV charger server.on("/updateChargerHvEnabled", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); charger_HV_enabled = (bool)value.toInt(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); // Route for enabling/disabling aux12v charger server.on("/updateChargerAux12vEnabled", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); charger_aux12V_enabled = (bool)value.toInt(); request->send(200, "text/plain", "Updated successfully"); } else { request->send(400, "text/plain", "Bad Request"); } }); #endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Send a GET request to /update server.on("/debug", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send(200, "text/plain", "Debug: all OK."); }); // Route to handle reboot command server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) return request->requestAuthentication(); request->send(200, "text/plain", "Rebooting server..."); //Equipment STOP without persisting the equipment state before restart // Max Charge/Discharge = 0; CAN = stop; contactors = open setBatteryPause(true, true, true, false); delay(1000); ESP.restart(); }); // Initialize ElegantOTA init_ElegantOTA(); // Start server server.begin(); } String getConnectResultString(wl_status_t status) { switch (status) { case WL_CONNECTED: return "Connected"; case WL_NO_SHIELD: return "No shield"; case WL_IDLE_STATUS: return "Idle status"; case WL_NO_SSID_AVAIL: return "No SSID available"; case WL_SCAN_COMPLETED: return "Scan completed"; case WL_CONNECT_FAILED: return "Connect failed"; case WL_CONNECTION_LOST: return "Connection lost"; case WL_DISCONNECTED: return "Disconnected"; default: return "Unknown"; } } void ota_monitor() { if (ota_active && ota_timeout_timer.elapsed()) { // OTA timeout, try to restore can and clear the update event set_event(EVENT_OTA_UPDATE_TIMEOUT, 0); onOTAEnd(false); } } // Function to initialize ElegantOTA void init_ElegantOTA() { ElegantOTA.begin(&server); // Start ElegantOTA // ElegantOTA callbacks ElegantOTA.onStart(onOTAStart); ElegantOTA.onProgress(onOTAProgress); ElegantOTA.onEnd(onOTAEnd); } String get_firmware_info_processor(const String& var) { if (var == "X") { String content = ""; static JsonDocument doc; #ifdef HW_LILYGO doc["hardware"] = "LilyGo T-CAN485"; #endif // HW_LILYGO #ifdef HW_STARK doc["hardware"] = "Stark CMR Module"; #endif // HW_STARK doc["firmware"] = String(version_number); serializeJson(doc, content); return content; } return String(); } String processor(const String& var) { if (var == "X") { String content = ""; content += "

" + String(ssidAP) + "

"; // ssidAP name is used as header name //Page format content += ""; // Start a new block with a specific background color content += "
"; // Show version number content += "

Software: " + String(version_number) + "

"; // Show hardware used: #ifdef HW_LILYGO content += "

Hardware: LilyGo T-CAN485

"; #endif // HW_LILYGO #ifdef HW_STARK content += "

Hardware: Stark CMR Module

"; #endif // HW_STARK content += "

Uptime: " + uptime_formatter::getUptime() + "

"; #ifdef FUNCTION_TIME_MEASUREMENT // Load information content += "

Core task max load: " + String(datalayer.system.status.core_task_max_us) + " us

"; content += "

Core task max load last 10 s: " + String(datalayer.system.status.core_task_10s_max_us) + " us

"; content += "

MQTT function (MQTT task) max load last 10 s: " + String(datalayer.system.status.mqtt_task_10s_max_us) + " us

"; content += "

WIFI function (MQTT task) max load last 10 s: " + String(datalayer.system.status.wifi_task_10s_max_us) + " us

"; content += "

loop() task max load last 10 s: " + String(datalayer.system.status.loop_task_10s_max_us) + " us

"; content += "

Max load @ worst case execution of core task:

"; content += "

10ms function timing: " + String(datalayer.system.status.time_snap_10ms_us) + " us

"; content += "

Values function timing: " + String(datalayer.system.status.time_snap_values_us) + " us

"; content += "

CAN/serial RX function timing: " + String(datalayer.system.status.time_snap_comm_us) + " us

"; content += "

CAN TX function timing: " + String(datalayer.system.status.time_snap_cantx_us) + " us

"; content += "

OTA function timing: " + String(datalayer.system.status.time_snap_ota_us) + " us

"; #endif // FUNCTION_TIME_MEASUREMENT wl_status_t status = WiFi.status(); // Display ssid of network connected to and, if connected to the WiFi, its own IP content += "

SSID: " + String(ssid.c_str()) + "

"; if (status == WL_CONNECTED) { content += "

IP: " + WiFi.localIP().toString() + "

"; // Get and display the signal strength (RSSI) and channel content += "

Signal strength: " + String(WiFi.RSSI()) + " dBm, at channel " + String(WiFi.channel()) + "

"; } else { content += "

Wifi state: " + getConnectResultString(status) + "

"; } // Close the block content += "
"; // Start a new block with a specific background color content += "
"; // Display which components are used content += "

Inverter protocol: "; #ifdef BYD_CAN content += "BYD Battery-Box Premium HVS over CAN Bus"; #endif // BYD_CAN #ifdef BYD_MODBUS content += "BYD 11kWh HVM battery over Modbus RTU"; #endif // BYD_MODBUS #ifdef FOXESS_CAN content += "FoxESS compatible HV2600/ECS4100 battery"; #endif // FOXESS_CAN #ifdef PYLON_CAN content += "Pylontech battery over CAN bus"; #endif // PYLON_CAN #ifdef SERIAL_LINK_TRANSMITTER content += "Serial link to another LilyGo board"; #endif // SERIAL_LINK_TRANSMITTER #ifdef SMA_CAN content += "BYD Battery-Box H 8.9kWh, 7 mod over CAN bus"; #endif // SMA_CAN #ifdef SOFAR_CAN content += "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame) over CAN bus"; #endif // SOFAR_CAN #ifdef SOLAX_CAN content += "SolaX Triple Power LFP over CAN bus"; #endif // SOLAX_CAN content += "

"; content += "

Battery protocol: "; #ifdef BMW_I3_BATTERY content += "BMW i3"; #endif // BMW_I3_BATTERY #ifdef BYD_ATTO_3_BATTERY content += "BYD Atto 3"; #endif // BYD_ATTO_3_BATTERY #ifdef CELLPOWER_BMS content += "Cellpower BMS"; #endif // CELLPOWER_BMS #ifdef CHADEMO_BATTERY content += "Chademo V2X mode"; #endif // CHADEMO_BATTERY #ifdef IMIEV_CZERO_ION_BATTERY content += "I-Miev / C-Zero / Ion Triplet"; #endif // IMIEV_CZERO_ION_BATTERY #ifdef JAGUAR_IPACE_BATTERY content += "Jaguar I-PACE"; #endif // JAGUAR_IPACE_BATTERY #ifdef KIA_HYUNDAI_64_BATTERY content += "Kia/Hyundai 64kWh"; #endif // KIA_HYUNDAI_64_BATTERY #ifdef KIA_E_GMP_BATTERY content += "Kia/Hyundai EGMP platform"; #endif // KIA_E_GMP_BATTERY #ifdef KIA_HYUNDAI_HYBRID_BATTERY content += "Kia/Hyundai Hybrid"; #endif // KIA_HYUNDAI_HYBRID_BATTERY #ifdef MG_5_BATTERY content += "MG 5"; #endif // MG_5_BATTERY #ifdef NISSAN_LEAF_BATTERY content += "Nissan LEAF"; #endif // NISSAN_LEAF_BATTERY #ifdef PYLON_BATTERY content += "Pylon compatible battery"; #endif // PYLON_BATTERY #ifdef RJXZS_BMS content += "RJXZS BMS, DIY battery"; #endif // RJXZS_BMS #ifdef RENAULT_KANGOO_BATTERY content += "Renault Kangoo"; #endif // RENAULT_KANGOO_BATTERY #ifdef RENAULT_TWIZY_BATTERY content += "Renault Twizy"; #endif // RENAULT_TWIZY_BATTERY #ifdef RENAULT_ZOE_GEN1_BATTERY content += "Renault Zoe Gen1 22/40"; #endif // RENAULT_ZOE_GEN1_BATTERY #ifdef RENAULT_ZOE_GEN2_BATTERY content += "Renault Zoe Gen2 50"; #endif // RENAULT_ZOE_GEN2_BATTERY #ifdef SANTA_FE_PHEV_BATTERY content += "Santa Fe PHEV"; #endif // SANTA_FE_PHEV_BATTERY #ifdef SERIAL_LINK_RECEIVER content += "Serial link to another LilyGo board"; #endif // SERIAL_LINK_RECEIVER #ifdef TESLA_MODEL_SX_BATTERY content += "Tesla Model S/X"; #endif // TESLA_MODEL_SX_BATTERY #ifdef TESLA_MODEL_3Y_BATTERY content += "Tesla Model 3/Y"; #endif // TESLA_MODEL_3Y_BATTERY #ifdef VOLVO_SPA_BATTERY content += "Volvo / Polestar 78kWh battery"; #endif // VOLVO_SPA_BATTERY #ifdef TEST_FAKE_BATTERY content += "Fake battery for testing purposes"; #endif // TEST_FAKE_BATTERY #ifdef DOUBLE_BATTERY content += " (Double battery)"; if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) { content += " (LFP)"; } #endif // DOUBLE_BATTERY content += "

"; #if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER content += "

Charger protocol: "; #ifdef CHEVYVOLT_CHARGER content += "Chevy Volt Gen1 Charger"; #endif // CHEVYVOLT_CHARGER #ifdef NISSANLEAF_CHARGER content += "Nissan LEAF 2013-2024 PDM charger"; #endif // NISSANLEAF_CHARGER content += "

"; #endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Close the block content += "
"; #ifdef DOUBLE_BATTERY // Start a new block with a specific background color. Color changes depending on BMS status content += "
"; content += "
(datalayer.battery.status.real_soc) / 100.0; // Convert to float and divide by 100 float socScaledFloat = static_cast(datalayer.battery.status.reported_soc) / 100.0; // Convert to float and divide by 100 float sohFloat = static_cast(datalayer.battery.status.soh_pptt) / 100.0; // Convert to float and divide by 100 float voltageFloat = static_cast(datalayer.battery.status.voltage_dV) / 10.0; // Convert to float and divide by 10 float currentFloat = static_cast(datalayer.battery.status.current_dA) / 10.0; // Convert to float and divide by 10 float powerFloat = static_cast(datalayer.battery.status.active_power_W); // Convert to float float tempMaxFloat = static_cast(datalayer.battery.status.temperature_max_dC) / 10.0; // Convert to float float tempMinFloat = static_cast(datalayer.battery.status.temperature_min_dC) / 10.0; // Convert to float uint16_t cell_delta_mv = datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV; content += "

Real SOC: " + String(socRealFloat, 2) + "

"; content += "

Scaled SOC: " + String(socScaledFloat, 2) + "

"; content += "

SOH: " + String(sohFloat, 2) + "

"; content += "

Voltage: " + String(voltageFloat, 1) + " V

"; content += "

Current: " + String(currentFloat, 1) + " A

"; content += formatPowerValue("Power", powerFloat, "", 1); content += formatPowerValue("Total capacity", datalayer.battery.info.total_capacity_Wh, "h", 0); content += formatPowerValue("Real Remaining capacity", datalayer.battery.status.remaining_capacity_Wh, "h", 1); content += formatPowerValue("Scaled Remaining capacity", datalayer.battery.status.reported_remaining_capacity_Wh, "h", 1); if (emulator_pause_status == NORMAL) { content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1); content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1); } else { content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1, "red"); content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1, "red"); } content += "

Cell max: " + String(datalayer.battery.status.cell_max_voltage_mV) + " mV

"; content += "

Cell min: " + String(datalayer.battery.status.cell_min_voltage_mV) + " mV

"; if (cell_delta_mv > datalayer.battery.info.max_cell_voltage_deviation_mV) { content += "

Cell delta: " + String(cell_delta_mv) + " mV

"; } else { content += "

Cell delta: " + String(cell_delta_mv) + " mV

"; } content += "

Temperature max: " + String(tempMaxFloat, 1) + " C

"; content += "

Temperature min: " + String(tempMinFloat, 1) + " C

"; if (datalayer.battery.status.bms_status == ACTIVE) { content += "

System status: OK

"; } else if (datalayer.battery.status.bms_status == UPDATING) { content += "

System status: UPDATING

"; } else { content += "

System status: FAULT

"; } if (datalayer.battery.status.current_dA == 0) { content += "

Battery idle

"; } else if (datalayer.battery.status.current_dA < 0) { content += "

Battery discharging!

"; } else { // > 0 content += "

Battery charging!

"; } content += "

Automatic contactor closing allowed:

"; content += "

Battery: "; if (datalayer.system.status.battery_allows_contactor_closing == true) { content += ""; } else { content += ""; } content += " Inverter: "; if (datalayer.system.status.inverter_allows_contactor_closing == true) { content += "

"; } else { content += ""; } if (emulator_pause_status == NORMAL) content += "

Power status: " + String(get_emulator_pause_status().c_str()) + "

"; else content += "

Power status: " + String(get_emulator_pause_status().c_str()) + "

"; #ifdef CONTACTOR_CONTROL content += "

Contactors controlled by Battery-Emulator: "; if (datalayer.system.status.contactor_control_closed) { content += "ON"; } else { content += "OFF"; } content += "

"; content += "

Pre Charge: "; if (digitalRead(PRECHARGE_PIN) == HIGH) { content += ""; } else { content += ""; } content += " Cont. Neg.: "; if (digitalRead(NEGATIVE_CONTACTOR_PIN) == HIGH) { content += ""; } else { content += ""; } content += " Cont. Pos.: "; if (digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH) { content += ""; } else { content += ""; } content += "

"; #endif // Close the block content += "
"; #ifdef DOUBLE_BATTERY content += "
"; // Display battery statistics within this block socRealFloat = static_cast(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100 //socScaledFloat; // Same value used for bat2 sohFloat = static_cast(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100 voltageFloat = static_cast(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10 currentFloat = static_cast(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10 powerFloat = static_cast(datalayer.battery2.status.active_power_W); // Convert to float tempMaxFloat = static_cast(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float tempMinFloat = static_cast(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float cell_delta_mv = datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV; content += "

Real SOC: " + String(socRealFloat, 2) + "

"; content += "

Scaled SOC: " + String(socScaledFloat, 2) + "

"; content += "

SOH: " + String(sohFloat, 2) + "

"; content += "

Voltage: " + String(voltageFloat, 1) + " V

"; content += "

Current: " + String(currentFloat, 1) + " A

"; content += formatPowerValue("Power", powerFloat, "", 1); content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 0); content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1); content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1); content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1); content += "

Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV

"; content += "

Cell min: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV

"; if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) { content += "

Cell delta: " + String(cell_delta_mv) + " mV

"; } else { content += "

Cell delta: " + String(cell_delta_mv) + " mV

"; } content += "

Temperature max: " + String(tempMaxFloat, 1) + " C

"; content += "

Temperature min: " + String(tempMinFloat, 1) + " C

"; if (datalayer.battery.status.bms_status == ACTIVE) { content += "

System status: OK

"; } else if (datalayer.battery.status.bms_status == UPDATING) { content += "

System status: UPDATING

"; } else { content += "

System status: FAULT

"; } if (datalayer.battery2.status.current_dA == 0) { content += "

Battery idle

"; } else if (datalayer.battery2.status.current_dA < 0) { content += "

Battery discharging!

"; } else { // > 0 content += "

Battery charging!

"; } content += "

Automatic contactor closing allowed:

"; content += "

Battery: "; if (datalayer.system.status.battery2_allows_contactor_closing == true) { content += ""; } else { content += ""; } content += " Inverter: "; if (datalayer.system.status.inverter_allows_contactor_closing == true) { content += "

"; } else { content += ""; } #ifdef CONTACTOR_CONTROL content += "

Contactors controlled by Battery-Emulator: "; if (datalayer.system.status.contactor_control_closed) { content += "ON"; } else { content += "OFF"; } content += "

"; #endif if (emulator_pause_status == NORMAL) content += "

Pause status: " + String(get_emulator_pause_status().c_str()) + "

"; else content += "

Pause status: " + String(get_emulator_pause_status().c_str()) + "

"; content += "
"; content += "
"; #endif // DOUBLE_BATTERY #if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Start a new block with orange background color content += "
"; content += "

Charger HV Enabled: "; if (charger_HV_enabled) { content += ""; } else { content += ""; } content += "

"; content += "

Charger Aux12v Enabled: "; if (charger_aux12V_enabled) { content += ""; } else { content += ""; } content += "

"; #ifdef CHEVYVOLT_CHARGER float chgPwrDC = static_cast(charger_stat_HVcur * charger_stat_HVvol); float chgPwrAC = static_cast(charger_stat_ACcur * charger_stat_ACvol); float chgEff = chgPwrDC / chgPwrAC * 100; float ACcur = charger_stat_ACcur; float ACvol = charger_stat_ACvol; float HVvol = charger_stat_HVvol; float HVcur = charger_stat_HVcur; float LVvol = charger_stat_LVvol; float LVcur = charger_stat_LVcur; content += formatPowerValue("Charger Output Power", chgPwrDC, "", 1); content += "

Charger Efficiency: " + String(chgEff) + "%

"; content += "

Charger HVDC Output V: " + String(HVvol, 2) + " V

"; content += "

Charger HVDC Output I: " + String(HVcur, 2) + " A

"; content += "

Charger LVDC Output I: " + String(LVcur, 2) + "

"; content += "

Charger LVDC Output V: " + String(LVvol, 2) + "

"; content += "

Charger AC Input V: " + String(ACvol, 2) + " VAC

"; content += "

Charger AC Input I: " + String(ACcur, 2) + " A

"; #endif // CHEVYVOLT_CHARGER #ifdef NISSANLEAF_CHARGER float chgPwrDC = static_cast(charger_stat_HVcur * 100); charger_stat_HVcur = chgPwrDC / (datalayer.battery.status.voltage_dV / 10); // P/U=I charger_stat_HVvol = static_cast(datalayer.battery.status.voltage_dV / 10); float ACvol = charger_stat_ACvol; float HVvol = charger_stat_HVvol; float HVcur = charger_stat_HVcur; content += formatPowerValue("Charger Output Power", chgPwrDC, "", 1); content += "

Charger HVDC Output V: " + String(HVvol, 2) + " V

"; content += "

Charger HVDC Output I: " + String(HVcur, 2) + " A

"; content += "

Charger AC Input V: " + String(ACvol, 2) + " VAC

"; #endif // NISSANLEAF_CHARGER // Close the block content += "
"; #endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER if (emulator_pause_request_ON) content += " "; else content += " "; content += " "; content += " "; content += " "; content += " "; content += " "; content += ""; if (WEBSERVER_AUTH_REQUIRED) content += ""; if (!datalayer.system.settings.equipment_stop_active) content += "


"; else content += "


"; content += ""; //Script for refreshing page content += ""; return content; } return String(); } void onOTAStart() { //try to Pause the battery setBatteryPause(true, true); // Log when OTA has started set_event(EVENT_OTA_UPDATE, 0); // If already set, make a new attempt clear_event(EVENT_OTA_UPDATE_TIMEOUT); ota_active = true; ota_timeout_timer.reset(); } void onOTAProgress(size_t current, size_t final) { // Log every 1 second if (millis() - ota_progress_millis > 1000) { ota_progress_millis = millis(); #ifdef DEBUG_VIA_USB Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final); #endif // DEBUG_VIA_USB // Reset the "watchdog" ota_timeout_timer.reset(); } } void onOTAEnd(bool success) { ota_active = false; clear_event(EVENT_OTA_UPDATE); // Log when OTA has finished if (success) { //Equipment STOP without persisting the equipment state before restart // Max Charge/Discharge = 0; CAN = stop; contactors = open setBatteryPause(true, true, true, false); // a reboot will be done by the OTA library. no need to do anything here #ifdef DEBUG_VIA_USB Serial.println("OTA update finished successfully!"); #endif // DEBUG_VIA_USB } else { #ifdef DEBUG_VIA_USB Serial.println("There was an error during OTA update!"); #endif // DEBUG_VIA_USB //try to Resume the battery pause and CAN communication setBatteryPause(false, false); } } template // This function makes power values appear as W when under 1000, and kW when over String formatPowerValue(String label, T value, String unit, int precision, String color) { String result = "

" + label + ": "; if (std::is_same::value || std::is_same::value || std::is_same::value) { float convertedValue = static_cast(value); if (convertedValue >= 1000.0 || convertedValue <= -1000.0) { result += String(convertedValue / 1000.0, precision) + " kW"; } else { result += String(convertedValue, 0) + " W"; } } result += unit + "

"; return result; }