Merge pull request #718 from dalathegreat/feature/tesla-balancing

Feature: Add forced Tesla LFP balancing functionality
This commit is contained in:
Daniel Öster 2025-01-07 18:44:18 +03:00 committed by GitHub
commit 64e633f10f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 225 additions and 7 deletions

View file

@ -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

View file

@ -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.

View file

@ -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 {

View file

@ -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

View file

@ -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:

View file

@ -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) \

View file

@ -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>&#10003;</span>"
: "<span style='color: red;'>&#10005;</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 "

View file

@ -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) {