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.
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.
| 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 |
| Pi 5 Physical Pin | BCM GPIO / Function | Target Module | Module Pin |
|---|---|---|---|
| Pin 1 | 3.3V Power | BME280 | VIN / VCC |
| Pin 3 | GPIO 2 (I2C1 SDA) | BME280 | SDA |
| Pin 5 | GPIO 3 (I2C1 SCL) | BME280 | SCL |
| Pin 6 | Ground | BME280 | GND |
| Pin 12 | GPIO 18 (PWM0) | Relay Module | IN (Signal) |
| Pin 2 | 5V Power | Relay Module | JD-VCC (Coil Power) |
| Pin 4 | 5V Power | Relay Module | VCC (Opto Power) |
| Pin 9 | Ground | Relay Module | GND |
Wiring Steps & Power Constraints
- 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.
- 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.
- 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.
- 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-configand enable it. - Cause B: You are running a headless lite image and the
i2c-devkernel module isn't loaded. Fix by runningsudo modprobe i2c-devand 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 it0x77. Runi2cdetect -y 1to 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.confand add:
listener 1883
allow_anonymous true
Then restart the service. See the Paho MQTT Python repository for client-side compatibility notes.
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.






