MQTT - dalathegreat/Battery-Emulator GitHub Wiki
The Battery-Emulator has MQTT support. It can be configured to publish data to your MQTT broker and then be used in whatever way you choose. You can modify the Battery-Emulator code to publish more data, less data, or other data, as well as subscribing to MQTT topics. There are also advanced functions that can be triggered via MQTT, such as charge limits.
The main purpose of this implementation is better integration with popular home automation platforms such as Home Assistant, in order to (for example) keep track of battery temperature and cell deviation. If "MQTT" and "Home Assistant" are not familiar words, you will likely not benefit from this until you're up to speed on the current state of home automation.
Note on naming: the MQTT topic name, the Home Assistant object-ID prefix, the HA device name, and the HA device identifier are no longer independently configurable settings. All four are automatically set to the device's hostname, which itself defaults to
battery-emulator-xxxx(xxxxbeing the last two bytes of the device's MAC address) unless a custom hostname has been set on the Connectivity settings page. The examples on this page usebattery-emulator-a1b2as a stand-in for this value — substitute your device's actual hostname wherever you see it.
- Enabling MQTT in the software
- Published data
- Home Assistant Discovery
- Subscriptions
- Running multiple Battery Emulators on one broker
- References
Note: This page documents the MQTT implementation as found in
Software/src/devboard/mqtt/mqtt.cpp. The behaviour described here matches recent builds. Older releases behave differently, notably around topic naming (see Running multiple Battery Emulators on one broker).
To start using MQTT, enable the Enable MQTT checkbox under the Connectivity settings in the Webserver.
The server IP/hostname, port, username and password for MQTT can be set there. If you want to use no login info, leave the username and password fields empty.
The following options are configured at runtime (stored in NVS) and do not require recompiling:
- MQTT server, port, username, password
- Enable MQTT
- Home Assistant auto-discovery
- Transmit all cell voltages
The device's Hostname field (also on the Connectivity settings page) determines the MQTT topic name, the HA object-ID prefix, the HA device name, and the HA device identifier — see the note at the top of this page.
NOTE: Make sure that your LAN is not on
192.168.4.x, since this conflicts with the built-in ESP access point!
Out of the box, the implementation publishes the following topics. All topics are prefixed with the device's topic name (its hostname), shown here as battery-emulator-a1b2.
| Topic | Contents | Retained |
|---|---|---|
battery-emulator-a1b2/status |
Availability string (online / offline) |
offline (LWT) is retained; online is not |
battery-emulator-a1b2/info |
Main battery + system status JSON | No |
battery-emulator-a1b2/spec_data |
All cell voltages (when enabled) | No |
battery-emulator-a1b2/balancing_data |
Per-cell balancing status (when cell voltages enabled) | No |
battery-emulator-a1b2/events |
Battery-Emulator events | No |
When a second battery is configured (double-battery setups), additional battery-emulator-a1b2/spec_data_2 and battery-emulator-a1b2/balancing_data_2 topics are published, and the second battery's values are added to .../info with a _2 suffix on each key (e.g. SOC_2, battery_voltage_2). A third battery, where supported, follows the same pattern with a _3 suffix and .../spec_data_3 / .../balancing_data_3 topics.
The publish interval defaults to 5000 ms. On each cycle the order is: status → events → info → spec_data (if enabled) → balancing_data (if enabled).
Holds the main battery state plus system/emulator status. Numeric scaling notes:
- All voltages (
battery_voltage,cell_min_voltage,cell_max_voltage) are published in volts as floats. -
cell_voltage_deltais the raw difference and is published in millivolts. -
battery_currentis in amperes, power values in watts, energy/capacity values in watt-hours. - Temperatures are in °C.
Example payload (single battery), published to battery-emulator-a1b2/info:
{
"bms_status": "ACTIVE",
"pause_status": "RUNNING",
"SOC": 63.30,
"SOC_real": 61.05,
"state_of_health": 92.00,
"temperature_min": 5.8,
"temperature_max": 7.4,
"stat_batt_power": -1450.0,
"battery_current": -3.5,
"battery_voltage": 386.4,
"cell_max_voltage": 3.787,
"cell_min_voltage": 3.765,
"cell_voltage_delta": 22.0,
"total_capacity": 75000.0,
"remaining_capacity_real": 45780.0,
"remaining_capacity": 47475.0,
"max_discharge_power": 30000.0,
"max_charge_power": 12000.0,
"balancing_active_cells": 0,
"balancing_status": "Ready",
"event_level": "INFO",
"emulator_status": "RUNNING",
"cpu_temp": 41,
"emulator_uptime": 10001
}Additional keys appear conditionally:
-
charged_energy/discharged_energy— only on batteries that support charged-energy tracking, and only once both totals are non-zero. -
dc_dc_current/dc_dc_voltage— only for Tesla Model 3/Y and Model S/X. -
autocal_taper,autocal_dwell_s,autocal_cooldown_ready,autocal_soc_drift— only for the BYD Atto 3.
balancing_status is one of: Unknown, Error, Ready, Active, Blocked.
Published only when Transmit all cell voltages is enabled, and only on battery implementations that report per-cell voltages. Cell voltages are published in volts as floats.
{
"cell_voltages": [3.779, 3.780, 3.782, 3.767, 3.783, 3.769, 3.782, 3.768]
}Published alongside spec_data (same enable flag) for batteries that report cell data. Each array element is the balancing status for the corresponding cell.
{
"cell_balancing": [true,true,true,true,false,false,false,false,true,...,false]
}One message is published per new, not-yet-published event. All values are strings.
{
"event_type": "RESET_SW",
"severity": "INFO",
"count": "1",
"data": "3",
"message": "Info: The board was reset via software, webserver or OTA. Normal operation",
"millis": "10001"
}A plain-text availability topic (not JSON):
-
onlineis republished on every publish cycle (not retained). -
offlineis registered as the broker's Last Will and Testament (QoS 1, retained), so the broker marks the device offline if the connection drops.
This topic is referenced by the availability block of every Home Assistant discovery message.
When Home Assistant auto-discovery is enabled, the device publishes retained configuration topics so entities are created automatically. Discovery topics are published under the hardcoded homeassistant/... prefix; the entity/object portion and the device identity are both derived from the device's hostname.
All discovery payloads share a common block:
{
"device": {
"identifiers": ["battery-emulator-a1b2"],
"manufacturer": "DalaTech",
"model": "Battery Emulator",
"name": "battery-emulator-a1b2"
},
"availability": [{ "topic": "battery-emulator-a1b2/status" }],
"payload_available": "online",
"payload_not_available": "offline",
"enabled_by_default": true
}manufacturer and model are fixed values; identifiers and name are the device's hostname, so each Battery-Emulator on the network appears as its own distinct HA device as long as each has a unique hostname (the default, MAC-based hostname already guarantees this).
The full set of auto-discovered sensors is generated from the battery and global templates, and includes (where supported): SOC (Scaled), SOC (real), State Of Health, Temperature Min/Max, Stat Batt Power, Battery Current, Cell Max/Min Voltage, Cell Voltage Delta, Battery Voltage, Battery Total/Remaining Capacity (scaled & real), Battery Max Charge/Discharge Power, Battery Charged/Discharged Energy, Balancing Active Cells, Balancing Status, DC-DC Current/Voltage (Tesla), the BYD Auto-cal set (BYD Atto 3), and the global entities BMS Status, Pause Status, Event Level, Emulator Status, Emulator Uptime and CPU Temperature. With a second (or third) battery, each battery sensor is duplicated with a 2 (or 3) name suffix.
Topic: homeassistant/sensor/<hostname>/<entity_id>/config
Example (homeassistant/sensor/battery-emulator-a1b2/SOC/config):
{
"name": "SOC (Scaled)",
"state_topic": "battery-emulator-a1b2/info",
"unique_id": "battery-emulator-a1b2_SOC",
"default_entity_id": "sensor.battery-emulator-a1b2_SOC",
"value_template": "{{ value_json.SOC }}",
"unit_of_measurement": "%",
"device_class": "battery",
"state_class": "measurement",
"suggested_display_precision": 1,
"device": { "identifiers": ["battery-emulator-a1b2"], "manufacturer": "DalaTech", "model": "BatteryEmulator", "name": "battery-emulator-a1b2" },
"availability": [{ "topic": "battery-emulator-a1b2/status" }],
"payload_available": "online",
"payload_not_available": "offline",
"enabled_by_default": true
}Topic: homeassistant/sensor/<hostname>/cell_voltage<N>/config (second battery uses a _2_ suffix in the topic, third battery _3_).
Example (homeassistant/sensor/battery-emulator-a1b2/cell_voltage96/config):
{
"name": "Battery Cell Voltage 96",
"default_entity_id": "sensor.battery-emulator-a1b2_battery_voltage_cell96",
"unique_id": "battery-emulator-a1b2_battery_voltage_cell96",
"device_class": "voltage",
"state_class": "measurement",
"suggested_display_precision": 3,
"icon": "mdi:current-dc",
"state_topic": "battery-emulator-a1b2/spec_data",
"unit_of_measurement": "V",
"value_template": "{{ value_json.cell_voltages[95] }}",
"device": { "identifiers": ["battery-emulator-a1b2"], "manufacturer": "DalaTech", "model": "BatteryEmulator", "name": "battery-emulator-a1b2" },
"availability": [{ "topic": "battery-emulator-a1b2/status" }],
"payload_available": "online",
"payload_not_available": "offline",
"enabled_by_default": true
}Topic: homeassistant/sensor/<hostname>/event/config
{
"name": "Event",
"state_topic": "battery-emulator-a1b2/events",
"unique_id": "battery-emulator-a1b2_event",
"default_entity_id": "sensor.battery-emulator-a1b2_event",
"value_template": "{{ value_json.event_type ~ ' (c:' ~ value_json.count ~ ',m:' ~ value_json.millis ~ ') ' ~ value_json.message }}",
"json_attributes_topic": "battery-emulator-a1b2/events",
"json_attributes_template": "{{ value_json | tojson }}",
"icon": "mdi:information-outline",
"device": { "identifiers": ["battery-emulator-a1b2"], "manufacturer": "DalaTech", "model": "BatteryEmulator", "name": "battery-emulator-a1b2" },
"availability": [{ "topic": "battery-emulator-a1b2/status" }],
"payload_available": "online",
"payload_not_available": "offline",
"enabled_by_default": true
}In addition to sensors, Home Assistant Button entities are auto-discovered for the supported commands, so they can be triggered from the HA dashboard. These are published once on MQTT connect.
Topic: homeassistant/button/<hostname>/<command>/config
| Button | Command | Action |
|---|---|---|
| Reset BMS | BMSRESET |
Triggers the BMS reset feature (only if remote BMS reset is enabled) |
| Pause charge/discharge | PAUSE |
Triggers the pause feature |
| Resume charge/discharge | RESUME |
Resumes from the paused state |
| Restart Battery Emulator | RESTART |
Restarts the Battery-Emulator |
| Open Contactors | STOP |
Triggers the stop feature |
The Battery-Emulator subscribes to <hostname>/command/+, e.g. battery-emulator-a1b2/command/+ (subscription QoS 1).
The currently supported commands are:
-
BMSRESET— Triggers a hardware power-cycle of the BMS. Only acted upon if remote BMS reset is enabled (see Remote trigger through MQTT); otherwise the message is ignored. -
PAUSE— Triggers the pause feature -
RESUME— Resumes from the paused state, and clears an equipment stop, allowing contactors to re-close (see Opening and closing contactors) -
RESTART— Restarts the Battery-Emulator (pauses, then reboots the board after a short delay) -
STOP— Triggers the equipment stop (opens contactors); see Opening and closing contactors -
SET_LIMITS— Sets a temporary charge and/or discharge limit
For example: battery-emulator-a1b2/command/PAUSE
The auto-discovered button labelled "Open Contactors" is the STOP command. There is no separate "Close Contactors" command or button — closing the contactors is exposed through MQTT as the RESUME command (the auto-discovered "Resume charge/discharge" button). The naming is asymmetric, which is why it can look as though closing is missing: STOP is named after its contactor effect, while its inverse RESUME is named after its pause effect.
Under the hood, STOP latches an equipment-stop state, and RESUME clears it:
-
STOPsets the equipment-stop flag, which forces the contactor state machine open and prevents it from re-closing. -
RESUMEclears the equipment-stop flag, which allows the contactor state machine to run through precharge and close again.
So the effective mapping is:
| Command | Effect on contactors | HA button label |
|---|---|---|
STOP |
Opens (sets equipment stop) | Open Contactors |
RESUME |
Closes (clears equipment stop) | Resume charge/discharge |
Two things to keep in mind:
-
RESUMEallows the contactors to close; it does not force them closed. They only actually close if the inverter also permits closing and the normal preconditions are met (battery detected, past the post-boot startup delay, no faults). If the inverter is what is holding the contactors open,RESUMEwill not override that. -
RESUMEdoes double duty — it both ends aPAUSEand clears an equipment stop. There is no command that closes the contactors without also resuming charge/discharge, just asSTOPcannot open them without also pausing.
Sets a temporary charge and/or discharge current limit for timeout seconds. While the limit is active it overrides the manual (user-set) limit in settings.
Limits are set as deciampere, i.e. 300 = 30.0 A.
| Parameter | Data type | Default |
|---|---|---|
max_charge |
number | disable limit |
max_discharge |
number | disable limit |
timeout |
seconds | 30 |
If max_charge or max_discharge is omitted (or not an integer), the corresponding limit is disabled. If timeout is omitted, it defaults to 30 seconds.
Example payload (max charge 30 A, max discharge 40 A, timeout 60 seconds), published to battery-emulator-a1b2/command/SET_LIMITS:
{
"max_charge": 300,
"max_discharge": 400,
"timeout": 60
}-
Temporary by design. The main loop checks every cycle whether
now > (timestamp_of_last_command + timeout). Once that is true, the remote limit flags are cleared and the remote values are zeroed, so the limit must be re-sent before each timeout to stay in effect. -
Not persisted. The remote limit is never written to flash, so it is also cleared by a reboot. After power-on no remote limit is active until a new
SET_LIMITSis received. - Reverts to the manual/BMS limit, not to "unrestricted". When the remote limit expires, the allowed current falls back to the manual (user-set) limit, or to the BMS/inverter-derived limit if no manual limit applies.
- Overrides rather than combines with the manual limit. While a remote limit is active, the manual user limit is bypassed — the remote value is used instead. The remote limit can therefore sit above your manual limit during the active window. It still only ever lowers the BMS/inverter-derived allowed current (it caps, it cannot raise the battery's own limit).
To cancel a limit quickly, send a new message with a short timeout (for instance 1 second).
Multiple Battery Emulators can share the same MQTT broker, and they separate themselves automatically: the MQTT topic name, the HA object-ID prefix, the HA device name, and the HA device identifier all default to the device's hostname, which itself defaults to battery-emulator-a1b2 (a1b2 being the last two bytes of the device's MAC address) — making it unique out of the box without any manual configuration. If you'd prefer a friendlier name, set a custom hostname on the Connectivity settings page; just make sure each device on the same broker gets a different one.
- Home Assistant MQTT overview — brokers, discovery, configuration and HA services related to MQTT
- Home Assistant MQTT Sensor — manual (non-discovery) setup of MQTT sensors
- ESP-IDF MQTT (esp-mqtt) documentation — the MQTT client library currently used