Home battery monitoring tells you how much energy you have stored, how hard the battery is working and whether any cell is drifting out of line long before it becomes a fault. In this tutorial you will publish battery management system (BMS) data such as state of charge, pack voltage, current, cell voltages and temperature to an MQTT broker, using a Victron Venus OS system as the worked example. You will also add low-battery and cell imbalance alerts that do not flood your phone.
What to Monitor on a Home Battery
A BMS exposes dozens of values. These are the ones that matter for day-to-day operation and early fault detection:
| Value | Unit | Why it matters | Typical alert idea |
|---|---|---|---|
| State of charge (SoC) | % | Energy available for the evening and night | Low below 20 %, clear above 25 % |
| Pack voltage | V | Cross-check for SoC, detects sensor faults | Outside the chemistry's normal range |
| Current | A | Positive charging, negative discharging | Sustained current near the BMS limit |
| Power | W | Voltage times current; easy to chart with PV and grid | Informational |
| Min / max cell voltage | V | Weakest and strongest cell | Spread above about 50 mV under rest |
| Temperature | °C | Lithium batteries must not charge below freezing | Below 5 °C or above 45 °C |
| Alarms and state | flags | BMS protection events | Any active alarm |
For lithium iron phosphate (LiFePO4) packs, cell voltage spread is often the most informative long-term signal. SoC is estimated by the BMS and can drift; cell voltages are measured. A spread that grows month by month points to a cell or balancing problem.
Where the Data Comes From
How you get BMS data depends on the system:
- Victron systems with a GX device (Venus OS). The GX device runs a local MQTT broker that exposes every value on the internal D-Bus. Enable it under Settings > Services (named Integrations on newer firmware) with the "MQTT on LAN" option.
- Hybrid inverters often expose battery SoC, voltage and current through Modbus registers, the same way they expose PV data; see solar inverter monitoring for the Modbus approach.
- DIY packs with JK, JBD or Daly BMS boards offer Bluetooth or UART interfaces. ESP32 firmware projects (including ESPHome components) read them and publish to MQTT.
- Server-rack batteries with CAN or RS485 ports usually report to the inverter, which then becomes your data source.
The rest of this guide uses Venus OS because its MQTT interface is well documented, but the publishing and alerting logic works for any source.
How Venus OS MQTT Works
The GX device publishes values on topics shaped like N/<portal-id>/<service>/<instance>/<path>, with a JSON payload such as {"value": 87.5}. Battery values live under the battery service:
N/<portal-id>/battery/512/Soc
N/<portal-id>/battery/512/Dc/0/Voltage
N/<portal-id>/battery/512/Dc/0/Current
N/<portal-id>/battery/512/Dc/0/Power
N/<portal-id>/battery/512/Dc/0/Temperature
N/<portal-id>/battery/512/System/MinCellVoltage
N/<portal-id>/battery/512/System/MaxCellVoltage
The instance number (512 here) varies by installation, and cell voltage paths are only available when the BMS reports them. Two details catch people out. First, the GX device only keeps publishing while a client sends a keepalive request to R/<portal-id>/keepalive regularly; without it, updates stop after about a minute. Second, the portal ID appears in the topic of N/<portal-id>/system/0/Serial, which is how a script can discover it.
Bridging Battery Data to Your Broker
The script below connects to the GX device on the LAN, sends keepalives, collects battery values and publishes one compact JSON summary every 10 seconds to your own broker. It tests against the public area of broker.connectmq.com, which needs no account but is readable by everyone, so it uses a random prefix.
pip install "paho-mqtt>=2.0"
import json
import threading
import time
import uuid
import paho.mqtt.client as mqtt
GX_HOST = "192.168.1.70" # Venus OS GX device
PREFIX = f"connectmq/demo/{uuid.uuid4().hex[:8]}/battery/house"
PUBLISH_EVERY = 10
PATHS = {
"Soc": "soc_pct",
"Dc/0/Voltage": "voltage_v",
"Dc/0/Current": "current_a",
"Dc/0/Power": "power_w",
"Dc/0/Temperature": "temp_c",
"System/MinCellVoltage": "cell_min_v",
"System/MaxCellVoltage": "cell_max_v",
}
values = {}
portal = {"id": None}
dest = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"batt-pub-{uuid.uuid4().hex[:6]}")
dest.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
dest.connect("broker.connectmq.com", 1883, keepalive=60)
dest.loop_start()
dest.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
def gx_connect(client, userdata, flags, reason_code, properties):
print("Connected to GX:", reason_code)
client.subscribe("N/+/system/0/Serial")
client.subscribe("N/+/battery/+/#")
def gx_message(client, userdata, msg):
parts = msg.topic.split("/")
if parts[2] == "system" and parts[-1] == "Serial" and portal["id"] is None:
portal["id"] = parts[1]
print("Portal ID:", portal["id"])
client.publish(f"R/{portal['id']}/keepalive", "")
return
if parts[2] != "battery":
return
path = "/".join(parts[4:])
if path in PATHS:
try:
values[PATHS[path]] = json.loads(msg.payload).get("value")
except (ValueError, AttributeError):
pass
def keepalive_loop(client):
while True:
if portal["id"]:
client.publish(f"R/{portal['id']}/keepalive", "")
time.sleep(30)
gx = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"batt-gx-{uuid.uuid4().hex[:6]}")
gx.on_connect = gx_connect
gx.on_message = gx_message
gx.connect(GX_HOST, 1883, keepalive=60)
gx.loop_start()
threading.Thread(target=keepalive_loop, args=(gx,), daemon=True).start()
print("Publishing to", PREFIX)
while True:
time.sleep(PUBLISH_EVERY)
if not values:
continue
out = dict(values)
if out.get("cell_min_v") is not None and out.get("cell_max_v") is not None:
out["cell_spread_mv"] = round((out["cell_max_v"] - out["cell_min_v"]) * 1000)
out["ts"] = int(time.time())
dest.publish(f"{PREFIX}/telemetry", json.dumps(out), qos=1, retain=True)
print(out)
Watch the result:
mosquitto_sub -h broker.connectmq.com -p 1883 -i batt-view-3318 -t "connectmq/demo/+/battery/#" -v
{"soc_pct": 63.4, "voltage_v": 52.71, "current_a": -18.2, "power_w": -959,
"temp_c": 21.0, "cell_min_v": 3.291, "cell_max_v": 3.304, "cell_spread_mv": 13, "ts": 1760611200}
Negative current and power mean the battery is discharging. Keep that sign convention consistent across sources so dashboards and automations do not need special cases.
Home Battery Monitoring Alerts Without the Noise
A low-battery alert that fires every ten seconds while SoC hovers around the threshold trains people to ignore it. Use hysteresis (different thresholds for raising and clearing) and publish alert transitions, not every reading:
import json
import paho.mqtt.client as mqtt
TELEMETRY = "connectmq/demo/<your-id>/battery/house/telemetry"
ALERTS = "connectmq/demo/<your-id>/battery/house/alerts"
RULES = {
# name: (field, raise_if, clear_if)
"low_soc": ("soc_pct", lambda v: v < 20, lambda v: v > 25),
"cell_imbalance": ("cell_spread_mv", lambda v: v > 50, lambda v: v < 30),
"cold_battery": ("temp_c", lambda v: v < 5, lambda v: v > 8),
"hot_battery": ("temp_c", lambda v: v > 45, lambda v: v < 40),
}
active = set()
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(TELEMETRY, qos=1)
def on_message(client, userdata, msg):
data = json.loads(msg.payload)
for name, (field, raise_if, clear_if) in RULES.items():
v = data.get(field)
if v is None:
continue
if name not in active and raise_if(v):
active.add(name)
client.publish(ALERTS, json.dumps({"alert": name, "state": "raised", field: v}), qos=1)
elif name in active and clear_if(v):
active.discard(name)
client.publish(ALERTS, json.dumps({"alert": name, "state": "cleared", field: v}), qos=1)
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="batt-alerts-9902")
c.on_connect = on_connect
c.on_message = on_message
c.connect("broker.connectmq.com", 1883, keepalive=60)
c.loop_forever()
Anything that can send notifications, such as Node-RED or Home Assistant, can subscribe to the alerts topic and forward it to your phone. For simple single-value thresholds, the ConnectMQ portal also offers threshold alert rules with an alerts log, which covers "SoC below 20" without running your own script; the hysteresis logic above is what you add when you need it.
Measure cell spread under rest or light load. During heavy charge or discharge, and near the top of charge, LiFePO4 cells naturally diverge more, so an imbalance alert during those periods is not necessarily a fault.
Charting Battery Behaviour Over Time
Live values answer "how full is the battery now"; history answers "is the battery still healthy". Because the broker only keeps the latest retained message, store the telemetry with a subscriber such as Telegraf writing to InfluxDB, Home Assistant's recorder or a small script writing to SQLite. Three charts pay off quickly:
- SoC with PV and grid power on the same time axis, which shows whether the battery is sized well for your evening load.
- Daily minimum SoC, which reveals seasonal shortfalls and whether a reserve setting is too high or too low.
- Cell spread at rest over months, the long-term health indicator for LiFePO4 packs. A slow upward trend is your cue to check balancing settings or contact the supplier.
Troubleshooting Checklist
- No messages from the GX device? Confirm MQTT on LAN is enabled and that you subscribed after connecting.
- Values stop after a minute? The keepalive is not reaching
R/<portal-id>/keepalive; check the discovered portal ID. - Cell voltages missing? Your BMS may not report them to Venus OS; check the battery's device page on the GX.
- SoC jumps after a full charge? The BMS resynchronized its estimate; this is normal after periods without reaching full.
- Values look stale on the dashboard? Check the retained status topic; if it says
offline, the bridge has stopped.
Moving to a Private Broker
Battery data shows when you are home and how your system is configured, and the public broker is shared, so use a private broker for the long-running setup. A fully managed MQTT service like ConnectMQ lets the bridge on your Raspberry Pi make one outbound TLS connection, with no router ports to open. Create a broker in the ConnectMQ portal and change the destination connection:
dest = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
dest.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
dest.tls_set()
dest.connect("broker.connectmq.com", 8883, keepalive=60)
From there, battery data can drive decisions alongside your home energy monitor and EV charger, such as charging the car only once the battery is above a set SoC. For broker hosting options see the cloud MQTT broker guide, and for the wider picture the IoT automation guide.
Monitor Your Battery with ConnectMQ
ConnectMQ provides a TLS-secured MQTT broker with generated credentials, a live topic explorer and threshold alert rules for values like state of charge. Create a free Serverless broker at app.connectmq.com, no credit card required, and connect your battery bridge in minutes.
Frequently Asked Questions
What is a good low-battery threshold for home batteries?
Many LiFePO4 users raise an alert around 15 to 20 percent SoC and clear it a few percent higher. Follow your battery manufacturer's minimum SoC recommendation for daily use.
Why monitor cell voltages instead of only state of charge?
SoC is an estimate that can drift, while cell voltages are direct measurements. A growing spread between the highest and lowest cell is an early sign of balancing problems or a weak cell.
Does Venus OS need internet for MQTT?
No. MQTT on LAN works entirely locally. The bridge in this guide reads locally and only makes an outbound connection to your chosen broker.
Can I monitor a DIY battery without Victron hardware?
Yes. JK, JBD and Daly BMS boards can be read over Bluetooth or UART by ESP32 firmware such as ESPHome components, which publish directly to MQTT using the same topic and alert design.



