When evaluating raspberry pi projects for home automation, the biggest mistake makers make is treating the Pi like a microcontroller. It is not an Arduino. It is a full Linux computer, which means you must design for OS-level delays, network stack failures, and file-system corruption. For a dedicated, hardwired climate and relay control hub, the concrete default recommendation is the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS (Bookworm) 64-bit, utilizing I2C for local sensors and MQTT for network integration.

This guide walks through building a Pi 5 hub that reads a BME280 environmental sensor and triggers a relay based on temperature thresholds, publishing state changes to a local Mosquitto MQTT broker. We will cover the exact wiring, the Pi 5-specific Python code, and how to debug the inevitable I2C and GPIO failures.

The Decision Matrix: Which Pi and Protocol for Your Hub?

Before buying parts, run your use case through this decision tree. Do not default to the most expensive board if your workload doesn't demand it.

Condition / Workload Recommended Board Recommended Protocol
Running local AI vision (Frigate) or voice (Piper/Whisper) Raspberry Pi 5 (8GB) Ethernet + USB Zigbee Coordinator
Simple battery-powered MQTT sensor node (no local relay) Raspberry Pi Zero 2 W WiFi (MQTT over TLS)
Dedicated hardwired climate/relay control hub (This Build) Raspberry Pi 5 (4GB) Wired I2C + Local MQTT
Concrete Pick: If you are building a stationary, mains-powered hub that directly switches relays and reads local I2C sensors without running heavy AI models, buy the Pi 5 4GB. The 8GB model runs hotter and wastes power for this specific workload, while the Zero 2 W lacks the native USB 3.0 and PCIe bandwidth if you later add a Zigbee coordinator or NVMe HAT.

Parts List & Spec Sheet for the Pi 5 Climate-Relay Hub

Prices reflect typical 2026 retail availability. Do not substitute the power supply; the Pi 5 requires USB-C PD 5V/5A to enable full peripheral current limits.

Component Exact Variant / Model Est. Price Why This Part?
Compute Board Raspberry Pi 5 (4GB) $60 RP1 southbridge handles I/O natively; 4GB is plenty for Python/MQTT.
Power Supply Official 27W USB-C PD (5V/5A) $12 Required to prevent brownouts when switching relays.
Sensor Adafruit BME280 I2C (Adafruit 2652) $15 Temp/Humidity/Pressure. 3.3V logic native; no level shifting needed.
Relay Module LCUS-1 3.3V Low-Level Trigger $6 Opto-isolated, explicitly designed for 3.3V Pi logic (unlike standard 5V Songle modules).
Thermal Raspberry Pi Active Cooler $5 PWM controlled via firmware; keeps RP1 chip in safe thermal envelope.

Wiring & Pin Mapping (I2C BME280 + 3.3V Relay)

The Pi 5 uses the standard 40-pin header layout, but the underlying silicon routing to the RP1 chip is different from the Pi 4. Always wire based on BCM GPIO numbers, not just physical pin positions.

Component Pin Pi 5 Physical Pin Pi 5 BCM GPIO Wire Color (Recommended)
BME280 VIN 1 (3.3V) N/A Red
BME280 GND 6 (GND) N/A Black
BME280 SCK (SCL) 5 GPIO 3 (SCL) Yellow
BME280 SDI (SDA) 3 GPIO 2 (SDA) Blue
Relay VCC 17 (3.3V) N/A Red
Relay GND 9 (GND) N/A Black
Relay IN (Signal) 12 GPIO 18 (PWM0) Green
Hardware Warning: Never wire a standard 5V relay module directly to Pi 5 GPIO pins expecting the 3.3V logic to trigger a 5V optocoupler reliably. It often results in floating states and phantom switching. Use a dedicated 3.3V relay module or an NPN transistor (like a 2N2222) to switch the 5V relay coil.

Complete Python Control Script with Error Handling

This script targets Raspberry Pi OS (Bookworm) 64-bit. It uses gpiozero (which natively supports the Pi 5's RP1 chip via the lgpio backend), smbus2 for I2C, and paho-mqtt for network telemetry.

Prerequisites: sudo apt install python3-gpiozero python3-smbus2 python3-paho-mqtt

import time
import json
import smbus2
import bme280
import paho.mqtt.client as mqtt
from gpiozero import OutputDevice
from signal import pause

# --- PIN & I2C DEFINITIONS ---
RELAY_BCM_PIN = 18       # Physical Pin 12
I2C_BUS_ID = 1           # Default I2C bus on Pi 5
BME280_ADDRESS = 0x76    # Default Adafruit BME280 address (check with i2cdetect)

# --- MQTT CONFIG ---
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC_STATE = "home/climate/livingroom/state"
MQTT_TOPIC_COMMAND = "home/climate/livingroom/set"

# Thresholds
TEMP_HIGH_THRESHOLD = 26.0  # Celsius
TEMP_LOW_THRESHOLD = 21.0   # Celsius

# Initialize Hardware
relay = OutputDevice(RELAY_BCM_PIN, active_high=False) # Low-level trigger relay
bus = smbus2.SMBus(I2C_BUS_ID)

def setup_mqtt():
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    client.on_connect = on_connect
    client.on_message = on_message
    try:
        client.connect(MQTT_BROKER, MQTT_PORT, 60)
        client.loop_start()
        return client
    except ConnectionRefusedError as e:
        print(f"[FATAL] MQTT Broker refused connection: {e}")
        raise

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        print("Connected to MQTT Broker")
        client.subscribe(MQTT_TOPIC_COMMAND)
    else:
        print(f"MQTT Connection failed with code {reason_code}")

def on_message(client, userdata, msg):
    payload = msg.payload.decode()
    print(f"Received command: {payload}")
    if payload == "ON":
        relay.on()
    elif payload == "OFF":
        relay.off()

def read_sensor():
    try:
        # Using standard bme280 library calibration and read
        calibration_params = bme280.load_calibration_params(bus, BME280_ADDRESS)
        data = bme280.sample(bus, BME280_ADDRESS, calibration_params)
        return round(data.temperature, 2), round(data.humidity, 2), round(data.pressure, 2)
    except OSError as e:
        print(f"[ERROR] I2C Read Failed: {e}")
        return None, None, None

def main():
    mqtt_client = setup_mqtt()
    print("Hub initialized. Monitoring environment...")
    
    try:
        while True:
            temp, hum, pres = read_sensor()
            
            if temp is not None:
                # Autonomous Relay Logic
                if temp >= TEMP_HIGH_THRESHOLD and not relay.is_active:
                    relay.on()
                    print(f"Temp {temp}C exceeded threshold. Relay ON.")
                elif temp <= TEMP_LOW_THRESHOLD and relay.is_active:
                    relay.off()
                    print(f"Temp {temp}C below threshold. Relay OFF.")
                
                # MQTT Publish
                payload = json.dumps({"temp": temp, "humidity": hum, "pressure": pres, "relay": relay.is_active})
                mqtt_client.publish(MQTT_TOPIC_STATE, payload)
            
            time.sleep(10)
            
    except KeyboardInterrupt:
        print("\nShutting down gracefully...")
    finally:
        relay.off()
        mqtt_client.loop_stop()
        mqtt_client.disconnect()
        bus.close()

if __name__ == "__main__":
    main()

Debugging: First Three Things to Check When It Fails

When your hub crashes or fails to trigger, do not rewrite the code immediately. Check these three exact failure modes, which account for 95% of Pi 5 automation bench issues.

1. The I2C Ghost: OSError: [Errno 121] Remote I/O error

The Symptom: The script crashes on the first bme280.sample() call with an Errno 121.

Ranked Causes:

  1. Wrong Address: The Adafruit BME280 defaults to 0x77, while many generic breakout boards use 0x76. Run i2cdetect -y 1 in the terminal. If you see 77, change BME280_ADDRESS in the code.
  2. Loose Dupont Wires: I2C is highly susceptible to capacitance and physical disconnects. Swap the SDA/SCL wires.
  3. Pull-up Resistor Conflict: If you have multiple I2C devices, their combined pull-up resistors might be dragging the 3.3V line too low. Remove external pull-ups if the breakout board already has them.

2. The Pi 5 GPIO Trap: RuntimeError: Cannot determine SOC peripheral base address

The Symptom: gpiozero or RPi.GPIO throws a base address error on startup.

The Fix: The Pi 5 uses the RP1 southbridge chip for GPIO, breaking legacy libraries. If you are using RPi.GPIO, uninstall it. Ensure you are using gpiozero version 2.0+ and have the lgpio Python binding installed (sudo apt install python3-lgpio). The code provided above uses gpiozero specifically to bypass this Pi 5 hardware quirk.

3. The Broker Wall: ConnectionRefusedError: [Errno 111] Connection refused

The Symptom: Sensor reads fine, but the script dies on client.connect().

Ranked Causes:

  1. Mosquitto Not Running: Run sudo systemctl status mosquitto. If it's dead, start it.
  2. Listener Config Missing: Mosquitto 2.0+ defaults to local-only and requires explicit listener configuration. Edit /etc/mosquitto/conf.d/default.conf and add:
    listener 1883
    allow_anonymous true
    Then restart the service.

Extending or Simplifying the Build

Once the baseline hub is stable on your bench, you need to decide how to scale it for actual home deployment.

To Simplify (The 'Set and Forget' Node):
If you don't need MQTT and just want a standalone thermostat, strip out the paho-mqtt blocks entirely. Replace the Pi 5 with a Raspberry Pi Zero 2 W, disable Bluetooth and WiFi in /boot/firmware/config.txt to reduce power draw and heat, and run the script via a systemd service. Total BOM drops to under $25.

To Extend (The Whole-Home Hub):
If you need to integrate 20+ wireless devices (Zigbee/Matter), do not wire them to the Pi's GPIO. The Pi 5's PCIe lane is the correct path. Add the Pineboards HatDrive! Bottom and a standard M.2 2230 NVMe SSD for bulletproof database storage (Home Assistant destroys microSD cards via write-wear). Then, plug a Sonoff Zigbee 3.0 USB Dongle Plus (P-Version) into the USB 3.0 port, run it through a USB 2.0 extension cable to escape the Pi's 2.4GHz RF noise, and flash it with Zigbee2MQTT. This transitions your build from a simple relay script into a production-grade local smart home server.