If you want to use a Raspberry Pi for Home Assistant and simultaneously read physical GPIO pins via custom Python scripts, you face an immediate architectural roadblock. The standard Home Assistant OS (HAOS) locks down the host operating system, blocking direct access to /dev/gpiomem. To build a bare-metal sensor node that feeds directly into your smart home dashboard, you must run Raspberry Pi OS (Bookworm) with Home Assistant Core installed via Docker, alongside a custom Python MQTT publisher.
This guide walks through building a Pi 5-based motion sensor node that publishes state changes to Home Assistant over MQTT. We will cover the exact hardware, the pin mapping, a production-ready Python script using the latest Paho MQTT v2.0 API, and the specific Bookworm OS errors that will crash your build if you aren't prepared.
The Architecture Decision: HAOS vs. Raspberry Pi OS
Before ordering parts, you must choose your operating environment. Most tutorials default to HAOS because it is a plug-and-play appliance. However, HAOS strips out the standard Linux GPIO toolchain. If your goal is to wire sensors directly to the Pi's 40-pin header and read them with Python, HAOS is the wrong tool.
| Feature | HAOS (Appliance Image) | Raspberry Pi OS + HA Core (Docker) |
|---|---|---|
| Direct GPIO Access | Blocked (No /dev/mem access) |
Full access via gpiozero / lgpio |
| Add-on Store | Native 1-click install | Manual Docker Compose setup required |
| RAM Overhead | ~1.2 GB (Supervisor + OS) | ~600 MB (Debian base + Docker) |
| OS Updates | Managed via HA Dashboard | Standard sudo apt update && upgrade |
| Best Use Case | Dedicated server, ESP32/Zigbee dongles | Direct GPIO wiring, custom Python integrations |
Decision framework: Choose HAOS if you plan to use ESP32 boards running ESPHome for your remote sensors. Choose Raspberry Pi OS + Docker if you want the Pi itself to act as the physical sensor node.
Parts List & Pin Mapping for the Pi 5 Sensor Node
This build targets the Raspberry Pi 5 (8GB variant), though the code and pinouts are fully backward-compatible with the Pi 4 Model B. The Pi 5 requires a dedicated active cooler and a 27W USB-C PD power supply to prevent brownouts when driving external 5V sensors.
Hardware BOM
- Compute: Raspberry Pi 5 (8GB) - ~$80
- Power: Official 27W USB-C PD Power Supply (5V/5A) - ~$12
- Thermal: Raspberry Pi Active Cooler - ~$5
- Sensor: HC-SR501 PIR Motion Sensor (adjustable delay/potentiometers) - ~$3
- Wiring: 22 AWG solid core jumper wires, 10kΩ pull-down resistor (optional but recommended for noisy environments)
GPIO Pin Mapping (BCM Numbering)
Always use Broadcom (BCM) pin numbering in your code, not physical board pin numbers. The HC-SR501 outputs 3.3V when triggered, which is safe for the Pi 5's GPIO bank.
| Pi 5 BCM Pin | Physical Pin | Function | HC-SR501 Pin |
|---|---|---|---|
| BCM 17 | Pin 11 | GPIO Input (Signal) | OUT (Middle) |
| 5V Power | Pin 2 | VCC (5.0V - 20V) | VCC (Left) |
| Ground | Pin 6 | Common Ground | GND (Right) |
The Python MQTT Integration Code
This script uses gpiozero for hardware abstraction and paho-mqtt for broker communication. It targets Paho MQTT v2.0, which introduced breaking changes to callback signatures. If you copy older v1.6 code from forums, it will crash on a fresh 2026 Bookworm install.
Prerequisites:
Install the required libraries in your virtual environment:
sudo apt install python3-rpi-lgpio (Critical for Pi 5 GPIO access)
pip install gpiozero paho-mqtt
import time
import json
import paho.mqtt.client as mqtt
from gpiozero import MotionSensor
from signal import pause
# --- Configuration & Pin Definitions ---
PIR_PIN = 17 # BCM 17 (Physical Pin 11)
MQTT_BROKER = '192.168.1.50'
MQTT_PORT = 1883
MQTT_USER = 'homeassistant'
MQTT_PASS = 'your_secure_password'
TOPIC_STATE = 'homeassistant/binary_sensor/workbench_motion/state'
TOPIC_AVAIL = 'homeassistant/binary_sensor/workbench_motion/availability'
# Initialize PIR sensor with gpiozero (handles hardware debouncing)
pir = MotionSensor(PIR_PIN, queue_len=3, threshold=0.5)
# --- MQTT Callbacks (Paho v2.0 API) ---
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print('Connected to MQTT Broker')
client.publish(TOPIC_AVAIL, 'online', retain=True)
else:
print(f'Connection failed with code: {reason_code}')
def on_disconnect(client, userdata, flags, reason_code, properties):
print(f'Disconnected from broker (Code: {reason_code}). Attempting auto-reconnect...')
# --- Sensor Event Handlers ---
def publish_motion():
print('Motion Detected')
client.publish(TOPIC_STATE, 'ON', retain=True)
def publish_clear():
print('Motion Cleared')
client.publish(TOPIC_STATE, 'OFF', retain=True)
# --- Main Execution Block ---
if __name__ == '__main__':
try:
# Initialize MQTT Client with v2.0 callback API
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='pi5_pir_node')
client.username_pw_set(MQTT_USER, MQTT_PASS)
client.will_set(TOPIC_AVAIL, 'offline', retain=True) # LWT
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
client.loop_start() # Non-blocking network loop
# Bind gpiozero events to MQTT publishers
pir.when_motion = publish_motion
pir.when_no_motion = publish_clear
print(f'Listening on BCM Pin {PIR_PIN}...')
pause() # Keep main thread alive
except KeyboardInterrupt:
print('Shutting down gracefully...')
except Exception as e:
print(f'Fatal Error: {e}')
finally:
client.publish(TOPIC_AVAIL, 'offline', retain=True)
client.loop_stop()
client.disconnect()
Home Assistant configuration.yaml Snippet
To ingest this data, add the following to your Home Assistant configuration.yaml (or via the MQTT integration UI if using discovery, though manual YAML is more transparent for debugging):
mqtt:
binary_sensor:
- name: 'Workbench Motion'
state_topic: 'homeassistant/binary_sensor/workbench_motion/state'
availability_topic: 'homeassistant/binary_sensor/workbench_motion/availability'
payload_on: 'ON'
payload_off: 'OFF'
device_class: motion
Debugging: First Three Things to Check When It Fails
When running custom Python on a Pi 5, you will inevitably hit OS-level permission or library version errors. Here are the exact error strings and their ranked fixes.
Error 1: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
The Cause: On Raspberry Pi OS Bookworm, the legacy RPi.GPIO library is deprecated and broken due to the shift from sysfs to the lgpio character device interface. gpiozero doesn't know how to talk to the Pi 5's new RP1 southbridge chip without the correct backend.
The Fix: Install the lgpio Python bindings and the system-level daemon.
sudo apt update
sudo apt install python3-rpi-lgpio lgpio
pip install rpi-lgpio
Error 2: ConnectionRefusedError: [Errno 111] Connection refused
The Cause: The Python script cannot reach the MQTT broker. This is almost always a network or broker configuration issue, not a code issue.
The Fix (Ranked):
- Check Broker IP: Verify
MQTT_BROKERin the script. If HA is running in Docker on the same Pi, use172.17.0.1(Docker bridge) or the Pi's actual LAN IP, not127.0.0.1(which points to the script's container/namespace). - Check Mosquitto Config: Modern Mosquitto (v2.0+) denies anonymous connections by default. Ensure your
mosquitto.confhasallow_anonymous falseand that your credentials match. - Firewall: Run
sudo ufw allow 1883/tcpif the Uncomplicated Firewall is active.
Error 3: RuntimeError: No access to /dev/mem. Try running as root!
The Cause: You are using the legacy RPi.GPIO library instead of gpiozero, or your user account lacks the gpio group permissions.
The Fix: Never run sensor scripts as root (sudo). Add your user to the gpio group and reboot:
sudo usermod -aG gpio $USER
sudo reboot
Extending and Simplifying the Build
Once the base PIR node is stable, you have two distinct paths forward depending on your project goals.
How to Simplify: The ESPHome Pivot
If you realize that running a full Linux OS just to read a single 3.3V GPIO pin is overkill, pivot to ESPHome. Buy an ESP32-WROOM-32 dev board (~$6), flash it with ESPHome via the HA dashboard, and wire the HC-SR501 to the ESP32. This allows you to wipe the Pi, flash the standard HAOS appliance image, and let the ESP32 handle the physical layer over WiFi. This is the industry-standard architecture for distributed smart home sensors.
How to Extend: Adding I2C Environmental Data
If you want to maximize the Pi 5's utility, add a BME280 temperature/humidity/pressure sensor via the I2C bus.
- Wiring: Connect BME280 VCC to 3.3V (Pin 1), GND to GND (Pin 9), SDA to BCM 2 (Pin 3), and SCL to BCM 3 (Pin 5).
- Software: Install
smbus2andbme280via pip. Add a secondary thread in the Python script that polls the I2C address0x76every 60 seconds and publishes tohomeassistant/sensor/bme280/stateas a JSON payload. - Hardware Note: Ensure your BME280 module has 3.3V logic level shifters. Cheap clones wired directly to 5V will fry the Pi 5's RP1 I2C controller.
By understanding the boundary between the host OS and the Home Assistant application layer, you can reliably turn a Raspberry Pi into a powerful, multi-protocol edge node for your smart home.






