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.

Table 1: Home Assistant Deployment Environments on Raspberry Pi
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.

Table 2: Pi 5 to HC-SR501 PIR Sensor Wiring
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)
⚠️ Callout Tip: The HC-SR501 has two orange potentiometers. Set the left one (Time Delay) fully counter-clockwise for a minimal ~0.3s pulse, and the right one (Sensitivity) to the middle position. We handle debounce and timing in software via Home Assistant, not in hardware.

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):

  1. Check Broker IP: Verify MQTT_BROKER in the script. If HA is running in Docker on the same Pi, use 172.17.0.1 (Docker bridge) or the Pi's actual LAN IP, not 127.0.0.1 (which points to the script's container/namespace).
  2. Check Mosquitto Config: Modern Mosquitto (v2.0+) denies anonymous connections by default. Ensure your mosquitto.conf has allow_anonymous false and that your credentials match.
  3. Firewall: Run sudo ufw allow 1883/tcp if 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 smbus2 and bme280 via pip. Add a secondary thread in the Python script that polls the I2C address 0x76 every 60 seconds and publishes to homeassistant/sensor/bme280/state as 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.