Batteries implement HTML renderers for status and commands

This commit is contained in:
Jaakko Haakana 2025-06-01 09:59:46 +03:00
parent d1fb28337e
commit 662ae57d2b
48 changed files with 2345 additions and 2020 deletions

View file

@ -2,6 +2,8 @@
#include <Preferences.h>
#include <ctime>
#include "../../../USER_SECRETS.h"
#include "../../battery/BATTERIES.h"
#include "../../battery/Battery.h"
#include "../../datalayer/datalayer.h"
#include "../../datalayer/datalayer_extended.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
@ -530,6 +532,32 @@ void init_webserver() {
}
});
for (const auto& cmd : battery_commands) {
auto route = String("/") + cmd.identifier;
server.on(
route.c_str(), HTTP_PUT,
[cmd](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
},
nullptr,
[cmd](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
String battIndex = "";
if (len > 0) {
battIndex += (char)data[0];
}
Battery* batt = battery;
if (battIndex == "1") {
batt = battery2;
}
if (batt) {
cmd.action(batt);
}
request->send(200, "text/plain", "Command performed.");
});
}
// Route for editing BATTERY_USE_VOLTAGE_LIMITS
server.on("/updateUseVoltageLimit", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
@ -572,102 +600,8 @@ void init_webserver() {
}
});
// Route for clearing isolation faults on Tesla
server.on("/teslaClearIsolation", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer.battery.settings.user_requests_tesla_isolation_clear = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for resetting BMS on Tesla
server.on("/teslaResetBMS", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer.battery.settings.user_requests_tesla_bms_reset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for triggering NVROL reset on Zoe Gen2 batteries
server.on("/triggerNVROL", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.zoePH2.UserRequestNVROLReset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for closing BMW iX Contactors
server.on("/bmwIxCloseContactorRequest", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.bmwix.UserRequestContactorClose = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for opening BMW iX Contactors
server.on("/bmwIxOpenContactorRequest", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.bmwix.UserRequestContactorOpen = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for resetting SOH on Nissan LEAF batteries
server.on("/resetSOH", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.nissanleaf.UserRequestSOHreset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for resetting Crash data on BYD Atto3 batteries
server.on("/resetCrash", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.bydAtto3.UserRequestCrashReset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for erasing DTC on Volvo/Polestar batteries
server.on("/volvoEraseDTC", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestDTCreset = true;
datalayer_extended.VolvoHybrid.UserRequestDTCreset = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for reading DTC on Volvo/Polestar batteries
server.on("/volvoReadDTC", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestDTCreadout = true;
datalayer_extended.VolvoHybrid.UserRequestDTCreadout = true;
request->send(200, "text/plain", "Updated successfully");
});
// Route for performing ECU reset on Volvo/Polestar batteries
server.on("/volvoBECMecuReset", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication();
}
datalayer_extended.VolvoPolestar.UserRequestBECMecuReset = true;
datalayer_extended.VolvoHybrid.UserRequestBECMecuReset = true;
request->send(200, "text/plain", "Updated successfully");
});
#ifdef TEST_FAKE_BATTERY
// Route for editing FakeBatteryVoltage
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
server.on("/updateBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (!request->hasParam("value")) {
@ -677,13 +611,10 @@ void init_webserver() {
String value = request->getParam("value")->value();
float val = value.toFloat();
datalayer.battery.status.voltage_dV = val * 10;
battery->set_voltage(val);
request->send(200, "text/plain", "Updated successfully");
});
#endif // TEST_FAKE_BATTERY
#ifdef TESLA_MODEL_3Y_BATTERY
// Route for editing balancing enabled
server.on("/TeslaBalAct", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -768,7 +699,6 @@ void init_webserver() {
request->send(400, "text/plain", "Bad Request");
}
});
#endif
if (charger) {
// Route for editing ChargerTargetV
@ -1022,9 +952,9 @@ String processor(const String& var) {
content += "</h4>";
content += "<h4 style='color: white;'>Battery protocol: ";
content += datalayer.system.info.battery_protocol;
#ifdef DOUBLE_BATTERY
content += " (Double battery)";
#endif // DOUBLE_BATTERY
if (battery2) {
content += " (Double battery)";
}
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
content += " (LFP)";
}
@ -1045,14 +975,14 @@ String processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
// Start a new block with a specific background color. Color changes depending on BMS status
content += "<div style='display: flex; width: 100%;'>";
content += "<div style='flex: 1; background-color: ";
#else
// Start a new block with a specific background color. Color changes depending on system status
content += "<div style='background-color: ";
#endif // DOUBLE_BATTERY
if (battery2) {
// Start a new block with a specific background color. Color changes depending on BMS status
content += "<div style='display: flex; width: 100%;'>";
content += "<div style='flex: 1; background-color: ";
} else {
// Start a new block with a specific background color. Color changes depending on system status
content += "<div style='background-color: ";
}
switch (led_get_color()) {
case led_color::GREEN:
@ -1176,27 +1106,27 @@ String processor(const String& var) {
}
content += "</h4>";
#ifdef MEB_BATTERY
content += "<h4>Battery BMS status: ";
switch (datalayer.battery.status.real_bms_status) {
case BMS_ACTIVE:
content += String("OK");
break;
case BMS_FAULT:
content += String("FAULT");
break;
case BMS_DISCONNECTED:
content += String("DISCONNECTED");
break;
case BMS_STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
if (battery->supports_real_BMS_status()) {
content += "<h4>Battery BMS status: ";
switch (datalayer.battery.status.real_bms_status) {
case BMS_ACTIVE:
content += String("OK");
break;
case BMS_FAULT:
content += String("FAULT");
break;
case BMS_DISCONNECTED:
content += String("DISCONNECTED");
break;
case BMS_STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
}
content += "</h4>";
}
content += "</h4>";
#endif
if (datalayer.battery.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
@ -1286,159 +1216,161 @@ String processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += "#2D3F2F;";
break;
case FAULT:
content += "#A70107;";
break;
default:
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
if (battery2) {
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += "#2D3F2F;";
break;
case FAULT:
content += "#A70107;";
break;
default:
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
// Display battery statistics within this block
socRealFloat =
static_cast<float>(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100
//socScaledFloat; // Same value used for bat2
sohFloat = static_cast<float>(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100
voltageFloat =
static_cast<float>(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10
currentFloat =
static_cast<float>(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(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;
// Display battery statistics within this block
socRealFloat =
static_cast<float>(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100
//socScaledFloat; // Same value used for bat2
sohFloat = static_cast<float>(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100
voltageFloat =
static_cast<float>(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10
currentFloat =
static_cast<float>(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(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;
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) +
"&percnt; (real: " + String(socRealFloat, 2) + "&percnt;)</h4>";
else
content += "<h4 style='color: white;'>SOC: " + String(socRealFloat, 2) + "&percnt;</h4>";
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) +
"&percnt; (real: " + String(socRealFloat, 2) + "&percnt;)</h4>";
else
content += "<h4 style='color: white;'>SOC: " + String(socRealFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>SOH: " + String(sohFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>Voltage: " + String(voltageFloat, 1) +
" V &nbsp; Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1);
content += "<h4 style='color: white;'>SOH: " + String(sohFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>Voltage: " + String(voltageFloat, 1) +
" V &nbsp; Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled total capacity: " +
formatPowerValue(datalayer.battery2.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.info.total_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled total capacity: " +
formatPowerValue(datalayer.battery2.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.info.total_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled remaining capacity: " +
formatPowerValue(datalayer.battery2.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled remaining capacity: " +
formatPowerValue(datalayer.battery2.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
if (datalayer.system.settings.equipment_stop_active) {
content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1, "red");
content += "<h4 style='color: red;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: red;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
} else {
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 += "<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
}
if (datalayer.system.settings.equipment_stop_active) {
content +=
formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1, "red");
content += "<h4 style='color: red;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: red;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
} else {
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 +=
"<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
}
content += "<h4>Cell min/max: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content +=
"<h4>Temperature min/max: " + String(tempMinFloat, 1) + " &deg;C / " + String(tempMaxFloat, 1) + " &deg;C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
content += "<h4>System status: OK </h4>";
} else if (datalayer.battery.status.bms_status == UPDATING) {
content += "<h4>System status: UPDATING </h4>";
} else {
content += "<h4>System status: FAULT </h4>";
}
if (datalayer.battery2.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery2.status.current_dA < 0) {
content += "<h4>Battery discharging!</h4>";
} else { // > 0
content += "<h4>Battery charging!</h4>";
}
content += "<h4>Cell min/max: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content += "<h4>Temperature min/max: " + String(tempMinFloat, 1) + " &deg;C / " + String(tempMaxFloat, 1) +
" &deg;C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
content += "<h4>System status: OK </h4>";
} else if (datalayer.battery.status.bms_status == UPDATING) {
content += "<h4>System status: UPDATING </h4>";
} else {
content += "<h4>System status: FAULT </h4>";
}
if (datalayer.battery2.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery2.status.current_dA < 0) {
content += "<h4>Battery discharging!</h4>";
} else { // > 0
content += "<h4>Battery charging!</h4>";
}
content += "<h4>Automatic contactor closing allowed:</h4>";
content += "<h4>Battery: ";
if (datalayer.system.status.battery2_allowed_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += "<h4>Automatic contactor closing allowed:</h4>";
content += "<h4>Battery: ";
if (datalayer.system.status.battery2_allowed_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Inverter: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
content += " Inverter: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
#ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
content += "<h4>Cont. Neg.: ";
content += "<h4>Cont. Neg.: ";
#ifdef PWM_CONTACTOR_CONTROL
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
}
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
}
#else // No PWM_CONTACTOR_CONTROL , we can read the pin and see feedback. Helpful if channel overloaded
if (digitalRead(SECOND_NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
if (digitalRead(SECOND_NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(SECOND_POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(SECOND_POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
#endif //no PWM_CONTACTOR_CONTROL
content += "</h4>";
content += "</h4>";
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
#endif // CONTACTOR_CONTROL
content += "</div>";
content += "</div>";
#endif // DOUBLE_BATTERY
content += "</div>";
content += "</div>";
}
if (charger) {
// Start a new block with orange background color