From cc6761a8a50ab439f65604c245d23117066476ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 31 Dec 2024 00:39:05 +0200 Subject: [PATCH 1/6] Add Tesla LFP balancing feature --- Software/src/battery/TESLA-BATTERY.cpp | 9 ++ Software/src/battery/TESLA-BATTERY.h | 9 +- Software/src/datalayer/datalayer.h | 15 ++++ Software/src/devboard/safety/safety.cpp | 16 ++++ .../src/devboard/webserver/settings_html.cpp | 77 ++++++++++++++++ Software/src/devboard/webserver/webserver.cpp | 87 +++++++++++++++++++ 6 files changed, 209 insertions(+), 4 deletions(-) diff --git a/Software/src/battery/TESLA-BATTERY.cpp b/Software/src/battery/TESLA-BATTERY.cpp index 8644405b4..70dbbdb25 100644 --- a/Software/src/battery/TESLA-BATTERY.cpp +++ b/Software/src/battery/TESLA-BATTERY.cpp @@ -901,6 +901,15 @@ 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.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 diff --git a/Software/src/battery/TESLA-BATTERY.h b/Software/src/battery/TESLA-BATTERY.h index 817ca1f6b..4a8438387 100644 --- a/Software/src/battery/TESLA-BATTERY.h +++ b/Software/src/battery/TESLA-BATTERY.h @@ -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 diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index 6064aa153..9a484a7a7 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -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 { diff --git a/Software/src/devboard/safety/safety.cpp b/Software/src/devboard/safety/safety.cpp index f662adfe9..35d285798 100644 --- a/Software/src/devboard/safety/safety.cpp +++ b/Software/src/devboard/safety/safety.cpp @@ -238,6 +238,22 @@ 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(); + //TODO, raise info event? Forced balancing starting! + } + + // 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 + //TODO, raise info event? Balancing time elapsed. Turning off... + } + } } //battery pause status begin diff --git a/Software/src/devboard/webserver/settings_html.cpp b/Software/src/devboard/webserver/settings_html.cpp index e21ec7229..a576ef387 100644 --- a/Software/src/devboard/webserver/settings_html.cpp +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -97,6 +97,42 @@ String settings_processor(const String& var) { content += ""; #endif +#ifdef TESLA_MODEL_3Y_BATTERY + + // Start a new block with grey background color + content += "
"; + + content += + "

Manual LFP balancing: " + + String(datalayer.battery.settings.user_requests_balancing ? "" + : "") + + "

"; + content += + "

Balancing max time: " + String(datalayer.battery.settings.balancing_time_ms / 60000.0, 1) + + " Minutes

"; + content += + "

Balancing float power: " + String(datalayer.battery.settings.balancing_float_power_W / 1.0, 0) + + " W

"; + content += + "

Max battery voltage: " + String(datalayer.battery.settings.balancing_max_pack_voltage_dV / 10.0, 0) + + " V

"; + content += + "

Max cell voltage: " + String(datalayer.battery.settings.balancing_max_cell_voltage_mV / 1.0, 0) + + " mV

"; + content += + "

Max cell voltage deviation: " + + String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0) + + " mV

"; + + // Close the block + content += "
"; +#endif + #if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Start a new block with orange background color @@ -206,6 +242,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 " diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 88e42481a..085244b83 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -395,6 +395,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(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(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(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(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(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) { From 5ec554708e81b6cf4a453cb047ab3157b14adef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 31 Dec 2024 00:47:57 +0200 Subject: [PATCH 2/6] Force SOC to 99pct during balancing --- Software/src/battery/TESLA-BATTERY.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Software/src/battery/TESLA-BATTERY.cpp b/Software/src/battery/TESLA-BATTERY.cpp index 70dbbdb25..eca9da8fd 100644 --- a/Software/src/battery/TESLA-BATTERY.cpp +++ b/Software/src/battery/TESLA-BATTERY.cpp @@ -904,6 +904,7 @@ void update_values_battery() { //This function maps all the values fetched via // 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 = From a60a1c3b5c8c7d107c9ae61ad4147b699d4462c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 7 Jan 2025 16:01:17 +0200 Subject: [PATCH 3/6] Add balancing start/end event --- Software/src/devboard/safety/safety.cpp | 8 ++++++-- Software/src/devboard/utils/events.cpp | 6 ++++++ Software/src/devboard/utils/events.h | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Software/src/devboard/safety/safety.cpp b/Software/src/devboard/safety/safety.cpp index 35d285798..ca69e5fb7 100644 --- a/Software/src/devboard/safety/safety.cpp +++ b/Software/src/devboard/safety/safety.cpp @@ -244,14 +244,18 @@ void update_machineryprotection() { // 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(); - //TODO, raise info event? Forced balancing starting! + 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 - //TODO, raise info event? Balancing time elapsed. Turning off... + set_event(EVENT_BALANCING_END, 0); + } else { + clear_event(EVENT_BALANCING_END); } } } diff --git a/Software/src/devboard/utils/events.cpp b/Software/src/devboard/utils/events.cpp index 88546af3f..23ad44543 100644 --- a/Software/src/devboard/utils/events.cpp +++ b/Software/src/devboard/utils/events.cpp @@ -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: diff --git a/Software/src/devboard/utils/events.h b/Software/src/devboard/utils/events.h index 972842fd6..c0c27b68e 100644 --- a/Software/src/devboard/utils/events.h +++ b/Software/src/devboard/utils/events.h @@ -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) \ From 3353bece6c5e236cb60f1f42623be9691875ded5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 7 Jan 2025 16:02:02 +0200 Subject: [PATCH 4/6] Increase LFP allowed imbalance mv limit --- Software/src/battery/TESLA-BATTERY.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/src/battery/TESLA-BATTERY.h b/Software/src/battery/TESLA-BATTERY.h index 4a8438387..9739b0e54 100644 --- a/Software/src/battery/TESLA-BATTERY.h +++ b/Software/src/battery/TESLA-BATTERY.h @@ -23,7 +23,7 @@ #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 From d852692b2ec19251f7ef43d74e879adb3c8a2d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 7 Jan 2025 16:03:01 +0200 Subject: [PATCH 5/6] Increase max LFP allowed cell limit --- Software/src/battery/TESLA-BATTERY.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/src/battery/TESLA-BATTERY.h b/Software/src/battery/TESLA-BATTERY.h index 9739b0e54..51439722e 100644 --- a/Software/src/battery/TESLA-BATTERY.h +++ b/Software/src/battery/TESLA-BATTERY.h @@ -26,7 +26,7 @@ #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. From d2d67db844545e0ec8a997d7fce12f5147ec6aa2 Mon Sep 17 00:00:00 2001 From: Matt Holmes Date: Wed, 8 Jan 2025 16:53:23 +0000 Subject: [PATCH 6/6] Adding ability to remotely trigger a BMS reset via MQTT and configure Device ID for multiple emulators (#746) * Adding ability to remotely trigger a BMS reset via MQTT * Adding comment to remove build warning * Enabling configuration of HA Device Id to allow for multiple battery emulators in HA without collision --- Software/USER_SETTINGS.cpp | 6 +- Software/USER_SETTINGS.h | 1 + .../comm_contactorcontrol.cpp | 36 ++++++-- .../contactorcontrol/comm_contactorcontrol.h | 9 ++ Software/src/devboard/mqtt/mqtt.cpp | 88 +++++++++++++++++-- Software/src/devboard/mqtt/mqtt.h | 1 + Software/src/include.h | 2 +- 7 files changed, 129 insertions(+), 14 deletions(-) diff --git a/Software/USER_SETTINGS.cpp b/Software/USER_SETTINGS.cpp index dc3363e27..3026ac801 100644 --- a/Software/USER_SETTINGS.cpp +++ b/Software/USER_SETTINGS.cpp @@ -47,8 +47,10 @@ const char* mqtt_object_id_prefix = "be_"; // Custom prefix for MQTT object ID. Previously, the prefix was automatically set to "esp32-XXXXXX_" const char* mqtt_device_name = "Battery Emulator"; // Custom device name in Home Assistant. Previously, the name was automatically set to "BatteryEmulator_esp32-XXXXXX" -#endif // MQTT_MANUAL_TOPIC_OBJECT_NAME -#endif // USE_MQTT +const char* ha_device_id = + "battery-emulator"; // Custom device ID in Home Assistant. Previously, the ID was always "battery-emulator" +#endif // MQTT_MANUAL_TOPIC_OBJECT_NAME +#endif // USE_MQTT #ifdef EQUIPMENT_STOP_BUTTON // Equipment stop button behavior. Use NC button for safety reasons. diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index b4d027fb9..6ce539029 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -67,6 +67,7 @@ //#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM for CONTACTOR_CONTROL, which lowers power consumption and heat generation. CONTACTOR_CONTROL must be enabled. //#define NC_CONTACTORS //Enable this line to control normally closed contactors. CONTACTOR_CONTROL must be enabled for this option. Extremely rare setting! //#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF +//#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF /* Shunt/Contactor settings */ //#define BMW_SBOX // SBOX relay control & battery current/voltage measurement diff --git a/Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp b/Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp index 8accc245e..c7714fe45 100644 --- a/Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp +++ b/Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp @@ -94,14 +94,14 @@ void init_contactors() { pinMode(BMS_2_POWER, OUTPUT); digitalWrite(BMS_2_POWER, HIGH); #endif BMS_2_POWER -#endif // HW with dedicated BMS pins -#ifdef PERIODIC_BMS_RESET // User has enabled BMS reset, turn on output on start +#endif // HW with dedicated BMS pins +#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) // User has enabled BMS reset, turn on output on start pinMode(BMS_POWER, OUTPUT); digitalWrite(BMS_POWER, HIGH); #ifdef BMS_2_POWER //Hardware supports 2x BMS pinMode(BMS_2_POWER, OUTPUT); digitalWrite(BMS_2_POWER, HIGH); -#endif BMS_2_POWER +#endif //BMS_2_POWER #endif //PERIODIC_BMS_RESET } @@ -222,17 +222,41 @@ void handle_contactors_battery2() { } #endif // CONTACTOR_CONTROL_DOUBLE_BATTERY -/* Once every 24 hours we remove power from the BMS_power pin for 30 seconds. This makes the BMS recalculate all SOC% and avoid memory leaks +/* PERIODIC_BMS_RESET - Once every 24 hours we remove power from the BMS_power pin for 30 seconds. +REMOTE_BMS_RESET - Allows the user to remotely powercycle the BMS by sending a command to the emulator via MQTT. + +This makes the BMS recalculate all SOC% and avoid memory leaks During that time we also set the emulator state to paused in order to not try and send CAN messages towards the battery Feature is only used if user has enabled PERIODIC_BMS_RESET in the USER_SETTINGS */ void handle_BMSpower() { -#ifdef PERIODIC_BMS_RESET +#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) // Get current time currentTime = millis(); +#ifdef PERIODIC_BMS_RESET // Check if 24 hours have passed since the last power removal - if (currentTime - lastPowerRemovalTime >= powerRemovalInterval && !isBMSResetActive) { + if (currentTime - lastPowerRemovalTime >= powerRemovalInterval) { + start_bms_reset(); + } +#endif //PERIODIC_BMS_RESET + + // If power has been removed for 30 seconds, restore the power and resume the emulator + if (isBMSResetActive && currentTime - lastPowerRemovalTime >= powerRemovalDuration) { + // Reapply power to the BMS + digitalWrite(BMS_POWER, HIGH); + + //Resume the battery pause and CAN communication + setBatteryPause(false, false, false, false); + + isBMSResetActive = false; // Reset the power removal flag + } +#endif //defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) +} + +void start_bms_reset() { +#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) + if (!isBMSResetActive) { lastPowerRemovalTime = currentTime; // Record the time when BMS reset was started // Set emulator state to paused (Max Charge/Discharge = 0 & CAN = stop) diff --git a/Software/src/communication/contactorcontrol/comm_contactorcontrol.h b/Software/src/communication/contactorcontrol/comm_contactorcontrol.h index ce2c0e842..febfd1186 100644 --- a/Software/src/communication/contactorcontrol/comm_contactorcontrol.h +++ b/Software/src/communication/contactorcontrol/comm_contactorcontrol.h @@ -15,6 +15,15 @@ */ void handle_BMSpower(); +/** + * @brief Start BMS reset sequence + * + * @param[in] void + * + * @return void + */ +void start_bms_reset(); + /** * @brief Contactor initialization * diff --git a/Software/src/devboard/mqtt/mqtt.cpp b/Software/src/devboard/mqtt/mqtt.cpp index 2797db3b2..e0aef8935 100644 --- a/Software/src/devboard/mqtt/mqtt.cpp +++ b/Software/src/devboard/mqtt/mqtt.cpp @@ -5,6 +5,7 @@ #include "../../../USER_SECRETS.h" #include "../../../USER_SETTINGS.h" #include "../../battery/BATTERIES.h" +#include "../../communication/contactorcontrol/comm_contactorcontrol.h" #include "../../datalayer/datalayer.h" #include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h" #include "../../lib/knolleary-pubsubclient/PubSubClient.h" @@ -20,6 +21,7 @@ MyTimer check_global_timer(800); // check timmer - low-priority MQTT checks, static String topic_name = ""; static String object_id_prefix = ""; static String device_name = ""; +static String device_id = ""; // Tracking reconnection attempts and failures static unsigned long lastReconnectAttempt = 0; @@ -90,6 +92,8 @@ SensorConfig sensorConfigs[] = { #endif // DOUBLE_BATTERY }; +SensorConfig buttonConfigs[] = {{"BMSRESET", "Reset BMS", "", "", ""}}; + static String generateCommonInfoAutoConfigTopic(const char* object_id) { return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config"; } @@ -102,6 +106,14 @@ static String generateEventsAutoConfigTopic(const char* object_id) { return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config"; } +static String generateButtonTopic(const char* subtype) { + return "homeassistant/button/" + topic_name + "/" + String(subtype); +} + +static String generateButtonAutoConfigTopic(const char* subtype) { + return generateButtonTopic(subtype) + "/config"; +} + #endif // HA_AUTODISCOVERY static std::vector order_events; @@ -130,7 +142,7 @@ static void publish_common_info(void) { } doc["enabled_by_default"] = true; doc["expire_after"] = 240; - doc["device"]["identifiers"][0] = "battery-emulator"; + doc["device"]["identifiers"][0] = ha_device_id; doc["device"]["manufacturer"] = "DalaTech"; doc["device"]["model"] = "BatteryEmulator"; doc["device"]["name"] = device_name; @@ -235,7 +247,7 @@ static void publish_cell_voltages(void) { doc["enabled_by_default"] = true; doc["expire_after"] = 240; doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}"; - doc["device"]["identifiers"][0] = "battery-emulator"; + doc["device"]["identifiers"][0] = ha_device_id; doc["device"]["manufacturer"] = "DalaTech"; doc["device"]["model"] = "BatteryEmulator"; doc["device"]["name"] = device_name; @@ -264,7 +276,7 @@ static void publish_cell_voltages(void) { doc["enabled_by_default"] = true; doc["expire_after"] = 240; doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}"; - doc["device"]["identifiers"][0] = "battery-emulator"; + doc["device"]["identifiers"][0] = ha_device_id; doc["device"]["manufacturer"] = "DalaTech"; doc["device"]["model"] = "BatteryEmulator"; doc["device"]["name"] = device_name; @@ -343,7 +355,7 @@ void publish_events() { doc["json_attributes_topic"] = state_topic; doc["json_attributes_template"] = "{{ value_json | tojson }}"; doc["enabled_by_default"] = true; - doc["device"]["identifiers"][0] = "battery-emulator"; + doc["device"]["identifiers"][0] = ha_device_id; doc["device"]["manufacturer"] = "DalaTech"; doc["device"]["model"] = "BatteryEmulator"; doc["device"]["name"] = device_name; @@ -400,6 +412,66 @@ void publish_events() { #endif // HA_AUTODISCOVERY } +static void publish_buttons_discovery(void) { +#ifdef HA_AUTODISCOVERY + static bool mqtt_first_transmission = true; + if (mqtt_first_transmission == true) { + mqtt_first_transmission = false; + +#ifdef DEBUG_LOG + logging.println("Publishing buttons discovery"); +#endif // DEBUG_LOG + + static JsonDocument doc; + for (int i = 0; i < sizeof(buttonConfigs) / sizeof(buttonConfigs[0]); i++) { + SensorConfig& config = buttonConfigs[i]; + doc["name"] = config.name; + doc["unique_id"] = config.object_id; + doc["command_topic"] = generateButtonTopic(config.object_id); + doc["enabled_by_default"] = true; + doc["expire_after"] = 240; + doc["device"]["identifiers"][0] = ha_device_id; + doc["device"]["manufacturer"] = "DalaTech"; + doc["device"]["model"] = "BatteryEmulator"; + doc["device"]["name"] = device_name; + doc["origin"]["name"] = "BatteryEmulator"; + doc["origin"]["sw"] = String(version_number) + "-mqtt"; + doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator"; + serializeJson(doc, mqtt_msg); + mqtt_publish(generateButtonAutoConfigTopic(config.object_id).c_str(), mqtt_msg, true); + doc.clear(); + } + } +#endif // HA_AUTODISCOVERY +} + +static void subscribe() { + for (int i = 0; i < sizeof(buttonConfigs) / sizeof(buttonConfigs[0]); i++) { + SensorConfig& config = buttonConfigs[i]; + const char* topic = generateButtonTopic(config.object_id).c_str(); +#ifdef DEBUG_LOG + logging.printf("Subscribing to topic: [%s]\n", topic); +#endif // DEBUG_LOG + client.subscribe(topic); + } +} + +void mqtt_message_received(char* topic, byte* payload, unsigned int length) { +#ifdef DEBUG_LOG + logging.printf("MQTT message arrived: [%s]\n", topic); +#endif // DEBUG_LOG + +#ifdef REMOTE_BMS_RESET + const char* bmsreset_topic = generateButtonTopic("BMSRESET").c_str(); + if (strcmp(topic, bmsreset_topic) == 0) { +#ifdef DEBUG_LOG + logging.println("Triggering BMS reset"); +#endif // DEBUG_LOG + start_bms_reset(); + } +#endif // REMOTE_BMS_RESET +} + /* If we lose the connection, get it back */ static bool reconnect() { // attempt one reconnection @@ -414,6 +486,10 @@ static bool reconnect() { clear_event(EVENT_MQTT_DISCONNECT); set_event(EVENT_MQTT_CONNECT, 0); reconnectAttempts = 0; // Reset attempts on successful connection +#ifdef HA_AUTODISCOVERY + publish_buttons_discovery(); +#endif + subscribe(); #ifdef DEBUG_LOG logging.println("connected"); #endif // DEBUG_LOG @@ -440,16 +516,18 @@ void init_mqtt(void) { topic_name = mqtt_topic_name; object_id_prefix = mqtt_object_id_prefix; device_name = mqtt_device_name; + device_id = ha_device_id; #else // Use default naming based on WiFi hostname for topic, object ID prefix, and device name topic_name = "battery-emulator_" + String(WiFi.getHostname()); object_id_prefix = String(WiFi.getHostname()) + String("_"); device_name = "BatteryEmulator_" + String(WiFi.getHostname()); - + device_id = "battery-emulator"; #endif #endif client.setServer(MQTT_SERVER, MQTT_PORT); + client.setCallback(mqtt_message_received); #ifdef DEBUG_LOG logging.println("MQTT initialized"); #endif // DEBUG_LOG diff --git a/Software/src/devboard/mqtt/mqtt.h b/Software/src/devboard/mqtt/mqtt.h index 91ddc0eaa..1ee57f307 100644 --- a/Software/src/devboard/mqtt/mqtt.h +++ b/Software/src/devboard/mqtt/mqtt.h @@ -47,6 +47,7 @@ extern const char* mqtt_password; extern const char* mqtt_topic_name; extern const char* mqtt_object_id_prefix; extern const char* mqtt_device_name; +extern const char* ha_device_id; extern char mqtt_msg[MQTT_MSG_BUFFER_SIZE]; diff --git a/Software/src/include.h b/Software/src/include.h index bb49e8653..792b84465 100644 --- a/Software/src/include.h +++ b/Software/src/include.h @@ -45,7 +45,7 @@ #endif #ifdef HW_LILYGO -#ifdef PERIODIC_BMS_RESET +#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) #if defined(CAN_ADDON) || defined(CANFD_ADDON) || defined(CHADEMO_BATTERY) //Check that BMS reset is not used at the same time as Chademo and CAN addons #error BMS RESET CANNOT BE USED AT SAME TIME AS CAN-ADDONS / CHADMEO! NOT ENOUGH GPIO!