Pi Zero MQTT Sensor Node with Mosquitto and paho-mqtt in Python
Writing readings to a CSV file is fine for one device, but a real IoT system has several sensor nodes reporting to a central place, and dashboards or automations reacting to them. MQTT is the standard lightweight protocol for this. A Pi Zero W or Zero 2 W with a sensor makes an excellent MQTT node: it has Wi-Fi, full Python, and enough power to both publish data and act on commands.
In this tutorial you will install the Mosquitto broker with password authentication, then write a Python node that publishes BME280 temperature, humidity and pressure as JSON every 30 seconds, reports whether it is online using MQTT's last will feature, and switches an LED when it receives a command.
MQTT Concepts
MQTT uses a publish/subscribe model. Clients never talk to each other directly; they all connect to a broker. A client publishes a message to a topic, a path-like string such as home/pizero1/env, and the broker delivers it to every client subscribed to that topic. Subscriptions can use wildcards: + matches one level and # matches everything below a level, so home/# receives every message in the house.
| Feature | What it does | Used here for |
|---|---|---|
| QoS 0 | At most once, no acknowledgement | Not used |
| QoS 1 | At least once, acknowledged by the broker | Sensor data and commands |
| Retained message | Broker stores the last message and sends it to new subscribers | Latest reading and online status |
| Last will (LWT) | Broker publishes a preset message if the client disconnects unexpectedly | Marking the node offline |
| Keepalive | Client pings the broker; a silent client is considered dead | Detecting power loss |
Our node uses three topics: home/pizero1/env for readings, home/pizero1/status for online or offline, and home/pizero1/led/set for commands.
Installing the Mosquitto Broker
The broker can run on a second Pi, a laptop or the same Zero for testing. Since Mosquitto 2.0, a fresh install only accepts connections from the local machine, so you must add a listener and an authentication method before other devices can connect.
sudo apt install -y mosquitto mosquitto-clients
# Create a password file with user 'sensor' (prompts for a password)
sudo mosquitto_passwd -c /etc/mosquitto/passwd sensor
sudo chown mosquitto: /etc/mosquitto/passwd
sudo chmod 600 /etc/mosquitto/passwd
# Listen on all interfaces, port 1883, and require a password
sudo tee /etc/mosquitto/conf.d/lan.conf > /dev/null <<'EOF'
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
EOF
sudo systemctl restart mosquitto
sudo systemctl enable mosquitto
# In a second terminal, watch everything under home/
mosquitto_sub -h localhost -u sensor -P 'your-password' -t 'home/#' -v
The Sensor Node Program
Wire the BME280 to I2C and an LED to GPIO17 as in the earlier tutorials. Install the client library in the same virtual environment as the BME280 driver. The code uses the paho-mqtt 2.x callback API.
# gpiozero and lgpio come from Raspberry Pi OS itself; the venv can see them
# only if it was created with --system-site-packages (as in the BME280 tutorial):
# python3 -m venv --system-site-packages ~/envs/sensors
source ~/envs/sensors/bin/activate
pip install 'paho-mqtt>=2.0' RPi.bme280 smbus2
#!/usr/bin/env python3
"""Raspberry Pi Zero MQTT sensor node (paho-mqtt 2.x)."""
import json
import time
import bme280
import smbus2
import paho.mqtt.client as mqtt
from gpiozero import LED
BROKER = '192.168.1.20' # IP or hostname of the Mosquitto broker
PORT = 1883
USER, PASSWORD = 'sensor', 'your-password'
NODE = 'pizero1'
T_ENV = f'home/{NODE}/env'
T_STATUS = f'home/{NODE}/status'
T_LED = f'home/{NODE}/led/set'
INTERVAL_S = 30
led = LED(17)
bus = smbus2.SMBus(1)
cal = bme280.load_calibration_params(bus, 0x76)
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print('Connect failed:', reason_code)
return
print('Connected to broker')
# Announce we are online (retained, so new subscribers see it)
client.publish(T_STATUS, 'online', qos=1, retain=True)
# Subscribe here so the subscription is renewed after every reconnect
client.subscribe(T_LED, qos=1)
def on_message(client, userdata, msg):
command = msg.payload.decode().strip().upper()
if command == 'ON':
led.on()
elif command == 'OFF':
led.off()
print('LED command:', command)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=NODE)
client.username_pw_set(USER, PASSWORD)
# Last will: published by the broker if we vanish without disconnecting
client.will_set(T_STATUS, 'offline', qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start() # network handling runs in a background thread
try:
while True:
d = bme280.sample(bus, 0x76, cal)
payload = json.dumps({
'temperature_c': round(d.temperature, 2),
'humidity_pct': round(d.humidity, 1),
'pressure_hpa': round(d.pressure, 2),
'ts': int(time.time()),
})
client.publish(T_ENV, payload, qos=1, retain=True)
time.sleep(INTERVAL_S)
except KeyboardInterrupt:
pass
finally:
# A clean disconnect does NOT trigger the will, so publish offline ourselves
client.publish(T_STATUS, 'offline', qos=1, retain=True).wait_for_publish()
client.loop_stop()
client.disconnect()
Testing the Node
Run the node with ~/envs/sensors/bin/python mqtt_node.py. In the mosquitto_sub window you should see the status message and a JSON reading every 30 seconds. Control the LED from any machine with the Mosquitto clients installed:
mosquitto_pub -h 192.168.1.20 -u sensor -P 'your-password' -t home/pizero1/led/set -m ON -q 1
home/pizero1/status online
home/pizero1/env {"temperature_c": 27.41, "humidity_pct": 58.2, "pressure_hpa": 1004.62, "ts": 1736932200}Now pull the power from the Zero. After roughly one and a half keepalive periods the broker notices the silence and publishes the will, so home/pizero1/status offline appears. This is how dashboards such as Node-RED or Home Assistant can show that a node is dead rather than displaying a stale reading forever.
Design Choices Worth Understanding
The node sends all three readings in one JSON message rather than one topic per value. JSON keeps related values together with a single timestamp and means one publish per interval, which saves radio time on a battery-powered node. The alternative, separate topics such as home/pizero1/temperature, is easier for very simple subscribers to parse. Either is valid; pick one convention and use it on every node.
Readings are published with retain=True, so a dashboard that starts later immediately gets the last value instead of waiting up to 30 seconds. Including the ts field matters with retained data, because a retained reading may be hours old if the node has died; the subscriber can compare the timestamp with the current time and grey out stale values.
QoS 1 is a good default for both data and commands on a home network. It guarantees delivery to the broker at the cost of an acknowledgement packet, and occasional duplicates are harmless here because sending ON twice leaves the LED on. QoS 2, which guarantees exactly-once delivery, adds two more packets per message and is rarely needed for sensor data.
Troubleshooting
- Connection refused from another device: the listener was not added, so Mosquitto only listens on localhost. Check
/etc/mosquitto/conf.d/lan.confandsudo journalctl -u mosquitto. - Not authorised: wrong username or password, or the password file is unreadable by the mosquitto user.
- TypeError about callback arguments: you have paho-mqtt 1.x installed, or code written for 1.x. Check with
pip show paho-mqtt. - LED commands stop after a Wi-Fi drop: subscriptions made outside
on_connectare lost on reconnect. Subscribe insideon_connectas shown. - Duplicate node kicked off: two clients used the same
client_id. Every node needs a unique ID.
Summary
You set up a password-protected Mosquitto broker and built a Pi Zero node that publishes JSON sensor data with QoS 1 and retained messages, reports its availability with a last will, and acts on commands by subscribing in on_connect. Run it as a systemd service, copy it to more Zeros with different NODE names, and you have the backbone of a home or lab monitoring network.