The Local-First Decision Matrix
Running home automation on Raspberry Pi hardware shifts control away from cloud-dependent walled gardens back to your local network. When designing a hardwired relay controller for 120V/240V loads (like lighting circuits, HVAC contactors, or irrigation valves), you must choose between microcontroller nodes (ESP32) and a centralized single-board computer (SBC).
| Decision Node | Option A | Option B | Winner & Rationale |
|---|---|---|---|
| Architecture | Distributed ESP32 Nodes | Centralized Raspberry Pi | Raspberry Pi. Centralized MQTT processing reduces WiFi congestion and simplifies state management for <16 relay channels. |
| Pi Model | Raspberry Pi 4 (4GB) | Raspberry Pi 5 (8GB) | Pi 5 (8GB). The Pi 5's PCIe lane and faster I/O handle Mosquitto, Home Assistant, and Python scripts without thermal throttling under load. |
| Relay Driver | Standard 5V Relay Board (JD-VCC) | 3.3V Opto-Isolated Relay | 3.3V Opto-Isolated. Standard 5V boards backfeed the Pi's 3.3V rail through optocoupler LEDs, risking silicon damage. Native 3.3V boards eliminate this. |
| Protocol | HTTP REST API | MQTT (Pub/Sub) | MQTT. Persistent connections and low overhead make it the undisputed standard for local smart home state syncing. |
Hardware Spec Sheet & Pin Mapping
The following bill of materials (BOM) assumes you are building a dedicated relay node. Prices reflect typical 2026 market rates for authentic components.
| Component | Exact Variant / Part Number | Est. Cost | Notes |
|---|---|---|---|
| SBC | Raspberry Pi 5 (8GB RAM) | $80.00 | Requires 27W USB-C PD power supply for full peripheral current. |
| Relay Module | Waveshare 4-CH Relay Module (3.3V) | $12.50 | Native 3.3V logic, opto-isolated, 10A/250VAC contacts. |
| Wiring | Female-to-Female Dupont (20cm) | $3.00 | Use 22 AWG silicone wire for high-vibration environments. |
| Storage | Samsung PRO Endurance 64GB microSD | $14.00 | Endurance-rated for continuous MQTT logging and OS writes. |
GPIO Pin Mapping (BCM Numbering)
The Raspberry Pi 5 uses the RP1 southbridge chip, but the standard BCM GPIO mapping remains backward-compatible for software. We use BCM numbering, not physical pin numbers.
| Relay Channel | BCM GPIO | Physical Pin | MQTT Topic Suffix |
|---|---|---|---|
| IN1 (Living Room) | 17 | 11 | home/living_room/set |
| IN2 (Kitchen) | 27 | 13 | home/kitchen/set |
| IN3 (Garage) | 22 | 15 | home/garage/set |
| IN4 (HVAC) | 23 | 16 | home/hvac/set |
| VCC (3.3V) | 3.3V PWR | 1 | N/A |
| GND | GND | 9 | N/A |
Step-by-Step Wiring & MQTT Setup
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit, Bookworm) to the microSD card. Enable SSH and configure WiFi in the advanced settings.
- Wire the Logic Side: Connect Pi Physical Pin 1 (3.3V) to Relay VCC. Connect Pi Physical Pin 9 (GND) to Relay GND. Connect BCM 17, 27, 22, and 23 to IN1, IN2, IN3, and IN4 respectively.
- Install Mosquitto Broker: SSH into the Pi and run:
sudo apt update sudo apt install mosquitto mosquitto-clients -y sudo systemctl enable mosquitto sudo systemctl start mosquitto - Configure Mosquitto for Local Network: By default, Mosquitto 2.0+ only binds to localhost. Create a config file to allow Home Assistant or other local devices to publish commands:
echo 'listener 1883 0.0.0.0 allow_anonymous true' | sudo tee /etc/mosquitto/conf.d/local.conf sudo systemctl restart mosquitto - Install Python Dependencies: We use
gpiozero(which automatically handles the Pi 5's RP1 chip via thelgpiobackend) and the Paho MQTT v2 client.sudo apt install python3-gpiozero python3-lgpio -y pip3 install paho-mqtt --break-system-packages
Python MQTT Relay Control Script
CallbackAPIVersion.VERSION2), which is mandatory for versions installed via pip in 2025/2026. Using v1 API syntax will throw a deprecation or signature error.
import paho.mqtt.client as mqtt
from gpiozero import OutputDevice
import logging
import time
import sys
# --- Configuration ---
MQTT_BROKER = 'localhost'
MQTT_PORT = 1883
MQTT_BASE_TOPIC = 'home/'
# Pin Definitions (BCM)
RELAY_MAPPING = {
'living_room': 17,
'kitchen': 27,
'garage': 22,
'hvac': 23
}
# --- Setup Logging ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
# --- Initialize GPIO Relays ---
# active_high=False assumes the relay triggers on LOW (standard for most opto boards)
relays = {}
for name, pin in RELAY_MAPPING.items():
try:
relays[name] = OutputDevice(pin, active_high=False, initial_value=False)
logging.info(f'Initialized relay {name} on BCM {pin}')
except Exception as e:
logging.error(f'Failed to initialize BCM {pin}: {e}')
sys.exit(1)
# --- MQTT Callbacks (Paho v2 API) ---
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
logging.info('Connected to MQTT Broker')
# Subscribe to all set topics
for name in RELAY_MAPPING.keys():
topic = f'{MQTT_BASE_TOPIC}{name}/set'
client.subscribe(topic)
logging.info(f'Subscribed to {topic}')
else:
logging.error(f'Connection failed with code: {reason_code}')
def on_message(client, userdata, msg):
payload = msg.payload.decode('utf-8').strip().upper()
# Extract room name from topic: 'home/kitchen/set' -> 'kitchen'
room = msg.topic.split('/')[1]
if room in relays:
if payload == 'ON':
relays[room].on()
logging.info(f'Relay {room} turned ON')
elif payload == 'OFF':
relays[room].off()
logging.info(f'Relay {room} turned OFF')
else:
logging.warning(f'Invalid payload for {room}: {payload}')
else:
logging.warning(f'Unknown room in topic: {msg.topic}')
def on_disconnect(client, userdata, flags, reason_code, properties):
logging.warning(f'Disconnected from broker (Code: {reason_code}). Attempting reconnect...')
# Ensure all relays drop to safe state (OFF) on disconnect
for name, relay in relays.items():
relay.off()
logging.info(f'Fail-safe: Relay {name} turned OFF')
# --- Main Execution ---
if __name__ == '__main__':
client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id='pi5_relay_controller'
)
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
# Enable automatic reconnect
client.reconnect_delay_set(min_delay=1, max_delay=30)
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
logging.info('Starting MQTT loop...')
client.loop_forever()
except KeyboardInterrupt:
logging.info('Shutdown requested by user.')
except Exception as e:
logging.critical(f'Fatal error in MQTT loop: {e}')
finally:
# Clean up GPIO on exit
for name, relay in relays.items():
relay.off()
relay.close()
logging.info('GPIO cleaned up. Exiting.')
Debugging: Connection Refused & GPIO Lockups
When deploying home automation on Raspberry Pi hardware, you will inevitably hit environment-specific errors. Here are the exact strings and ranked causes for the two most common failures.
Error 1: ConnectionRefusedError: [Errno 111] Connection refused
This occurs during client.connect() when the Python script cannot reach the Mosquitto broker.
- Cause 1 (Most Likely): Mosquitto is not running or crashed. Fix:
sudo systemctl status mosquittoand checkjournalctl -u mosquitto. - Cause 2: Mosquitto 2.0+ default security restriction. If you forgot to add the
listener 1883 0.0.0.0config, it only accepts local connections via Unix socket or specific IPv6 bindings. Fix: Verify/etc/mosquitto/conf.d/local.confexists and restart the service. - Cause 3: Port 1883 is blocked by UFW (Uncomplicated Firewall). Fix:
sudo ufw allow 1883/tcp.
Error 2: gpiozero.exc.GPIOPinInUse: pin 17 is already in use
The gpiozero library (via the lgpio backend on Pi 5) strictly enforces single-process pin ownership.
- Cause 1 (Most Likely): A zombie Python process from a previous run is still holding the pin. Fix: Run
ps aux | grep pythonandkill -9 <PID>. - Cause 2: Another automation service (like Node-RED or Home Assistant's direct GPIO integration) is configured to use BCM 17. Fix: Disable the conflicting service or change your pin mapping.
- Cause 3: The pin is reserved by a device tree overlay in
/boot/firmware/config.txt. Fix: Check for conflictingdtoverlaylines.
- Broker Health: Run
mosquitto_sub -h localhost -t '#' -vin a separate terminal to verify the broker is actively routing messages. - Zombie Processes: Run
sudo lsof -i :1883andsudo fuser /dev/gpiochip0to see exactly what is holding the network port and the GPIO chip. - Power Supply Brownouts: The Pi 5 will throttle or drop USB/GPIO stability if the power supply sags. Check
vcgencmd get_throttled. If it returns anything other thanthrottled=0x0, your power supply or cable is inadequate.
Extending or Simplifying the Build
Depending on your project scope, you may need to scale this architecture up or strip it down.
How to Extend (Scaling to 16+ Channels)
The Pi 5 has 27 usable GPIO pins, but running 16 individual wires to a relay board creates a rats' nest and risks loose connections vibrating out of the headers.
The Upgrade Path: Switch from parallel GPIO wiring to an I2C GPIO Expander (MCP23017). The MCP23017 provides 16 additional I/O pins using only the Pi's I2C bus (BCM 2 and BCM 3). You can chain up to 8 MCP23017 chips on a single bus, giving you 128 relay channels. In Python, replace gpiozero.OutputDevice with the adafruit-circuitpython-mcp230xx library. Ensure you add 4.7kΩ pull-up resistors to the SDA and SCL lines.
How to Simplify (The 1-Channel Appliance Hack)
If you only need to control a single 120V appliance (like a workshop dust collector or a water heater) and don't want to manage MQTT topics, strip the build down to a local HTTP server.
The Downgrade Path: Remove Mosquitto entirely. Install Flask (pip3 install flask). Create a single route (@app.route('/toggle')) that flips the gpiozero state. This reduces the software stack to just the OS and Python, eliminating broker maintenance, though you lose the persistent state-syncing benefits of MQTT integration with Home Assistant.
For further reading on MQTT integration standards, refer to the Home Assistant MQTT Integration documentation. For hardware-specific GPIO changes on the newest silicon, consult the Raspberry Pi 5 Hardware Documentation regarding the RP1 southbridge chip.






