Raspberry Pi

Raspberry Pi BLE to MQTT Bridge with Python and Bleak

Use the Raspberry Pi's built-in Bluetooth, the bleak library and paho-mqtt to scan BLE sensor advertisements, decode temperature and humidity, and publish throttled readings to MQTT.

Raspberry Pi receiving Bluetooth signals from small sensors, illustrating a raspberry pi ble to mqtt bridge
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

A Raspberry Pi BLE to MQTT bridge turns cheap battery-powered Bluetooth sensors into networked devices you can graph, alert on and automate. In this tutorial you will use the Pi's built-in Bluetooth radio, the bleak library and paho-mqtt to listen for sensor advertisements, decode temperature, humidity and battery values, and publish them to an MQTT broker at a sensible rate. You will test against broker.connectmq.com first and then move to private credentials.

Why Bridge BLE to MQTT

Bluetooth Low Energy sensors are small, inexpensive and run for a year or more on a coin cell, but their range is a room or two and they cannot reach the internet on their own. A Raspberry Pi with an onboard radio (Pi 3B and later, Zero W and Zero 2 W) can sit in the middle of a house, greenhouse or cold room, hear every sensor nearby and forward the data over Wi-Fi or Ethernet.

Most sensors broadcast their readings in advertisement packets. That is convenient: the bridge never needs to connect to the sensor, so dozens of devices can be read passively without draining their batteries. Connection-based (GATT) reading is still possible with bleak, but it is slower, uses more power on the sensor and limits how many devices you can handle at once. This guide focuses on advertisements, with a note on GATT at the end.

What You Need

Item Notes
Raspberry Pi with Bluetooth Pi 3B, 4, 5, Zero W or Zero 2 W; a USB BLE dongle also works
Raspberry Pi OS Bookworm BlueZ is installed by default
BLE sensors This example decodes Xiaomi LYWSD03MMC thermometers running the popular open-source custom firmware
Python 3.11+ Ships with Bookworm

The decoding logic is device-specific, but the bridge structure is not. If you have a different sensor, you only change one function.

Install bleak and paho-mqtt

bleak uses BlueZ over D-Bus on Linux, so no root access or extra system packages are required on a standard install. Check that the radio is up, then create a virtual environment:

bluetoothctl show | grep -E "Powered|Name"
sudo rfkill unblock bluetooth
python3 -m venv ~/ble-bridge/.venv
source ~/ble-bridge/.venv/bin/activate
pip install bleak "paho-mqtt>=2.0"

A quick scan confirms bleak can see devices:

python -c "import asyncio; from bleak import BleakScanner; print(*asyncio.run(BleakScanner.discover(timeout=8)), sep='\n')"

Decoding Advertisements

The custom firmware for LYWSD03MMC sensors broadcasts readings as service data under the Environmental Sensing UUID 0x181A. Two layouts are common, and you can tell them apart by length:

  • pvvx format, 15 bytes, little-endian: MAC (6 bytes, reversed), temperature as int16 in 0.01 degrees C, humidity as uint16 in 0.01 percent, battery millivolts as uint16, battery percent, a frame counter and flags.
  • ATC1441 format, 13 bytes, big-endian: MAC (6 bytes), temperature as int16 in 0.1 degrees C, humidity percent, battery percent, battery millivolts as uint16 and a counter.

Decoding those with struct is short and testable. For other devices, look at the manufacturer_data dictionary (keyed by company ID) or service_data (keyed by UUID string) in the advertisement and apply the vendor's documented format.

The Raspberry Pi BLE to MQTT Bridge Script

The script runs a continuous scanner with a detection callback. Every advertisement from a known sensor is decoded and stored; a separate async task publishes each sensor's latest reading at most once every 30 seconds. paho-mqtt runs its own network thread via loop_start(), so it coexists cleanly with asyncio.

import asyncio
import json
import os
import struct
import time
import uuid

import paho.mqtt.client as mqtt
from bleak import BleakScanner

BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"ble-bridge-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/b4n8r2/ble")
PUBLISH_EVERY = 30  # seconds per sensor

ESS_UUID = "0000181a-0000-1000-8000-00805f9b34fb"
latest = {}        # mac -> reading
last_sent = {}     # mac -> timestamp


def decode_181a(data: bytes):
    if len(data) == 15:  # pvvx custom format
        temp, hum, mv, batt = struct.unpack_from("<hHHB", data, 6)
        return {"temperature": temp / 100, "humidity": hum / 100,
                "battery_mv": mv, "battery": batt}
    if len(data) == 13:  # ATC1441 format
        temp, hum, batt, mv = struct.unpack_from(">hBBH", data, 6)
        return {"temperature": temp / 10, "humidity": hum,
                "battery_mv": mv, "battery": batt}
    return None


def on_advertisement(device, adv):
    data = adv.service_data.get(ESS_UUID)
    if not data:
        return
    reading = decode_181a(data)
    if reading:
        reading["rssi"] = adv.rssi
        reading["ts"] = int(time.time())
        latest[device.address.replace(":", "").lower()] = reading


def on_connect(client, userdata, flags, reason_code, properties):
    print("MQTT connected:", reason_code)
    client.publish(f"{PREFIX}/bridge/status", "online", qos=1, retain=True)


def make_client():
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
    if USERNAME:
        c.username_pw_set(USERNAME, PASSWORD)
    if PORT == 8883:
        c.tls_set()
    c.will_set(f"{PREFIX}/bridge/status", "offline", qos=1, retain=True)
    c.reconnect_delay_set(1, 60)
    c.on_connect = on_connect
    c.connect_async(BROKER, PORT, keepalive=60)
    c.loop_start()
    return c


async def publisher(client):
    while True:
        now = time.time()
        for mac, reading in list(latest.items()):
            if now - last_sent.get(mac, 0) >= PUBLISH_EVERY and client.is_connected():
                client.publish(f"{PREFIX}/{mac}", json.dumps(reading), qos=1, retain=True)
                last_sent[mac] = now
        await asyncio.sleep(1)


async def main():
    client = make_client()
    scanner = BleakScanner(detection_callback=on_advertisement)
    await scanner.start()
    print("scanning...")
    try:
        await publisher(client)
    finally:
        await scanner.stop()
        client.publish(f"{PREFIX}/bridge/status", "offline", qos=1, retain=True)
        client.loop_stop()
        client.disconnect()


if __name__ == "__main__":
    asyncio.run(main())

Subscribe from your laptop to watch readings arrive:

mosquitto_sub -h broker.connectmq.com -p 1883 -i ble-viewer-b4n8r2 -t "connectmq/demo/b4n8r2/ble/#" -v

Each sensor gets its own topic named after its MAC address, and the reading is retained, so a dashboard that connects later immediately sees the most recent value. If you prefer friendly names, keep a small dictionary that maps MAC addresses to room names and use those in the topic.

Design Choices That Matter

Throttle per device. Sensors may advertise every few seconds. Publishing each one wastes bandwidth and, on the free public MQTT broker, quickly hits the 10 messages per second per client limit when you have many sensors. A 30-second window per sensor is plenty for room climate.

Retain the latest state, not the history. Retained messages give new subscribers the current value instantly. History belongs in a database: a subscriber writing to SQLite or InfluxDB, such as the one in the Raspberry Pi Grafana and InfluxDB guide, is the right place for it.

Include RSSI. Signal strength tells you when a sensor is about to fall out of range or when the bridge needs to move. It is also a cheap way to detect a dying sensor before its battery reading drops.

Watch for silence. A sensor that stops advertising is invisible to the bridge. Add the ts field to dashboards, or alert when it gets old. ConnectMQ threshold alert rules work on numeric values, so a subscriber that publishes "seconds since last reading" per sensor gives you a simple staleness alarm.

Troubleshooting

  1. No devices found: run bluetoothctl power on and rfkill list. On a Pi with Wi-Fi and Bluetooth sharing one chip, heavy 2.4 GHz Wi-Fi traffic can reduce BLE reception; Ethernet or 5 GHz Wi-Fi helps.
  2. Devices found but no service data: the sensor is running stock firmware, which encrypts its advertisements, or it uses a different format. Print adv in the callback to inspect the raw fields.
  3. org.bluez.Error.InProgress: another process is already scanning. Stop other scanners, or run only one bridge per adapter.
  4. Readings stop after hours: BlueZ sometimes needs a restart on older kernels. Running the bridge as a systemd service with Restart=always covers this, as described in running a Python MQTT script on boot.
  5. Weak range: a USB extension cable to move a dongle away from the Pi's metal case and USB 3 ports can make a noticeable difference, since USB 3 is known to cause 2.4 GHz interference.

Reading GATT Characteristics

Some devices expose data only through a connection. bleak handles that with BleakClient:

from bleak import BleakClient

async def read_battery(address):
    async with BleakClient(address) as client:
        value = await client.read_gatt_char("00002a19-0000-1000-8000-00805f9b34fb")
        return value[0]

Connections are expensive, so read GATT values on a slow schedule and stop scanning while connecting if your adapter struggles with both at once. The bleak documentation covers notifications, which are the efficient way to stream connected data.

Going to Production

When the bridge works, move off the public broker, which is readable by anyone. Create a private broker on ConnectMQ, set MQTT_HOST, MQTT_PORT=8883, MQTT_USERNAME, MQTT_PASSWORD and MQTT_CLIENT_ID from the portal, and change the topic prefix to something like home/ble. The bridge needs no inbound ports, only an outbound TLS connection, so it works behind any home router or cellular modem. A managed MQTT broker also means there is no broker process on the Pi to maintain.

If you are new to MQTT on the Pi, the Raspberry Pi MQTT tutorial covers the basics, and the Raspberry Pi IoT gateway article shows how to combine BLE with other protocols on one device.

Connect Your BLE Sensors to ConnectMQ

Sign up at app.connectmq.com for the free Serverless plan, no credit card needed, and you will have broker credentials in a couple of minutes. Point the bridge at them and open the live topic explorer to see every sensor appear under its own topic.

Frequently Asked Questions

How many BLE sensors can one Raspberry Pi handle?

For passive advertisement scanning, dozens of sensors within radio range are typical, because the Pi only listens. Range and walls usually limit you before CPU does.

Do I need to pair the sensors?

No. Advertisement-based sensors broadcast to anyone listening, so no pairing or connection is required. Encrypted advertisements are the exception and need the device's key.

Can I run this on a Raspberry Pi Zero 2 W?

Yes. The bridge uses little CPU and memory, and the Zero 2 W has built-in Bluetooth and Wi-Fi, which makes it a good low-cost bridge per room or building.

Should I use QoS 0 or QoS 1 for sensor readings?

QoS 1 is a good default for readings you want delivered at least once. For very frequent values where the next reading replaces the last, QoS 0 is fine.

Related articles

Pico W vs Pico 2 W for MQTT: RP2040 vs RP2350 Performance, Memory and TLS

The Pico 2 W keeps the Pico W's footprint and Wi-Fi chip but swaps the RP2040 for the faster RP2350. This comparison looks at what that means for MQTT devices: CPU, memory, TLS, security features, software support, and which board to buy for which project.

8 min read

Raspberry Pi RFID MQTT Access Logging with the RC522 Reader

Wire an RC522 RFID reader to a Raspberry Pi, read tags in Python, check them against a local allowlist and publish every access attempt as an MQTT event that loggers, dashboards and alerting tools can consume.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.