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
This commit is contained in:
Matt Holmes 2025-01-08 16:53:23 +00:00 committed by GitHub
parent 64e633f10f
commit d2d67db844
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 129 additions and 14 deletions

View file

@ -47,6 +47,8 @@ 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"
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

View file

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

View file

@ -95,13 +95,13 @@ void init_contactors() {
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
#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)

View file

@ -15,6 +15,15 @@
*/
void handle_BMSpower();
/**
* @brief Start BMS reset sequence
*
* @param[in] void
*
* @return void
*/
void start_bms_reset();
/**
* @brief Contactor initialization
*

View file

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

View file

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

View file

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