Getting sensor readings from a Raspberry Pi into MQTT takes three pieces: a sensor library, the paho-mqtt client and a service manager that keeps the script alive. This tutorial wires a BME280 (or a DHT22) to a Pi, publishes temperature, humidity and pressure as JSON every 10 seconds, and runs the publisher as a systemd service that reconnects on its own and starts at boot.
Everything below was written for Raspberry Pi OS Bookworm (or newer) with Python 3.11+, paho-mqtt 2.x and the Adafruit CircuitPython sensor drivers. It works on a Pi Zero 2 W, Pi 3, Pi 4 and Pi 5.
What you need
| Item | Notes |
|---|---|
| Raspberry Pi with network access | Wi-Fi or Ethernet, SSH enabled |
| BME280 breakout (I2C) | Temperature, humidity, pressure. Address 0x76 or 0x77 |
| or DHT22 / AM2302 | Temperature and humidity on a single GPIO pin |
| 4 jumper wires | 3.3 V, GND, SDA, SCL (BME280) |
| An MQTT broker | Any reachable broker; the examples use broker.connectmq.com |
Wire the BME280 to 3V3 (pin 1), GND (pin 6), SDA (pin 3, GPIO2) and SCL (pin 5, GPIO3). A DHT22 goes to 3V3, GND and a data pin such as GPIO4 (pin 7), with a 10 kΩ pull-up resistor if your module does not already include one.
You do not need a broker on the Pi. If you want one there anyway, see running a broker on Raspberry Pi; this article only covers the client side.
1. Enable I2C and check the sensor
sudo raspi-config nonint do_i2c 0
sudo apt update && sudo apt install -y i2c-tools python3-venv
i2cdetect -y 1i2cdetect prints a grid. You should see 76 or 77 — that is the BME280's address. An empty grid means a wiring problem (SDA and SCL swapped is the classic one) or I2C is not enabled yet; reboot once after raspi-config if needed.
2. Create a virtual environment
Bookworm refuses system-wide pip install with error: externally-managed-environment. Use a venv in a project folder instead:
mkdir -p ~/sensors && cd ~/sensors
python3 -m venv .venv
.venv/bin/pip install paho-mqtt adafruit-circuitpython-bme280
# for a DHT22 instead:
# .venv/bin/pip install paho-mqtt adafruit-circuitpython-dhtThe Adafruit drivers pull in Blinka, the compatibility layer that provides board and busio on Linux.
Quick sanity check before any MQTT code:
.venv/bin/python -c "import board; from adafruit_bme280 import basic as b; s=b.Adafruit_BME280_I2C(board.I2C(), address=0x76); print(s.temperature, s.relative_humidity, s.pressure)"If you get ValueError: No I2C device at address: 0x76, try address=0x77.
3. The publisher script
The script below connects with a Last Will so subscribers know when the Pi drops off, publishes a retained online status, then sends one JSON reading every 10 seconds. Credentials come from environment variables so the same file works on your desk and in production.
import json, os, socket, time
import board
import paho.mqtt.client as mqtt
from adafruit_bme280 import basic as adafruit_bme280
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "8883"))
DEVICE = socket.gethostname()
BASE = f"testmqtt/pi/{DEVICE}"
INTERVAL = 10
sensor = adafruit_bme280.Adafruit_BME280_I2C(board.I2C(), address=0x76)
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"connect failed: {reason_code}", flush=True)
return
print("connected", flush=True)
client.publish(f"{BASE}/status", "online", qos=1, retain=True)
def on_disconnect(client, userdata, flags, reason_code, properties):
print(f"disconnected: {reason_code}", flush=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"pi-{DEVICE}")
client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
if PORT == 8883:
client.tls_set() # system CA store
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect_async(HOST, PORT, keepalive=60)
client.loop_start() # network thread, retries until connected
while True:
reading = {
"temperature": round(sensor.temperature, 2),
"humidity": round(sensor.relative_humidity, 1),
"pressure": round(sensor.pressure, 1),
"ts": int(time.time()),
}
info = client.publish(f"{BASE}/bme280", json.dumps(reading), qos=1)
print(f"publish mid={info.mid} rc={info.rc} {reading}", flush=True)
time.sleep(INTERVAL)A few choices worth explaining:
connect_async()+loop_start()— at boot the network may not be ready.connect()would raise immediately;connect_async()lets paho's background thread keep retrying, and the same thread reconnects after any later drop with the backoff set byreconnect_delay_set().- QoS 1 for readings, so a brief outage does not silently lose data. paho queues QoS 1 messages while disconnected and sends them once the connection is back. QoS 0 would be lighter but drops anything sent during an outage.
- Retained status + Last Will — anything that subscribes later immediately sees
onlineoroffline. The pattern is explained in MQTT Last Will. - Client ID from the hostname — stable and unique per Pi. Two devices sharing an ID will kick each other off the broker in a loop.
Using a DHT22 instead
The DHT22 protocol is timing-sensitive under Linux, so reads fail regularly with RuntimeError (for example "Checksum did not validate" or "A full buffer was not returned"). That is normal — skip the sample and try again:
import adafruit_dht
dht = adafruit_dht.DHT22(board.D4)
def read_dht():
for _ in range(5):
try:
return {"temperature": dht.temperature, "humidity": dht.humidity}
except RuntimeError:
time.sleep(2) # DHT22 needs ~2 s between reads
return None # publish nothing this roundCall read_dht() in the main loop and only publish when it returns a value.
4. Test it by hand
Put the credentials in a file that only your user can read, then run the script in the foreground:
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USER=username
MQTT_PASS=passwordchmod 600 ~/sensors/mqtt.env
set -a; . ~/sensors/mqtt.env; set +a
.venv/bin/python publish.pyYou should see connected followed by a publish mid=... line every 10 seconds. Now watch from another machine:
mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" -t 'testmqtt/pi/#' -vOr skip the install and open the online MQTT client in your browser, connect to the same broker and subscribe to testmqtt/pi/#. You will see the retained status message first, then a fresh bme280 JSON payload every interval.
Watch your Pi's readings in the browser
If nothing arrives, check the printed reason code. Bad user name or password and Not authorized are broker-side rejections; look them up in the MQTT reason codes tool. A timeout usually means the port is blocked — the connection errors guide walks through each case.
5. Run it as a systemd service
A script in an SSH session dies when you log out. systemd starts it at boot, restarts it if it crashes and collects its output in the journal.
[Unit]
Description=BME280 to MQTT publisher
Wants=network-online.target
After=network-online.target
[Service]
User=pi
WorkingDirectory=/home/pi/sensors
EnvironmentFile=/home/pi/sensors/mqtt.env
ExecStart=/home/pi/sensors/.venv/bin/python -u /home/pi/sensors/publish.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetReplace pi with your actual user name — recent Raspberry Pi OS images make you choose one at setup. The user must be in the i2c group (or gpio for a DHT22); the default user already is.
sudo systemctl daemon-reload
sudo systemctl enable --now bme280-mqtt
journalctl -u bme280-mqtt -f-u on the Python command line disables output buffering so log lines appear in the journal immediately (the flush=True in the script does the same).
Prove it survives failures
sudo reboot— the readings should resume within a minute, with no SSH session open.- Unplug the network for a minute. Subscribers see
offlineonce the broker notices the missing keep-alive (up to 1.5 × 60 seconds), thenonlineagain after reconnecting, followed by the readings queued during the outage. sudo systemctl kill bme280-mqtt— systemd restarts the script after 5 seconds.
Where to go next
- Shape the payload for dashboards and databases: JSON payloads in Python.
- Add more sensors and a display: the MQTT weather station builds on this script.
- Feed Home Assistant by publishing discovery messages, covered in Home Assistant MQTT.
A Pi, a sensor and forty lines of Python are enough for reliable telemetry as long as the boring parts are handled: a venv for dependencies, a stable client ID, a Last Will for presence and systemd for restarts. Test each failure on your desk once, and the Pi in the garden will behave the same way.
Frequently asked questions
Do I need to run an MQTT broker on the Raspberry Pi itself?
No. The Pi only needs a client library such as paho-mqtt. It can publish to any reachable broker: one installed on the same Pi, another machine on your LAN, or a managed broker in the cloud.
Why does pip fail with "externally-managed-environment" on Raspberry Pi OS?
Raspberry Pi OS Bookworm and later protect the system Python (PEP 668). Create a virtual environment with python3 -m venv and install paho-mqtt and the sensor libraries into it, then point your systemd service at the venv's python.
BME280 or DHT22 — which sensor should I use with MQTT?
The BME280 talks I2C, reads reliably every time and adds air pressure. The DHT22 is cheap but uses a timing-sensitive one-wire protocol, so individual reads fail regularly and your code must retry. For long-running telemetry the BME280 is less hassle.

