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.
Default Recommendation: For a robust, 4-to-8 channel hardwired controller, build around the Raspberry Pi 5 (8GB) running Raspberry Pi OS Lite, paired with a Waveshare 4-Channel 3.3V Relay Module and Eclipse Mosquitto as the local broker.

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

⚠️ HIGH VOLTAGE SAFETY WARNING: The relay module switches mains voltage (120V/240V AC). Never wire the NO/COM/NC screw terminals while the circuit is energized. De-energize the breaker, verify dead with a CAT III multimeter, and use proper ferrule crimps on stranded wire. If you are not comfortable with mains wiring, hire a licensed electrician. Local electrical codes (NEC/IEC) dictate enclosure requirements for mains-voltage switching.
  1. 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.
  2. 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.
  3. 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
  4. 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
  5. Install Python Dependencies: We use gpiozero (which automatically handles the Pi 5's RP1 chip via the lgpio backend) 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

Target Board Variant: This code is explicitly written for the Raspberry Pi 5 (8GB) running 64-bit Bookworm. It utilizes the Paho MQTT v2.0 API (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 mosquitto and check journalctl -u mosquitto.
  • Cause 2: Mosquitto 2.0+ default security restriction. If you forgot to add the listener 1883 0.0.0.0 config, it only accepts local connections via Unix socket or specific IPv6 bindings. Fix: Verify /etc/mosquitto/conf.d/local.conf exists 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 python and kill -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 conflicting dtoverlay lines.
The First 3 Things to Check When the System Fails:
  1. Broker Health: Run mosquitto_sub -h localhost -t '#' -v in a separate terminal to verify the broker is actively routing messages.
  2. Zombie Processes: Run sudo lsof -i :1883 and sudo fuser /dev/gpiochip0 to see exactly what is holding the network port and the GPIO chip.
  3. 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 than throttled=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.