mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 17:59:27 +02:00
Merge pull request #718 from dalathegreat/feature/tesla-balancing
Feature: Add forced Tesla LFP balancing functionality
This commit is contained in:
commit
64e633f10f
8 changed files with 225 additions and 7 deletions
|
@ -901,6 +901,16 @@ void update_values_battery() { //This function maps all the values fetched via
|
|||
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
|
||||
}
|
||||
|
||||
// During forced balancing request via webserver, we allow the battery to exceed normal safety parameters
|
||||
if (datalayer.battery.settings.user_requests_balancing) {
|
||||
datalayer.battery.status.real_soc = 9900; //Force battery to show up as 99% when balancing
|
||||
datalayer.battery.info.max_design_voltage_dV = datalayer.battery.settings.balancing_max_pack_voltage_dV;
|
||||
datalayer.battery.info.max_cell_voltage_mV = datalayer.battery.settings.balancing_max_cell_voltage_mV;
|
||||
datalayer.battery.info.max_cell_voltage_deviation_mV =
|
||||
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV;
|
||||
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.balancing_float_power_W;
|
||||
}
|
||||
#endif // TESLA_MODEL_3Y_BATTERY
|
||||
|
||||
// Update webserver datalayer
|
||||
|
|
|
@ -10,10 +10,11 @@
|
|||
#define MAXDISCHARGEPOWERALLOWED 60000 // 60000W we use a define since the value supplied by Tesla is always 0
|
||||
|
||||
/* Do not change the defines below */
|
||||
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
|
||||
#define RAMPDOWNPOWERALLOWED 15000 // What power we ramp down from towards top balancing
|
||||
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
|
||||
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
|
||||
#define RAMPDOWN_SOC 900 // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
|
||||
#define RAMPDOWNPOWERALLOWED \
|
||||
15000 // What power we ramp down from towards top balancing (usually same as MAXCHARGEPOWERALLOWED)
|
||||
#define FLOAT_MAX_POWER_W 200 // W, what power to allow for top balancing battery
|
||||
#define FLOAT_START_MV 20 // mV, how many mV under overvoltage to start float charging
|
||||
|
||||
#define MAX_PACK_VOLTAGE_SX_NCMA 4600 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_SX_NCMA 3100 // V+1, if pack voltage goes over this, charge stops
|
||||
|
@ -22,10 +23,10 @@
|
|||
#define MAX_PACK_VOLTAGE_3Y_LFP 3880 // V+1, if pack voltage goes over this, charge stops
|
||||
#define MIN_PACK_VOLTAGE_3Y_LFP 2968 // V+1, if pack voltage goes below this, discharge stops
|
||||
#define MAX_CELL_DEVIATION_NCA_NCM 500 //LED turns yellow on the board if mv delta exceeds this value
|
||||
#define MAX_CELL_DEVIATION_LFP 200 //LED turns yellow on the board if mv delta exceeds this value
|
||||
#define MAX_CELL_DEVIATION_LFP 400 //LED turns yellow on the board if mv delta exceeds this value
|
||||
#define MAX_CELL_VOLTAGE_NCA_NCM 4250 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_NCA_NCM 2950 //Battery is put into emergency stop if one cell goes below this value
|
||||
#define MAX_CELL_VOLTAGE_LFP 3550 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MAX_CELL_VOLTAGE_LFP 3650 //Battery is put into emergency stop if one cell goes over this value
|
||||
#define MIN_CELL_VOLTAGE_LFP 2800 //Battery is put into emergency stop if one cell goes below this value
|
||||
|
||||
//#define EXP_TESLA_BMS_DIGITAL_HVIL // Experimental parameter. Forces the transmission of additional CAN frames for experimental purposes, to test potential HVIL issues in 3/Y packs with newer firmware.
|
||||
|
|
|
@ -121,6 +121,21 @@ typedef struct {
|
|||
/** The user specified maximum allowed discharge voltage, in deciVolt. 3000 = 300.0 V */
|
||||
uint16_t max_user_set_discharge_voltage_dV = BATTERY_MAX_DISCHARGE_VOLTAGE;
|
||||
|
||||
/** Tesla specific settings that are edited on the fly when manually forcing a balance charge for LFP chemistry */
|
||||
/* Bool for specifying if user has requested manual balancing */
|
||||
bool user_requests_balancing = false;
|
||||
/* Forced balancing max time & start timestamp */
|
||||
uint32_t balancing_time_ms = 3600000; //1h default, (60min*60sec*1000ms)
|
||||
uint32_t balancing_start_time_ms = 0; //For keeping track when balancing started
|
||||
/* Max cell voltage during forced balancing */
|
||||
uint16_t balancing_max_cell_voltage_mV = 3650;
|
||||
/* Max cell deviation allowed during forced balancing */
|
||||
uint16_t balancing_max_deviation_cell_voltage_mV = 400;
|
||||
/* Float max power during forced balancing */
|
||||
uint16_t balancing_float_power_W = 1000;
|
||||
/* Maximum voltage for entire battery pack during forced balancing */
|
||||
uint16_t balancing_max_pack_voltage_dV = 3940;
|
||||
|
||||
} DATALAYER_BATTERY_SETTINGS_TYPE;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
@ -238,6 +238,26 @@ void update_machineryprotection() {
|
|||
if (datalayer.battery.status.max_charge_power_W == 0) {
|
||||
datalayer.battery.status.max_charge_current_dA = 0;
|
||||
}
|
||||
|
||||
//Decrement the forced balancing timer incase user requested it
|
||||
if (datalayer.battery.settings.user_requests_balancing) {
|
||||
// If this is the start of the balancing period, capture the current time
|
||||
if (datalayer.battery.settings.balancing_start_time_ms == 0) {
|
||||
datalayer.battery.settings.balancing_start_time_ms = millis();
|
||||
set_event(EVENT_BALANCING_START, 0);
|
||||
} else {
|
||||
clear_event(EVENT_BALANCING_START);
|
||||
}
|
||||
|
||||
// Check if the elapsed time exceeds the balancing time
|
||||
if (millis() - datalayer.battery.settings.balancing_start_time_ms >= datalayer.battery.settings.balancing_time_ms) {
|
||||
datalayer.battery.settings.user_requests_balancing = false;
|
||||
datalayer.battery.settings.balancing_start_time_ms = 0; // Reset the start time
|
||||
set_event(EVENT_BALANCING_END, 0);
|
||||
} else {
|
||||
clear_event(EVENT_BALANCING_END);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//battery pause status begin
|
||||
|
|
|
@ -159,6 +159,8 @@ void init_events(void) {
|
|||
events.entries[EVENT_SOC_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_SOC_UNAVAILABLE].level = EVENT_LEVEL_WARNING;
|
||||
events.entries[EVENT_KWH_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BALANCING_START].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BALANCING_END].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BATTERY_EMPTY].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BATTERY_FULL].level = EVENT_LEVEL_INFO;
|
||||
events.entries[EVENT_BATTERY_FROZEN].level = EVENT_LEVEL_INFO;
|
||||
|
@ -302,6 +304,10 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
|
|||
return "Warning: SOC not sent by BMS. Calibrate BMS via app.";
|
||||
case EVENT_KWH_PLAUSIBILITY_ERROR:
|
||||
return "Info: kWh remaining reported by battery not plausible. Battery needs cycling.";
|
||||
case EVENT_BALANCING_START:
|
||||
return "Info: Balancing has started";
|
||||
case EVENT_BALANCING_END:
|
||||
return "Info: Balancing has ended";
|
||||
case EVENT_BATTERY_EMPTY:
|
||||
return "Info: Battery is completely discharged";
|
||||
case EVENT_BATTERY_FULL:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
|
||||
|
||||
#define EE_MAGIC_HEADER_VALUE 0x0018 // 0x0000 to 0xFFFF
|
||||
#define EE_MAGIC_HEADER_VALUE 0x0019 // 0x0000 to 0xFFFF
|
||||
|
||||
#define GENERATE_ENUM(ENUM) ENUM,
|
||||
#define GENERATE_STRING(STRING) #STRING,
|
||||
|
@ -45,6 +45,8 @@
|
|||
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
|
||||
XX(EVENT_SOC_UNAVAILABLE) \
|
||||
XX(EVENT_KWH_PLAUSIBILITY_ERROR) \
|
||||
XX(EVENT_BALANCING_START) \
|
||||
XX(EVENT_BALANCING_END) \
|
||||
XX(EVENT_BATTERY_EMPTY) \
|
||||
XX(EVENT_BATTERY_FULL) \
|
||||
XX(EVENT_BATTERY_FROZEN) \
|
||||
|
|
|
@ -102,6 +102,42 @@ String settings_processor(const String& var) {
|
|||
content += "</div>";
|
||||
#endif
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
|
||||
// Start a new block with grey background color
|
||||
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
|
||||
|
||||
content +=
|
||||
"<h4 style='color: white;'>Manual LFP balancing: <span id='TSL_BAL_ACT'>" +
|
||||
String(datalayer.battery.settings.user_requests_balancing ? "<span>✓</span>"
|
||||
: "<span style='color: red;'>✕</span>") +
|
||||
"</span> <button onclick='editTeslaBalAct()'>Edit</button></h4>";
|
||||
content +=
|
||||
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
|
||||
";'>Balancing max time: " + String(datalayer.battery.settings.balancing_time_ms / 60000.0, 1) +
|
||||
" Minutes </span> <button onclick='editBalTime()'>Edit</button></h4>";
|
||||
content +=
|
||||
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
|
||||
";'>Balancing float power: " + String(datalayer.battery.settings.balancing_float_power_W / 1.0, 0) +
|
||||
" W </span> <button onclick='editBalFloatPower()'>Edit</button></h4>";
|
||||
content +=
|
||||
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
|
||||
";'>Max battery voltage: " + String(datalayer.battery.settings.balancing_max_pack_voltage_dV / 10.0, 0) +
|
||||
" V </span> <button onclick='editBalMaxPackV()'>Edit</button></h4>";
|
||||
content +=
|
||||
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
|
||||
";'>Max cell voltage: " + String(datalayer.battery.settings.balancing_max_cell_voltage_mV / 1.0, 0) +
|
||||
" mV </span> <button onclick='editBalMaxCellV()'>Edit</button></h4>";
|
||||
content +=
|
||||
"<h4 style='color: " + String(datalayer.battery.settings.user_requests_balancing ? "white" : "darkgrey") +
|
||||
";'>Max cell voltage deviation: " +
|
||||
String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0) +
|
||||
" mV </span> <button onclick='editBalMaxDevCellV()'>Edit</button></h4>";
|
||||
|
||||
// Close the block
|
||||
content += "</div>";
|
||||
#endif
|
||||
|
||||
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
|
||||
|
||||
// Start a new block with orange background color
|
||||
|
@ -211,6 +247,47 @@ String settings_processor(const String& var) {
|
|||
"between 0 "
|
||||
"and 1000.0');}}}";
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
content +=
|
||||
"function editTeslaBalAct(){var value=prompt('Enable or disable forced LFP balancing. Makes the battery charge "
|
||||
"to 101percent. This should be performed once every month, to keep LFP batteries balanced. Ensure battery is "
|
||||
"fully charged before enabling, and also that you have enough sun or grid power to feed power into the battery "
|
||||
"while balancing is active. Enter 1 for enabled, 0 "
|
||||
"for disabled');if(value!==null){if(value==0||value==1){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"TeslaBalAct?value='+value,true);xhr.send();}}else{alert('Invalid value. Please enter 1 or 0');}}";
|
||||
content +=
|
||||
"function editBalTime(){var value=prompt('Enter new max balancing time in "
|
||||
"minutes');if(value!==null){if(value>=1&&value<=300){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"BalTime?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
|
||||
"between 1 and 300');}}}";
|
||||
content +=
|
||||
"function editBalFloatPower(){var value=prompt('Power level in Watt to float charge during forced "
|
||||
"balancing');if(value!==null){if(value>=100&&value<=2000){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"BalFloatPower?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
|
||||
"between 100 and 2000');}}}";
|
||||
content +=
|
||||
"function editBalMaxPackV(){var value=prompt('Battery pack max voltage temporarily raised to this value during "
|
||||
"forced balancing. Value in V');if(value!==null){if(value>=380&&value<=410){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"BalMaxPackV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
|
||||
"between 380 and 410');}}}";
|
||||
content +=
|
||||
"function editBalMaxCellV(){var value=prompt('Cellvoltage max temporarily raised to this value during forced "
|
||||
"balancing. Value in mV');if(value!==null){if(value>=3400&&value<=3750){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"BalMaxCellV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
|
||||
"between 3400 and 3750');}}}";
|
||||
content +=
|
||||
"function editBalMaxDevCellV(){var value=prompt('Cellvoltage max deviation temporarily raised to this value "
|
||||
"during forced balancing. Value in mV');if(value!==null){if(value>=300&&value<=600){var xhr=new "
|
||||
"XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/"
|
||||
"BalMaxDevCellV?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value "
|
||||
"between 300 and 600');}}}";
|
||||
#endif
|
||||
|
||||
#ifdef TEST_FAKE_BATTERY
|
||||
content +=
|
||||
"function editFakeBatteryVoltage(){var value=prompt('Enter new fake battery "
|
||||
|
|
|
@ -439,6 +439,93 @@ void init_webserver() {
|
|||
});
|
||||
#endif // TEST_FAKE_BATTERY
|
||||
|
||||
#ifdef TESLA_MODEL_3Y_BATTERY
|
||||
|
||||
// Route for editing balancing enabled
|
||||
server.on("/TeslaBalAct", 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.user_requests_balancing = value.toInt();
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing balancing max time
|
||||
server.on("/BalTime", 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.balancing_time_ms = static_cast<uint32_t>(value.toFloat() * 60000);
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing balancing max power
|
||||
server.on("/BalFloatPower", 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.balancing_float_power_W = static_cast<uint16_t>(value.toFloat());
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing balancing max pack voltage
|
||||
server.on("/BalMaxPackV", 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.balancing_max_pack_voltage_dV = static_cast<uint16_t>(value.toFloat() * 10);
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing balancing max cell voltage
|
||||
server.on("/BalMaxCellV", 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.balancing_max_cell_voltage_mV = static_cast<uint16_t>(value.toFloat());
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
|
||||
// Route for editing balancing max cell voltage deviation
|
||||
server.on("/BalMaxDevCellV", 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.balancing_max_deviation_cell_voltage_mV = static_cast<uint16_t>(value.toFloat());
|
||||
store_settings();
|
||||
request->send(200, "text/plain", "Updated successfully");
|
||||
} else {
|
||||
request->send(400, "text/plain", "Bad Request");
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
|
||||
// Route for editing ChargerTargetV
|
||||
server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue