To integrate physical GPIO hardware directly with a Raspberry Pi with Home Assistant, you must bypass the locked-down Home Assistant OS. The most robust architecture for bare-metal pin access is running Raspberry Pi OS (Bookworm), deploying Home Assistant via Docker, installing a local Mosquitto MQTT broker, and using a Python paho-mqtt script to bridge the Pi's GPIO header to your smart home network. This guide targets the Raspberry Pi 4 Model B (4GB) and provides the exact wiring, Python code, and debugging steps to control a 120V/240V workshop load (like a dust collector) using a local limit switch and relay.
Project Spec Sheet & Parts List
Before wiring, verify your components. Using a 5V relay module with a 3.3V logic trigger is a common trap; the module's optocoupler requires a 5V supply, but the control signal must be 3.3V to avoid frying the Pi's GPIO pin.
| Component | Exact Variant | Est. Price (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (4GB) | $55.00 | Target board. Pi 5 requires rpi-lgpio backend. |
| Power Supply | Official 27W USB-C PSU (5.1V / 3A) | $18.00 | Provides enough headroom for the 5V relay coil. |
| Relay Module | Songle SRD-05VDC-SL-C (1-Channel) | $4.50 | Active-LOW trigger, optically isolated. |
| Sensor | Industrial Magnetic Limit Switch | $12.00 | Normally Open (NO), dry contact. |
| Storage | Samsung PRO Endurance 64GB microSD | $14.00 | High endurance for HA database write cycles. |
Hardware Wiring & Pin Mapping
The Raspberry Pi 4 GPIO pins operate at 3.3V logic and can source a maximum of 16mA per pin (with a 50mA total limit across all 3.3V pins). The Songle relay coil draws ~70mA at 5V, so never attempt to power the relay coil from the Pi's 3.3V or 5V GPIO pins directly. We use the Pi's 5V power rail (Pin 2) for the relay VCC, which is fed directly from the USB-C power supply's 5V bus.
| Component Pin | Pi 4 Physical Pin | BCM GPIO | Function |
|---|---|---|---|
| Relay VCC | Pin 2 | 5V Power | Supplies 5V to relay coil and optocoupler. |
| Relay GND | Pin 6 | Ground | Common ground return. |
| Relay IN (Signal) | Pin 13 | GPIO 27 | Active-LOW trigger. Pulls to GND to energize. |
| Limit Switch (Signal) | Pin 11 | GPIO 17 | Internal pull-up enabled. Reads LOW when closed. |
| Limit Switch (Return) | Pin 9 | Ground | Completes the switch circuit. |
The Python MQTT Bridge Code
This script uses gpiozero for hardware abstraction and paho-mqtt for the broker connection. It targets Python 3.11 on Raspberry Pi OS (Bookworm). Note that paho-mqtt v2.0 introduced a mandatory CallbackAPIVersion parameter; older tutorials using v1.5 syntax will throw runtime errors on modern installs.
Install dependencies first:
sudo apt install python3-gpiozero python3-pip
pip3 install paho-mqtt --break-system-packages
import paho.mqtt.client as mqtt
from gpiozero import DigitalInputDevice, OutputDevice
import time
import logging
import sys
# --- PIN DEFINITIONS (BCM) ---
PIN_LIMIT_SWITCH = 17
PIN_RELAY = 27
# --- MQTT CONFIGURATION ---
BROKER_IP = "127.0.0.1"
BROKER_PORT = 1883
TOPIC_SENSOR_STATE = "homeassistant/binary_sensor/workshop_door/state"
TOPIC_RELAY_COMMAND = "homeassistant/switch/workshop_dust_collector/set"
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
# Initialize Hardware
# active_high=False means the relay energizes when the pin is driven LOW
limit_switch = DigitalInputDevice(PIN_LIMIT_SWITCH, pull_up=True, bounce_time=0.05)
dust_relay = OutputDevice(PIN_RELAY, active_high=False, initial_value=False)
def on_connect(client, userdata, flags, reason_code, properties):
"""Paho MQTT v2.0 callback signature."""
if reason_code == 0:
logging.info("Connected to Mosquitto broker.")
client.subscribe(TOPIC_RELAY_COMMAND)
else:
logging.error(f"Connection failed with code: {reason_code}")
def on_message(client, userdata, msg):
"""Handle incoming relay commands from Home Assistant."""
payload = msg.payload.decode("utf-8").strip().upper()
logging.info(f"Received command: {payload} on {msg.topic}")
if payload == "ON":
dust_relay.on()
elif payload == "OFF":
dust_relay.off()
def publish_door_state(client, state):
"""Publish limit switch state."""
payload = "ON" if state else "OFF"
client.publish(TOPIC_SENSOR_STATE, payload, retain=True)
logging.info(f"Door state changed to: {payload}")
def main():
# Paho MQTT v2.0 requires explicit API version declaration
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pi4_gpio_node")
client.on_connect = on_connect
client.on_message = on_message
try:
client.connect(BROKER_IP, BROKER_PORT, keepalive=60)
except ConnectionRefusedError as e:
logging.critical(f"Broker connection failed: {e}. Is Mosquitto running?")
sys.exit(1)
except Exception as e:
logging.critical(f"Unexpected connection error: {e}")
sys.exit(1)
# Bind hardware events to MQTT publishers
limit_switch.when_activated = lambda: publish_door_state(client, True)
limit_switch.when_deactivated = lambda: publish_door_state(client, False)
# Start MQTT network loop in background thread
client.loop_start()
# Publish initial state on boot
publish_door_state(client, limit_switch.is_active)
try:
while True:
time.sleep(1) # Keep main thread alive
except KeyboardInterrupt:
logging.info("Shutting down gracefully...")
client.loop_stop()
dust_relay.off()
client.disconnect()
if __name__ == "__main__":
main()
Debugging: Connection Refused and MQTT Failures
When deploying this script, the most frequent failure mode occurs during the client.connect() handshake. If your script crashes immediately, look for this exact traceback:
ConnectionRefusedError: [Errno 111] Connection refused
This means the Python socket reached port 1883 on localhost, but no service accepted the TCP handshake. Here are the ranked causes and fixes:
- Mosquitto is not installed or not running.
Fix: Install viasudo apt install mosquitto mosquitto-clients. Verify it is active withsystemctl status mosquitto. - Anonymous access is disabled in Mosquitto v2+.
Fix: Mosquitto 2.0 defaults to denying anonymous connections. Edit/etc/mosquitto/conf.d/default.confand addallow_anonymous true, then restart the service withsudo systemctl restart mosquitto. - Listener is bound to a specific interface, not localhost.
Fix: Ensure your Mosquitto config containslistener 1883 127.0.0.1. If it is bound only to an external IP or IPv6, the 127.0.0.1 connection will be refused.
The First Three Things to Check When It Fails
If the script connects but Home Assistant isn't reacting, run this diagnostic triage:
- Verify the Broker is Listening: Run
sudo netstat -tulpn | grep 1883. You must seemosquittolistening on127.0.0.1:1883or0.0.0.0:1883. - Sniff the Traffic: Open a second terminal and run
mosquitto_sub -h 127.0.0.1 -t "#" -v. Physically trigger the limit switch. If you see the payload print in the terminal, the Pi script is working, and the fault lies in the Home Assistant MQTT integration configuration. - Check HA MQTT Discovery: Ensure the Home Assistant MQTT Integration is configured to point to the exact same broker IP. If HA is running in a Docker container on the same Pi, it cannot use
127.0.0.1to reach the host's Mosquitto; it must use the Pi's local LAN IP (e.g.,192.168.1.50) or the Docker host gateway.
Extending or Simplifying the Build
Depending on your workshop needs, you may want to adjust the complexity of this architecture.
How to Simplify: The ESPHome Route
Running Python scripts on the Home Assistant host introduces OS-level dependencies and SD card write wear. To simplify, move the GPIO logic off the Pi entirely. Wire the limit switch and relay to an ESP32-WROOM-32 dev board, flash it with ESPHome, and let the ESP32 handle the MQTT publishing over WiFi. The Raspberry Pi then acts strictly as the Home Assistant server, which is its intended role.
How to Extend: Adding Zigbee and Thread
If you want to expand beyond wired GPIO to wireless sensors, plug a Sonoff Zigbee 3.0 USB Dongle Plus (P-version) into the Pi. Critical wiring note: Plug the dongle into a USB 2.0 port (the black ones on the Pi 4) or use a 1-meter USB 2.0 extension cable. The Pi 4's USB 3.0 ports (blue) generate massive 2.4GHz RF noise that will deafen the Zigbee coordinator, causing phantom dropouts in your mesh network. Configure the Raspberry Pi USB subsystem to pass through to the Zigbee2MQTT Docker container.
Frequently Asked Questions
Can I run Home Assistant OS and still use the Raspberry Pi GPIO pins?
Not easily. Home Assistant OS (HAOS) uses a read-only squashfs root filesystem and a highly restricted Docker environment designed for appliance-like stability. You cannot simply SSH in and run a persistent Python GPIO script. To use bare-metal GPIO pins directly on the Pi, you must install Raspberry Pi OS (Bookworm), install Docker, and run Home Assistant Container or Supervised. If you are locked into HAOS, you must build a custom Home Assistant Add-on that maps the /dev/gpiomem device into the container, which is significantly more complex than the Python script provided above.
How do I connect a Raspberry Pi with Home Assistant to Zigbee devices?
You need a Zigbee coordinator USB stick, like the Sonoff Zigbee 3.0 USB Dongle Plus or the Home Assistant SkyConnect. Plug it into the Pi, install the Zigbee2MQTT or ZHA integration in Home Assistant, and configure the serial port path (usually /dev/ttyACM0). As mentioned in the extension section, always use a USB 2.0 port or an extension cable to avoid 2.4GHz interference from the Pi's USB 3.0 bus and WiFi chip.
Is a Raspberry Pi 5 better than a Pi 4 for running Home Assistant in 2026?
For pure Home Assistant database operations and dashboard rendering, the Raspberry Pi 5's quad-core Cortex-A76 offers a noticeable speed bump over the Pi 4's Cortex-A72, especially when loading heavy Lovelace dashboards with dozens of camera feeds. However, the Pi 5 changed the GPIO architecture, deprecating the legacy RPi.GPIO library in favor of libgpiod. If you use a Pi 5 for this exact Python build, you must install the rpi-lgpio package (pip3 install rpi-lgpio) so that gpiozero can correctly interface with the new pin controller. For most dedicated smart home servers, a used Pi 4 4GB remains the most cost-effective and software-compatible workhorse.






