Project Overview & Target Hardware

When evaluating home automation projects with Raspberry Pi, the most robust approach is keeping your control logic and message broker strictly local. Relying on cloud APIs for basic HVAC or exhaust fan control introduces latency and privacy risks. This build creates a standalone, local-first climate controller that reads temperature and humidity, publishes the data to a local MQTT broker, and triggers a 5V relay based on configurable thresholds.

Target Board Variant: This code and wiring guide specifically target the Raspberry Pi 5 (8GB variant). The Pi 5 utilizes the RP1 southbridge chip, which fundamentally changes how GPIO and I2C buses are handled at the hardware level compared to the Pi 4. Legacy libraries like RPi.GPIO are deprecated and will fail on the Pi 5; this project uses gpiozero (backed by lgpio) and smbus2 for reliable, modern operation.

Difficulty Rating: Intermediate
Estimated Time: 90 minutes (hardware) + 45 minutes (software/config)
Estimated Cost: ~$127 USD (2026 pricing)

Hardware Spec Sheet & Pin Mapping

Before cutting wires, verify your exact component variants. Using a 5V logic relay with a 3.3V Pi 5 GPIO pin requires an optocoupled module with a dedicated logic trigger, not a raw transistor board.

Table 1: Bill of Materials (Exact Variants & 2026 Pricing)
Component Exact Variant / Model Specs & Notes Est. Price
Microcontroller Raspberry Pi 5 (8GB) Requires official 27W USB-C PD PSU for full peripheral current. $80.00
Sensor Bosch BME280 Breakout Must be the 3.3V I2C variant (e.g., Adafruit 2652 or generic with onboard LDO). $12.00
Actuator Songle SRD-05VDC-SL-C 5V coil, optocoupled module with JD-VCC jumper for 3.3V logic triggering. $8.00
Storage SanDisk Extreme 32GB microSDXC UHS-I (A2 rating recommended for database logging). $15.00
Power Supply Official Pi 5 27W PSU USB-C PD 5V/5A. Standard 3A phone chargers will throttle GPIO current. $12.00
Table 2: Pi 5 GPIO Pin Mapping (BCM Numbering)
Pi 5 Physical Pin BCM GPIO / Function Target Module Module Pin
Pin 13.3V PowerBME280VIN / VCC
Pin 3GPIO 2 (I2C1 SDA)BME280SDA
Pin 5GPIO 3 (I2C1 SCL)BME280SCL
Pin 6GroundBME280GND
Pin 12GPIO 18 (PWM0)Relay ModuleIN (Signal)
Pin 25V PowerRelay ModuleJD-VCC (Coil Power)
Pin 45V PowerRelay ModuleVCC (Opto Power)
Pin 9GroundRelay ModuleGND

Wiring Steps & Power Constraints

MAINS VOLTAGE WARNING: The relay module switches up to 10A at 120V/240V AC. If you connect the relay's Common (COM) and Normally Open (NO) terminals to mains-powered HVAC equipment or line-voltage lighting, you must de-energize the circuit at the breaker, verify dead with a CAT III multimeter, and comply with local electrical codes. For this bench test, we are only switching a 12V DC desktop fan to validate the logic safely.
  1. Prepare the I2C Bus: Connect the BME280 to Pi 5 Pins 1, 3, 5, and 6. The Pi 5 RP1 chip includes internal pull-ups on the primary I2C bus, but if your BME280 breakout lacks onboard pull-ups, you may need to add 4.7kΩ resistors between SDA/SCL and 3.3V.
  2. Configure the Relay Optocoupler: Most 5V relay modules have a 3-pin jumper labeled JD-VCC and VCC. Remove the jumper. Connect Pi 5V (Pin 2) to JD-VCC (this powers the relay coil). Connect Pi 3.3V (Pin 1) to the module's VCC (this powers the optocoupler LED). This isolates the 5V coil noise from the Pi's 3.3V logic rail.
  3. Connect the Signal: Run a jumper from Pi GPIO 18 (Pin 12) to the Relay IN pin. GPIO 18 is hardware PWM-capable, though we are using it as a standard digital output here.
  4. Enable I2C in Firmware: Boot the Pi, open a terminal, and run sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi.

Complete Python Control Script

This script requires three libraries. Install them via your virtual environment or system pip: sudo apt install python3-gpiozero python3-smbus2 python3-paho-mqtt or use pip install gpiozero smbus2 paho-mqtt bme280.

We use the bme280 wrapper library to handle the complex Bosch compensation math for temperature and humidity, keeping our main loop clean. For more on the underlying GPIO architecture, refer to the gpiozero documentation.

import time
import sys
import logging
from gpiozero import OutputDevice
from smbus2 import SMBus
import bme280
import paho.mqtt.client as mqtt

# --- PIN & CONFIGURATION DEFINITIONS ---
RELAY_GPIO = 18               # BCM 18 / Physical Pin 12
I2C_BUS_ID = 1                # Pi 5 primary I2C bus
BME280_I2C_ADDR = 0x76        # Check with i2cdetect; some are 0x77
MQTT_BROKER_IP = '127.0.0.1'  # Local Mosquitto broker
MQTT_PORT = 1883
TEMP_THRESHOLD_C = 24.5       # Trigger relay above this temp
POLL_INTERVAL_SEC = 10

# --- LOGGING SETUP ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')

# --- HARDWARE INITIALIZATION ---
relay = OutputDevice(RELAY_GPIO, active_high=True, initial_value=False)
bus = SMBus(I2C_BUS_ID)

try:
    # Load BME280 calibration parameters
    calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
    logging.info("BME280 calibration loaded successfully.")
except Exception as e:
    logging.critical(f"Failed to initialize BME280 at address {hex(BME280_I2C_ADDR)}: {e}")
    sys.exit(1)

# --- MQTT CALLBACKS ---
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        logging.info(f"Connected to MQTT broker at {MQTT_BROKER_IP}")
    else:
        logging.error(f"MQTT connection failed with code: {reason_code}")

# --- MQTT CLIENT SETUP ---
# Using Paho MQTT v2.0 API callback signature
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pi5_climate_node")
client.on_connect = on_connect

try:
    client.connect(MQTT_BROKER_IP, MQTT_PORT, keepalive=60)
    client.loop_start()
except Exception as e:
    logging.error(f"Initial MQTT connection error: {e}")

# --- MAIN CONTROL LOOP ---
try:
    while True:
        try:
            # Read sensor data
            data = bme280.sample(bus, BME280_I2C_ADDR, calibration_params)
            temp_c = round(data.temperature, 2)
            humidity = round(data.humidity, 2)
            
            # Control Logic
            if temp_c > TEMP_THRESHOLD_C:
                if not relay.is_active:
                    relay.on()
                    logging.info(f"Temp {temp_c}C > Threshold. Relay ENGAGED.")
            else:
                if relay.is_active:
                    relay.off()
                    logging.info(f"Temp {temp_c}C <= Threshold. Relay DISENGAGED.")

            # Publish to MQTT
            payload = f'{{"temp_c": {temp_c}, "humidity": {humidity}, "relay_state": {relay.is_active}}}'
            client.publish("home/livingroom/climate", payload, qos=1)
            
        except OSError as e:
            logging.error(f"I2C Read Error: {e}. Check wiring.")
        except Exception as e:
            logging.error(f"Unexpected loop error: {e}")
            
        time.sleep(POLL_INTERVAL_SEC)

except KeyboardInterrupt:
    logging.info("Shutting down safely...")
finally:
    relay.off()
    client.loop_stop()
    client.disconnect()
    bus.close()
    logging.info("Hardware pins released and MQTT disconnected.")

Debugging: Exact Errors & The "First Three" Checks

When building home automation projects with Raspberry Pi, hardware and daemon configurations are the primary failure points. If your script crashes on startup, look for these exact error strings.

1. FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'

  • Cause A: I2C is disabled in the Pi 5 firmware. Run sudo raspi-config and enable it.
  • Cause B: You are running a headless lite image and the i2c-dev kernel module isn't loaded. Fix by running sudo modprobe i2c-dev and adding it to /etc/modules.

2. OSError: [Errno 121] Remote I/O error

  • Cause A: Wrong I2C address. The BME280 defaults to 0x76, but some Adafruit/breakout boards tie the SDO pin high, making it 0x77. Run i2cdetect -y 1 to verify.
  • Cause B: Voltage mismatch or missing pull-ups. If you are using a raw BME280 chip without a breakout board's LDO and pull-ups, the Pi 5's 3.3V logic won't read the floating lines reliably.

3. ConnectionRefusedError: [Errno 111] Connection refused

  • Cause A: Mosquitto isn't running. Check with sudo systemctl status mosquitto.
  • Cause B (Most Common in 2026): Eclipse Mosquitto 2.0+ changed default security. It no longer allows anonymous connections or binds to external interfaces by default. You must edit /etc/mosquitto/conf.d/default.conf and add:
    listener 1883
    allow_anonymous true
    Then restart the service. See the Paho MQTT Python repository for client-side compatibility notes.
The "First Three" Checks When It Fails:
1. Run i2cdetect -y 1. If you don't see 76 or 77 in the grid, your sensor wiring or power is wrong.
2. Run mosquitto_pub -h 127.0.0.1 -t "test" -m "hello". If it hangs or refuses, your Mosquitto 2.0 listener config is blocking local traffic.
3. Measure the voltage between the Relay Module's IN pin and GND with a multimeter while the script runs. It must swing from ~0V to ~3.3V. If it only reaches 1.5V, your optocoupler jumper is misconfigured.

Extending vs. Simplifying the Build

Depending on your deployment environment, you may need to scale this node up or strip it down.

How to Simplify (The Offline Cabin Approach)

If you are deploying this in an off-grid location or a shed where running a local MQTT broker is overkill, drop the paho-mqtt dependency entirely. Replace the MQTT publish block with a local SQLite database insert using Python's built-in sqlite3 library, or simply write the threshold logic to a local CSV log. You can also replace the Python daemon with a simple Bash script triggered by a cron job that reads the I2C bus via the i2cget command-line tool, eliminating the need for Python virtual environments altogether.

How to Extend (The Whole-Home Approach)

To integrate this into a broader smart home ecosystem, extend the build by adding Home Assistant MQTT Discovery. Instead of just publishing raw JSON to home/livingroom/climate, publish a retained configuration payload to homeassistant/sensor/pi5_climate/config. Home Assistant will automatically detect the Pi 5 as a native climate entity, complete with historical graphs and dashboard cards.

On the hardware side, swap the single 5V relay for a 4-channel 12V relay board controlled via an I2C GPIO expander (like the MCP23017). The Pi 5's 3.3V logic and limited GPIO count make I2C expansion the cleanest way to handle multi-zone HVAC dampers or whole-home lighting contactors without running out of physical pins. For deeper integration with building management standards, look into the official Raspberry Pi industrial compute module documentation for DIN-rail mounting and RS485 transceiver integration.