Reading BMS Data Over BLE with a Raspberry Pi: Protocol Decoding and Home Assistant Integration
Many lithium battery packs — particularly the growing range of aftermarket LiFePO4 units entering the South African solar market — come with a BMS (Battery Management System) that exposes data over Bluetooth Low Energy. Most come with a proprietary Android app. What they don't come with is any documentation about the BLE protocol, any supported integration with inverters or home automation platforms, or any way to get the data into a system you actually control.
Why bother decoding it yourself
If your inverter and battery come from different manufacturers — which is extremely common in DIY solar setups — the inverter cannot read the battery's state of charge. This means it can't make intelligent decisions about charge cutoff, grid-tie behaviour, or battery protection. The inverter sees the battery as a dumb DC source. Additionally, without per-cell visibility you cannot detect cell drift early — the first sign of a failing cell is often a voltage delta that only becomes visible when you're monitoring all cells individually.
Step 1: Identify the BLE characteristics
Install bleak (pip install bleak) on your Raspberry Pi. Run a BLE scan to find your BMS device by its MAC address or device name. Then use bleak's service discovery to enumerate all GATT services and characteristics. Most BMS devices have one primary service with a write characteristic (for sending command bytes) and a notify characteristic (for receiving responses).
Capture the Android app's BLE traffic using nRF Sniffer or by enabling developer mode on an Android phone and using Wireshark with the Bluetooth HCI snoop log. This gives you the raw bytes the app sends to query the BMS and the responses it receives.
Step 2: Decode the protocol
BMS protocols are typically simple request-response: the host sends a command byte or byte sequence, the BMS responds with a fixed-format packet. Common commands are: 0x03 (basic info — voltage, current, SoC, temperature) and 0x04 (cell voltages — one 2-byte big-endian value per cell).
Parse the response packet byte by byte. Total voltage is usually a 2-byte big-endian value in 10mV units — divide by 100 for volts. Current is signed, often in 10mA units. SoC is a single byte, 0–100. Cell voltages are sequential 2-byte values in 1mV units. Temperature sensors are 2-byte offsets from 2731 (273.1K) — subtract 2731 and divide by 10 for Celsius.
Once you have the decode logic, write a Python class that sends the query command, reads the notify response, and returns a structured dictionary.
Step 3: Expose to Home Assistant via MQTT
Home Assistant's MQTT integration supports auto-discovery: publish a JSON configuration payload to homeassistant/sensor/bms_cell_01/config and Home Assistant automatically creates the sensor entity with the right unit of measurement, device class, and state topic.
For a 15-cell battery, create sensors for each cell voltage (bms_battery1_cell_01 through _15), plus sensors for total voltage, current, SoC, temperature, MOS temperature, and cycle count. Group them under a single device by using the same device identifier in every config payload.
Poll the BMS every 10 seconds in a loop, publish updated values to the state topics, and your Home Assistant dashboard will show live per-cell voltages with history graphs out of the box.
Step 4: Bridge to the inverter over Modbus
To make your inverter charge and discharge the battery correctly, it needs to receive BMS data in a protocol it understands. Most inverters support a Modbus RTU register map for battery state. Write the SoC, voltage, current, and temperature values from your BMS reader into the correct Modbus registers using the pymodbus library.
The specific register addresses depend on your inverter model — consult the inverter's Modbus documentation. Common registers: holding register 0x0100 for SoC, 0x0101 for voltage, 0x0102 for current. Some inverters expect a heartbeat byte on a specific register to confirm the BMS is alive — miss it and the inverter falls back to a default charge profile.
Step 5: CAN bus for deeper inverter integration
Higher-end inverters use CAN bus for battery communication rather than Modbus. CAN allows the battery to send a broader dataset including cell-level alarms, max charge and discharge current limits, and BMS status flags. A CAN HAT for the Raspberry Pi (based on the MCP2515 controller) gives you the hardware interface; python-can handles the software side.
The CAN message format is typically defined in a DBC file specific to the inverter manufacturer. Reverse-engineer or request this file from the manufacturer. Encode your BMS data into the correct CAN frames and transmit on the expected CAN IDs at the expected interval — usually every 1 second for the primary battery state frame.
Moving to ESP32 for fleet deployment
A Raspberry Pi works well for a single installation but is overkill for fleet deployment — it's expensive, power-hungry, and takes time to boot. We're currently migrating this stack to custom ESP32 hardware with a built-in BLE radio, RS485 transceiver for Modbus, and CAN transceiver for CAN bus — all in one compact board.
The ESP32 version connects to a central fleet management dashboard over WiFi, reporting live battery telemetry, GPS location of the installation, and firmware version. Remote firmware updates push new decode logic or Modbus register maps to the entire fleet without a site visit. This architecture is the right one for scaling to dozens or hundreds of installations across multiple sites.
Need this built, not just explained?
We build production systems around exactly this kind of engineering. Tell us what you're working on.
Start a conversation