Most raspberry pi home automation projects online rely on outdated Pi 3 hardware and the legacy RPi.GPIO library. In 2026, with the Raspberry Pi 5 and the shift to the lgpio backend in Raspberry Pi OS Bookworm, building a robust multi-zone relay controller requires updated power budgeting and modern Python libraries. While an ESP32 is great for simple remote sensors, a Raspberry Pi excels when you need local processing, simultaneous Home Assistant integration, or complex USB peripheral handling.

This guide walks through building a 4-channel MQTT relay node. It targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm, using gpiozero for hardware abstraction and paho-mqtt v2.0 for broker communication. You will get exact pin mappings, a production-ready Python script with error handling, and the specific debugging steps for when the hardware inevitably misbehaves.

Hardware Spec Sheet and GPIO Pin Mapping

Before touching a single jumper wire, you must verify your power budget. The Raspberry Pi 5 requires a 27W USB-C PD power supply (5V/5A) to maintain full peripheral current limits. A standard 4-channel relay module draws roughly 70mA per coil. If all four relays engage simultaneously, the sudden current spike can trigger a brownout if your power supply is undersized.

Table 1: Component Specifications and Power Budget
Component Model / Variant Operating Voltage Current Draw (Max) Notes
Microcontroller Raspberry Pi 5 (4GB) 5V DC (USB-C PD) 2.5A (Idle/Peak mix) Requires official 27W PSU for full 1.2A peripheral budget
Relay Module Songle SRD-05VDC-SL-C (4-Ch) 5V DC (Coil) ~280mA (All 4 coils) Optocoupler isolated, Active-LOW logic trigger
Optocoupler PC817 (On-module) 1.2V Forward ~20mA per channel Provides 5000Vrms isolation between logic and coil
Logic Level Shifter Bi-directional (Optional) 3.3V to 5V Negligible Recommended if using older non-optocoupler relay boards

The Raspberry Pi 5 GPIO operates at 3.3V. Because our chosen relay module features built-in PC817 optocouplers, the 3.3V logic from the Pi is sufficient to trigger the internal IR LED, which then switches the 5V relay coil. This eliminates the need for a logic level shifter.

Table 2: GPIO Pin Mapping (40-Pin Header)
Relay Module Pin Raspberry Pi 5 Pin BCM GPIO Number Function
VCC (Logic) Pin 1 3.3V Power Powers the optocoupler IR LEDs
GND Pin 6 Ground Common ground reference
IN1 Pin 11 GPIO 17 Relay 1 Trigger (Active LOW)
IN2 Pin 13 GPIO 27 Relay 2 Trigger (Active LOW)
IN3 Pin 15 GPIO 22 Relay 3 Trigger (Active LOW)
IN4 Pin 16 GPIO 23 Relay 4 Trigger (Active LOW)
JD-VCC (Coil) External 5V Source N/A Powers the physical relay coils (See wiring note)

Assembly and Mains Safety Wiring

⚠️ Mains Voltage Warning: If you are switching 120V/240V AC loads (lights, pumps, heaters), de-energize the circuit at the breaker panel and verify it is dead with a non-contact voltage tester and a multimeter. Local electrical codes may require a licensed electrician for permanent mains wiring. Never route low-voltage GPIO wires in the same conduit as mains voltage.
  1. Remove the JD-VCC Jumper: This is the most common mistake in raspberry pi home automation projects. On the relay module, locate the blue jumper cap labeled JD-VCC and remove it. This separates the logic circuit from the relay coil circuit.
  2. Wire the Logic Side: Connect the Pi's 3.3V (Pin 1) to the module's VCC. Connect Pi GND (Pin 6) to the module's GND. Connect GPIO 17, 27, 22, and 23 to IN1 through IN4 respectively.
  3. Wire the Coil Side: Connect an external 5V power supply (like a spare USB buck converter) to the JD-VCC pin and the module's GND pin. This ensures that when the relay coils energize and create back-EMF, the voltage droop does not reset your Pi 5.
  4. Connect the Loads: Use the Common (COM) and Normally Open (NO) screw terminals on the Songle relays to switch your target loads. Leave the Normally Closed (NC) terminals empty unless your application requires fail-safe operation.

Complete Python MQTT Control Code

This script uses gpiozero (which leverages the lgpio backend in Bookworm) and paho-mqtt v2.0. It listens for ON and OFF payloads on specific MQTT topics. Ensure you install the dependencies via pip install gpiozero paho-mqtt inside a virtual environment.

import paho.mqtt.client as mqtt
from gpiozero import OutputDevice
import time
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# Target: Raspberry Pi 5 (Bookworm OS)
# BCM GPIO Pin Definitions
RELAY_PINS = [17, 27, 22, 23]

# Initialize relays. active_high=False because optocoupler modules trigger on LOW
relays = [OutputDevice(pin, active_high=False, initial_value=False) for pin in RELAY_PINS]

# MQTT Configuration
MQTT_BROKER = '192.168.1.100'  # Replace with your Mosquitto broker IP
MQTT_PORT = 1883
TOPIC_PREFIX = 'home/relays/'  # Topics will be home/relays/1, home/relays/2, etc.

def on_connect(client, userdata, flags, rc, properties=None):
    """Callback for when the client receives a CONNACK response from the server."""
    if rc == 0:
        logging.info('Successfully connected to MQTT Broker')
        # Subscribe to all 4 relay channels
        for i in range(len(RELAY_PINS)):
            topic = f'{TOPIC_PREFIX}{i+1}'
            client.subscribe(topic)
            logging.info(f'Subscribed to {topic}')
    else:
        logging.error(f'Connection failed with result code {rc}')

def on_message(client, userdata, msg):
    """Callback for when a PUBLISH message is received from the server."""
    try:
        # Extract channel number from topic (e.g., 'home/relays/2' -> 2)
        channel = int(msg.topic.split('/')[-1]) - 1
        payload = msg.payload.decode('utf-8').strip().upper()
        
        if 0 <= channel < len(relays):
            if payload == 'ON':
                relays[channel].on()
                logging.info(f'Relay {channel + 1} turned ON')
            elif payload == 'OFF':
                relays[channel].off()
                logging.info(f'Relay {channel + 1} turned OFF')
            else:
                logging.warning(f'Unknown payload received: {payload}')
    except ValueError:
        logging.error(f'Failed to parse channel number from topic: {msg.topic}')
    except Exception as e:
        logging.error(f'Error processing message: {e}')

def main():
    # Paho MQTT v2.0 requires explicit callback API version
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    client.on_connect = on_connect
    client.on_message = on_message

    try:
        logging.info(f'Connecting to broker at {MQTT_BROKER}:{MQTT_PORT}...')
        client.connect(MQTT_BROKER, MQTT_PORT, 60)
        # loop_forever handles reconnections automatically
        client.loop_forever()
    except ConnectionRefusedError:
        logging.error('ConnectionRefusedError: [Errno 111] Connection refused. Is Mosquitto running?')
    except KeyboardInterrupt:
        logging.info('Shutdown signal received...')
    except Exception as e:
        logging.critical(f'Unexpected error: {e}')
    finally:
        # Safe cleanup: turn off all relays and close GPIO pins
        logging.info('Cleaning up GPIO pins...')
        for r in relays:
            r.off()
            r.close()

if __name__ == '__main__':
    main()

Debugging: First Three Things to Check When It Fails

When your raspberry pi home automation projects fail to trigger loads, do not immediately rewrite the code. Hardware and network boundaries are usually the culprits. Here are the first three things to check, ranked by likelihood.

1. The MQTT Broker is Unreachable

Exact Error String: ConnectionRefusedError: [Errno 111] Connection refused

Causes & Fixes:

  • Mosquitto is not running: SSH into your broker machine and run sudo systemctl status mosquitto. If it is dead, start it. Check Mosquitto documentation for listener configuration if running on a non-standard port.
  • Firewall blocking port 1883: Run sudo ufw allow 1883/tcp on the broker host.
  • Wrong IP Address: Verify MQTT_BROKER in the Python script matches the static IP of your broker. Do not rely on mDNS (.local) for production automation nodes; use static IPs.

2. GPIO Permissions and Library Mismatches

Exact Error String: ModuleNotFoundError: No module named 'lgpio' OR lgpio.error: 'gpiochip4' is not a valid chip

Causes & Fixes:

  • Missing Backend: Raspberry Pi OS Bookworm deprecated RPi.GPIO. gpiozero now defaults to lgpio. Install it via sudo apt install python3-lgpio or pip install lgpio in your venv.
  • Permissions: Ensure your user is in the gpio group (sudo usermod -aG gpio $USER), then log out and back in. Never run automation scripts as root via sudo; it breaks virtual environments and creates security risks.

3. Power Supply Brownouts

Exact Error String: Kernel log shows Under-voltage detected! or the Pi randomly reboots when relays click.

Causes & Fixes:

  • Back-EMF Spikes: If you ignored the JD-VCC jumper removal step, the inductive kickback from the relay coils is dragging the Pi's 5V rail down. Separate the logic and coil power supplies as detailed in the assembly steps.
  • Undersized PSU: Verify you are using the official Raspberry Pi 27W USB-C PD power supply. Third-party phone chargers often drop voltage below 4.8V when the Pi requests peak current.

Scaling the Build: Extend or Simplify

Not every node needs a 4-channel MQTT setup. Here is how to adapt this architecture based on your deployment constraints.

How to Simplify

If you only need to control a single 12V DC water valve or a simple fan, drop the MQTT broker entirely. Replace the paho-mqtt loop with a lightweight FastAPI HTTP server. This allows you to trigger the relay via simple webhooks (e.g., from a motion camera or a smart button) without maintaining a dedicated Mosquitto broker. You can also swap the Pi 5 for a Raspberry Pi Zero 2 W to reduce power consumption to under 1.5W idle.

How to Extend

To turn this into a comprehensive environmental controller, add an I2C sensor like the BME280. Wire the sensor's SDA and SCL pins to GPIO 2 (Pin 3) and GPIO 3 (Pin 5). Using the adafruit-circuitpython-bme280 library, you can read temperature and humidity every 60 seconds and publish it to an home/climate/sensor1 MQTT topic. Home Assistant can then subscribe to that topic and automatically publish an ON command back to home/relays/1 if the temperature exceeds 24°C, creating a fully localized, closed-loop climate control system that survives internet outages.