When evaluating smart home raspberry pi projects for physical load control, the most reliable architecture separates the logic layer from the switching layer using MQTT. While microcontrollers like the ESP32 are great for remote battery-powered sensors, a Raspberry Pi excels as a centralized, wired GPIO relay node due to its robust Linux environment, native Home Assistant compatibility, and ability to handle local database logging without memory constraints.
This guide walks through building a 4-channel MQTT relay controller. We will use the Raspberry Pi 4 Model B (4GB variant) to drive a 5V optocoupler relay module, controlled via Python and a local Mosquitto MQTT broker. The code targets the Raspberry Pi 4 Model B 4GB running Raspberry Pi OS Lite (64-bit, Bookworm release), utilizing the modern gpiozero library to avoid legacy permission traps.
Project Spec Sheet & Hardware Requirements
Before ordering parts, note that relay modules vary wildly in trigger logic. This build assumes an Active LOW relay module with optocouplers, which is the safest and most common variant for Pi GPIO integration. Active LOW means the relay engages when the GPIO pin is pulled to ground (0V), preventing accidental relay triggering during Pi boot sequences when pins float.
| Component | Exact Variant / Specification | Estimated Cost (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (4GB RAM) | $55.00 | 4GB prevents OOM kills if running local MQTT broker and HA sidecars. |
| Relay Module | 4-Channel 5V Relay with Optocouplers (Active LOW) | $8.50 | Must have JD-VCC jumper removed for true optical isolation. |
| Storage | 32GB MicroSD (SanDisk Extreme A2) | $12.00 | A2 class is mandatory for I/O queue handling in Linux. |
| Power Supply | Official 27W USB-C Power Supply | $18.00 | Prevents under-voltage brownouts when 4 relays click simultaneously. |
| Wiring | 22 AWG Solid Core Jumper Wires (Female-to-Female) | $5.00 | Use solid core for secure breadboard/terminal block connections. |
Wiring & Pin Mapping
We use BCM (Broadcom) pin numbering, which is the standard for modern Python GPIO libraries. Do not use physical board pin numbers, as they change between Pi revisions. The 5V pin on the Pi (Physical Pin 2) powers the relay coil side, while the 3.3V logic from the GPIO pins triggers the optocoupler LEDs.
| Relay Module Pin | Pi 4B Physical Pin | Pi 4B BCM GPIO | Function / Load |
|---|---|---|---|
| VCC | 2 (5V Power) | N/A | Relay coil power (5V) |
| GND | 6 (Ground) | N/A | Common ground reference |
| IN1 | 11 | GPIO 17 | Living Room Lights |
| IN2 | 13 | GPIO 27 | Kitchen Under-Cabinet LEDs |
| IN3 | 15 | GPIO 22 | Bedroom Blinds Motor |
| IN4 | 16 | GPIO 23 | Porch Exhaust Fan |
Python MQTT Controller Code
This script uses gpiozero for hardware abstraction and paho-mqtt for broker communication. It listens to the smarthome/relays/# topic and expects a simple JSON payload. Ensure you install the dependencies via pip install paho-mqtt gpiozero.
import paho.mqtt.client as mqtt
from gpiozero import OutputDevice
import json
import time
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- PIN DEFINITIONS (BCM) ---
# Using OutputDevice with active_state=False handles Active LOW logic automatically
RELAY_PINS = {
"living_light": OutputDevice(17, active_high=False, initial_value=False),
"kitchen_led": OutputDevice(27, active_high=False, initial_value=False),
"bedroom_blind": OutputDevice(22, active_high=False, initial_value=False),
"porch_fan": OutputDevice(23, active_high=False, initial_value=False)
}
# --- MQTT CONFIGURATION ---
BROKER_IP = "192.168.1.50" # Replace with your local Mosquitto broker IP
BROKER_PORT = 1883
TOPIC_SUBSCRIBE = "smarthome/relays/command"
CLIENT_ID = "pi4_relay_node_01"
def on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
logging.info("Connected to MQTT Broker successfully.")
client.subscribe(TOPIC_SUBSCRIBE, qos=1)
else:
logging.error(f"Failed to connect, return code {rc}")
def on_message(client, userdata, msg):
try:
payload = json.loads(msg.payload.decode())
target_relay = payload.get("device")
state = payload.get("state")
if target_relay in RELAY_PINS:
if state == "ON":
RELAY_PINS[target_relay].on()
logging.info(f"Engaged relay: {target_relay}")
elif state == "OFF":
RELAY_PINS[target_relay].off()
logging.info(f"Disengaged relay: {target_relay}")
else:
logging.warning(f"Invalid state command: {state}")
else:
logging.warning(f"Unknown device requested: {target_relay}")
except json.JSONDecodeError:
logging.error("Received invalid JSON payload.")
except Exception as e:
logging.error(f"Error processing message: {e}")
def main():
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.on_message = on_message
try:
client.connect(BROKER_IP, BROKER_PORT, keepalive=60)
logging.info("Starting MQTT loop...")
client.loop_forever()
except ConnectionRefusedError:
logging.critical(f"Connection refused. Is Mosquitto running on {BROKER_IP}?")
except KeyboardInterrupt:
logging.info("Shutdown signal received.")
finally:
logging.info("Cleaning up GPIO and disconnecting...")
for relay in RELAY_PINS.values():
relay.off()
relay.close()
client.disconnect()
if __name__ == "__main__":
main()
Debugging: First Three Things to Check When It Fails
Embedded Linux environments introduce failure modes you don't see on bare-metal microcontrollers. If your relays aren't clicking or the script crashes, follow this triage sequence.
1. The Exact Error: RuntimeError: No access to /dev/mem. Try running as root!
Ranked Causes:
- Using legacy RPi.GPIO on Bookworm: Modern Raspberry Pi OS (Bookworm and later) restricts
/dev/memaccess for security. The legacyRPi.GPIOlibrary often triggers this if not run withsudo. - User not in the gpio group: If you must use legacy libraries, your user lacks hardware permissions.
The Fix: This is exactly why the code above uses gpiozero. It defaults to the lgpio backend on modern Pi OS, which uses the standard /dev/gpiochip0 character device and does not require root privileges. If you are copying old tutorials that use import RPi.GPIO as GPIO, rewrite them using gpiozero.
2. The Exact Error: ConnectionRefusedError: [Errno 111] Connection refused
Ranked Causes:
- Mosquitto is not running or bound to localhost only: By default, modern Mosquitto installs do not allow external connections without a config file.
- Wrong IP address: The Pi is trying to reach a broker that doesn't exist on the subnet.
The Fix: SSH into your MQTT broker machine. Edit /etc/mosquitto/conf.d/default.conf and add listener 1883 and allow_anonymous true (for local testing only). Restart the service with sudo systemctl restart mosquitto.
3. Relays Click Randomly on Boot or Shutdown
Ranked Causes:
- GPIO Floating during Boot: Before Linux loads the device tree, Pi GPIO pins float, which can trigger sensitive optocouplers.
- Active HIGH vs Active LOW mismatch: The code assumes Active LOW, but the physical module is Active HIGH.
The Fix: Verify your relay module. If it's Active LOW, ensure your Python code initializes with active_high=False (as shown above). Physically, you can also add 10kΩ pull-up resistors between the GPIO pins and the 3.3V rail to hold them high during boot.
Before connecting the relay module to the Pi, power the Pi and run a simple script to toggle the pins. Use a multimeter in DC Voltage mode. Probe the GPIO pin and ground. You should see it swing cleanly between ~3.2V (High) and ~0.05V (Low). If it reads 1.5V or floats, you have a wiring fault or a blown GPIO trace.
Extending or Simplifying the Build
How to Simplify: If you don't want to manage a separate MQTT broker and Python scripts, install Home Assistant OS directly onto the Raspberry Pi. You can then use the built-in Raspberry Pi GPIO integration via YAML configuration. This eliminates the need for custom Python code and external MQTT routing, though it ties the hardware directly to the HA instance.
How to Extend: To make this a true environmental node, add an I2C BME280 sensor (SDA to GPIO 2, SCL to GPIO 3) to log room temperature and humidity alongside the relay states. You can also implement MQTT Last Will and Testament (LWT) in the Python code to publish an "offline" status to your dashboard if the Pi loses network connectivity, ensuring your smart home dashboard never shows a stale "ON" state for a dead node.
FAQ: Smart Home Raspberry Pi Projects
Why use a Raspberry Pi instead of an ESP32 for smart home relay projects?
An ESP32 is cheaper ($6 vs $55) and better for distributed, low-power nodes. However, for a centralized 4-to-8 channel relay controller mounted in a structured wiring panel, the Raspberry Pi wins on connectivity and storage. The Pi offers native Gigabit Ethernet (crucial for reliable MQTT traffic without WiFi interference), a full Linux filesystem for local SQLite database logging, and the ability to run Docker containers like Zigbee2MQTT or Home Assistant Core alongside your relay script.
How do I integrate this Pi relay controller with Home Assistant?
In Home Assistant, navigate to Settings > Devices & Services > MQTT. Add an MQTT Switch entity for each relay in your configuration.yaml. Set the command_topic to smarthome/relays/command and configure a payload template that formats the JSON exactly as the Python script expects: {"device": "living_light", "state": "{{ 'ON' if value == 'ON' else 'OFF' }}"}. This bridges the HA UI to the Pi's GPIO seamlessly.
What happens to the relays if the Raspberry Pi loses power or reboots?
By default, when the Pi loses power, the GPIO pins lose their state, and the relays will disengage (turn off) because the optocoupler LEDs lose their ground path. When the Pi reboots, the Python script must be configured as a systemd service to restart automatically. If you require the relays to maintain their last known state during a reboot, you must implement a state-saving mechanism (writing the state to a local JSON file on every MQTT message) and read that file on script startup before connecting to the broker.






