Smart plug MQTT monitoring is the cheapest way to find out what each appliance in your home really costs to run. In this tutorial you will connect Tasmota and Shelly plugs to a broker, normalize their different payloads, store readings in SQLite with a Python subscriber, and produce a daily kWh chart per appliance. The approach handles reboots and counter resets, which is where most homemade energy loggers quietly go wrong.
Which Smart Plugs Speak MQTT
You need a plug that can publish to a broker you choose, not only to a vendor cloud. Three families cover most of the market for this purpose:
| Plug family | How MQTT is enabled | Energy fields | Notes |
|---|---|---|---|
| Tasmota-flashed plugs (many ESP-based models) | Built into Tasmota firmware | ENERGY.Power (W), ENERGY.Total (kWh), Today, Yesterday |
Some models ship with Tasmota; others need flashing |
| Shelly Plug S Gen3 / Plus Plug | Local web UI, Settings > MQTT | apower (W), aenergy.total (Wh) |
Gen2/Gen3 JSON-RPC API |
| ESPHome-based plugs | mqtt: block in ESPHome YAML |
One topic per sensor | Flexible but per-entity topics |
Check that the plug has power metering before you buy; many cheap plugs only switch. Also check the rated current against the appliance, since kettles, heaters and dryers are near the limit of most plugs.
Connecting a Tasmota Plug
Open the Tasmota web UI, then Configuration > Configure MQTT, or use the console for a faster setup. The Backlog command applies several settings in one go and restarts the plug:
Backlog MqttHost broker.connectmq.com; MqttPort 1883; MqttClient plug-kitchen-kettle-9f2; Topic kettle; FullTopic connectmq/demo/p4w8/%prefix%/%topic%/; TelePeriod 60
This makes the plug publish its telemetry every 60 seconds to connectmq/demo/p4w8/tele/kettle/SENSOR, with a payload like:
{
"Time": "2026-10-16T07:31:12",
"ENERGY": {
"TotalStartTime": "2025-11-02T18:04:51",
"Total": 48.215,
"Yesterday": 0.412,
"Today": 0.197,
"Period": 3,
"Power": 0,
"ApparentPower": 0,
"ReactivePower": 0,
"Factor": 0.00,
"Voltage": 231,
"Current": 0.000
}
}
Total is the lifetime energy in kWh, Power is the instantaneous active power in watts. Use a random path segment such as p4w8 because the public test broker is shared and readable by everyone. The Tasmota-specific details, including TLS builds, are covered in the Tasmota MQTT guide.
Connecting a Shelly Plug
On a Shelly Plus Plug or Plug S Gen3, open the local web UI, go to Settings > MQTT, enable it, set the server to broker.connectmq.com:1883, set the prefix to connectmq/demo/p4w8/shelly/fridge, and enable Generic status update over MQTT. The plug then publishes complete snapshots on connectmq/demo/p4w8/shelly/fridge/status/switch:0, where apower is watts and aenergy.total is the lifetime energy in watt-hours, not kWh. Mixing up those units is the most common bug when combining brands. The Shelly MQTT setup guide explains the full topic and RPC structure.
Designing the Smart Plug MQTT Data Pipeline
Every plug already publishes to the broker; the missing piece is something that listens and keeps history. Brokers deliver live messages and retained values but are not databases, so the logger is a separate MQTT client. The plan:
- Subscribe to both plug formats with wildcards.
- Normalize each message into
(time, plug, power_w, total_kwh). - Store every sample in SQLite.
- Compute daily energy from the difference between consecutive lifetime totals, ignoring negative jumps caused by counter resets.
- Plot one bar series per plug per day.
Why compute from lifetime totals instead of integrating power? Because power is sampled once a minute, and a kettle that runs for 90 seconds between two samples may appear as zero. The plug's own energy counter measures continuously, so deltas of the total are accurate even with sparse sampling. If a message is lost, the next delta simply covers a longer period.
The Python Logger
pip install "paho-mqtt>=2.0" matplotlib
import json
import sqlite3
import sys
from datetime import datetime
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/p4w8"
DB = sqlite3.connect("plugs.db", check_same_thread=False)
DB.execute("""CREATE TABLE IF NOT EXISTS samples (
ts TEXT, plug TEXT, power_w REAL, total_kwh REAL)""")
DB.commit()
def normalize(topic, payload):
parts = topic.split("/")
data = json.loads(payload)
if "/tele/" in topic and topic.endswith("/SENSOR"):
e = data.get("ENERGY")
if not e:
return None
return parts[-2], float(e["Power"]), float(e["Total"])
if "/shelly/" in topic and topic.endswith("/status/switch:0"):
if "aenergy" not in data:
return None
plug = parts[parts.index("shelly") + 1]
return plug, float(data.get("apower", 0)), float(data["aenergy"]["total"]) / 1000.0
return None
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
client.subscribe(f"{PREFIX}/tele/+/SENSOR", qos=1)
client.subscribe(f"{PREFIX}/shelly/+/status/switch:0", qos=1)
def on_message(client, userdata, msg):
try:
row = normalize(msg.topic, msg.payload)
except (ValueError, KeyError) as exc:
print("Skipping", msg.topic, exc)
return
if row is None:
return
plug, power, total = row
DB.execute("INSERT INTO samples VALUES (?, ?, ?, ?)",
(datetime.now().isoformat(timespec="seconds"), plug, power, total))
DB.commit()
print(f"{plug:12s} {power:8.1f} W {total:10.3f} kWh")
def daily_kwh():
rows = DB.execute("SELECT ts, plug, total_kwh FROM samples ORDER BY plug, ts").fetchall()
result, last = {}, {}
for ts, plug, total in rows:
day = ts[:10]
prev = last.get(plug)
last[plug] = total
if prev is None:
continue
delta = total - prev
if delta < 0: # counter reset or plug replaced: skip this interval
continue
result.setdefault(plug, {}).setdefault(day, 0.0)
result[plug][day] += delta
return result
def plot():
import matplotlib.pyplot as plt
data = daily_kwh()
days = sorted({d for per in data.values() for d in per})
width = 0.8 / max(len(data), 1)
fig, ax = plt.subplots(figsize=(10, 5))
for i, (plug, per_day) in enumerate(sorted(data.items())):
xs = [n + i * width for n in range(len(days))]
ax.bar(xs, [per_day.get(d, 0) for d in days], width, label=plug)
ax.set_xticks([n + 0.4 - width / 2 for n in range(len(days))])
ax.set_xticklabels(days, rotation=45, ha="right")
ax.set_ylabel("kWh per day")
ax.legend()
fig.tight_layout()
fig.savefig("daily_kwh.png", dpi=120)
for plug, per_day in sorted(data.items()):
print(plug, {d: round(v, 3) for d, v in sorted(per_day.items())})
if __name__ == "__main__":
if sys.argv[1:] == ["plot"]:
plot()
else:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="plug-logger-3391")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()
Leave python plug_logger.py running, and after a day or two run python plug_logger.py plot to produce daily_kwh.png and print the numbers. Days are bucketed by the logger's local time, which is usually what you want for household reporting.
Making Sense of the Numbers
Once you have a week of data, a few patterns usually stand out:
- Standby loads. Televisions, consoles and network gear that never drop below a few watts add up over a year. Look at the minimum
power_wper plug, not just the daily total. - Fridges and freezers show a regular duty cycle. A rising daily total at the same room temperature can indicate a failing seal or dirty condenser.
- Short high loads such as kettles and coffee machines barely register in power samples, but the energy counter captures them, which validates the delta method.
- Annual cost is daily kWh multiplied by 365 and your tariff. Calculate it per plug so you know which change is worth making.
For a whole-house view to compare against, see the home energy monitor with MQTT tutorial.
Turning Smart Plug MQTT Data into Alerts
History tells you what happened; alerts tell you while it is still happening. Because every reading already flows through the broker, you can add alerting without touching the plugs:
- Freezer stopped. A freezer plug reporting under a few watts for more than an hour usually means a tripped thermostat or a door left open. The ConnectMQ portal's threshold alert rules can watch a numeric field on a topic, and a Python or Node-RED subscriber can add the "for more than an hour" condition.
- Forgotten appliance. An iron, heater or hair straightener drawing power for longer than usual is a classic smart plug MQTT automation: publish
offto the plug's command topic after a timeout. - Washing machine finished. Power dropping from hundreds of watts to near zero and staying there for a few minutes is a reliable "cycle done" signal for a phone notification.
Keep the logic in one subscriber rather than spreading it across devices, so you can change thresholds without reflashing anything.
Troubleshooting Checklist
| Problem | Cause | Fix |
|---|---|---|
| Daily totals are 1000 times too large | Shelly Wh treated as kWh | Divide aenergy.total by 1000 |
| No Tasmota data for minutes | TelePeriod too long |
Set TelePeriod 60 or lower (minimum 10) |
| Plugs disconnect each other | Duplicate MQTT client IDs | Unique MqttClient per plug |
| Negative daily values | Counter reset not handled | Skip negative deltas as above |
| Power readings look wrong | Uncalibrated plug | Calibrate with PowerSet, VoltageSet, CurrentSet in Tasmota |
Moving to a Private Broker
The public broker is perfect for this experiment, but energy data reveals when you are home, and anyone who guesses your prefix can also switch your plugs. Before leaving the setup running, move to a cloud MQTT broker with credentials and TLS. With ConnectMQ, create a broker in the portal, then give each plug its own username, password and client ID. In Tasmota that means a TLS-enabled build and MqttPort 8883; in Shelly, enable SSL with the built-in CA bundle. In the logger, add:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.tls_set()
client.connect("broker.connectmq.com", 8883, keepalive=60)
The free Serverless plan's 25 connections are enough for a dozen plugs, the logger and a dashboard. A fully managed MQTT service also means the broker is reachable from your phone without opening ports on your router. The IoT automation guide shows how plug data can trigger automations as well.
Start Monitoring Your Plugs with ConnectMQ
ConnectMQ gives your smart plugs, loggers and dashboards a private, TLS-secured broker with a live topic explorer and threshold alert rules, for example to warn you when a freezer plug reports zero watts for too long. Create a free Serverless broker at app.connectmq.com, no credit card required.
Frequently Asked Questions
Can any smart plug send MQTT data?
No. Only plugs with MQTT in their firmware, such as Tasmota, Shelly Gen2/Gen3 or ESPHome devices, can publish to a broker you choose. Cloud-only plugs need a vendor integration instead.
How accurate are smart plug energy readings?
They are usually good enough for comparing appliances and spotting waste, but they are not billing-grade meters. Calibrating against a known resistive load improves accuracy noticeably on Tasmota plugs.
Why use the energy total instead of the power value?
The plug integrates energy continuously, while power is only sampled when a message is sent. Differences between lifetime totals capture short loads that power samples miss.
How often should a smart plug publish?
Every 30 to 60 seconds is a good balance for history and dashboards. Shorter intervals add traffic without improving daily totals, because those come from the energy counter.



