IoT Projects

Project: Smart Home Automation

Build a complete smart home system with ESP32 sensors, relay-controlled appliances, MQTT messaging, a Python backend, and a web dashboard — with scheduling and voice integration.

Project: Smart Home Automation

This project builds a real smart home controller: ESP32 devices sense temperature and occupancy, a Raspberry Pi acts as the hub, MQTT connects everything, and a web dashboard lets you monitor and control appliances remotely.


System Architecture

ESP32 Node (Bedroom)          ESP32 Node (Living Room)
  ├── DHT22 (temp/humidity)     ├── DHT22
  ├── PIR (motion)              ├── PIR
  └── Relay (AC/Fan control)    ├── Relay (lights)
        │                       └── Relay (TV power)
        │ MQTT (local WiFi)           │
        ▼                             ▼
  Raspberry Pi 4 (Hub)
  ├── Mosquitto MQTT Broker
  ├── Python Backend (automation rules)
  ├── SQLite / InfluxDB (history)
  └── Flask Web Dashboard
        │
        │ HTTPS / WS
        ▼
  Mobile/Web Browser
  ├── Live readings
  ├── Manual controls
  └── Schedule editor

Device Firmware (ESP32 / MicroPython)

python
# main.py on ESP32 node
import dht, utime, network, ujson
from machine import Pin, Timer
from umqtt.simple import MQTTClient

# ── Configuration ──────────────────────────────────
from config import WIFI_SSID, WIFI_PASSWORD, MQTT_BROKER, DEVICE_ID, ROOM

# ── Hardware setup ─────────────────────────────────
dht_sensor = dht.DHT22(Pin(4))
pir_sensor = Pin(5, Pin.IN)
relay_ac   = Pin(26, Pin.OUT, value=1)  # Active LOW, start OFF
relay_fan  = Pin(27, Pin.OUT, value=1)

TELEMETRY_TOPIC = f"home/{ROOM}/telemetry"
COMMAND_TOPIC   = f"home/{ROOM}/commands"
STATUS_TOPIC    = f"home/{ROOM}/status"

# ── State ──────────────────────────────────────────
state = {
    "ac_on":       False,
    "fan_on":      False,
    "occupancy":   False,
    "last_motion": 0,
    "temperature": None,
    "humidity":    None,
}

# ── MQTT callback ──────────────────────────────────
def on_command(topic, msg):
    """Handle commands from the hub."""
    cmd = ujson.loads(msg)
    action = cmd.get("action")

    if action == "SET_AC":
        state["ac_on"] = cmd["value"]
        relay_ac.value(0 if state["ac_on"] else 1)  # Active LOW

    elif action == "SET_FAN":
        state["fan_on"] = cmd["value"]
        relay_fan.value(0 if state["fan_on"] else 1)

    elif action == "GET_STATUS":
        publish_status()

    print(f"Command executed: {cmd}")

def publish_status():
    status = ujson.dumps({
        "device_id":   DEVICE_ID,
        "room":        ROOM,
        "ac_on":       state["ac_on"],
        "fan_on":      state["fan_on"],
        "occupancy":   state["occupancy"],
        "temperature": state["temperature"],
        "humidity":    state["humidity"],
        "timestamp":   utime.time()
    })
    mqtt.publish(STATUS_TOPIC, status, retain=True)

# ── WiFi ───────────────────────────────────────────
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
while not wlan.isconnected():
    utime.sleep_ms(500)
print(f"WiFi: {wlan.ifconfig()[0]}")

# ── MQTT ───────────────────────────────────────────
mqtt = MQTTClient(
    client_id=DEVICE_ID,
    server=MQTT_BROKER,
    keepalive=60
)
mqtt.set_callback(on_command)
mqtt.connect()
mqtt.subscribe(COMMAND_TOPIC)

# ── Publish online status ──────────────────────────
mqtt.publish(
    f"home/{ROOM}/online",
    ujson.dumps({"online": True, "device_id": DEVICE_ID}),
    retain=True
)

# ── Main loop ──────────────────────────────────────
last_telemetry = 0
TELEMETRY_INTERVAL = 60_000  # ms

while True:
    mqtt.check_msg()  # Non-blocking MQTT poll

    # PIR motion detection
    if pir_sensor.value():
        state["occupancy"]   = True
        state["last_motion"] = utime.ticks_ms()
    elif utime.ticks_diff(utime.ticks_ms(), state["last_motion"]) > 300_000:
        state["occupancy"] = False  # No motion for 5 min

    # Periodic sensor reading and telemetry
    now = utime.ticks_ms()
    if utime.ticks_diff(now, last_telemetry) >= TELEMETRY_INTERVAL:
        last_telemetry = now
        try:
            dht_sensor.measure()
            state["temperature"] = dht_sensor.temperature()
            state["humidity"]    = dht_sensor.humidity()

            payload = ujson.dumps({
                "device_id":   DEVICE_ID,
                "room":        ROOM,
                "temperature": state["temperature"],
                "humidity":    state["humidity"],
                "occupancy":   state["occupancy"],
                "timestamp":   utime.time()
            })
            mqtt.publish(TELEMETRY_TOPIC, payload)
        except OSError as e:
            print(f"Sensor error: {e}")

    utime.sleep_ms(200)

Hub Backend (Raspberry Pi / Python)

python
# hub.py — automation rules engine
import paho.mqtt.client as mqtt
import json
import sqlite3
import time
import threading
from datetime import datetime

class SmartHomeHub:
    def __init__(self, broker="localhost"):
        self.rooms = {}  # room → latest state
        self.db = self._init_db()
        self.rules = [
            self.rule_auto_ac,
            self.rule_unoccupied_off,
        ]

        self.client = mqtt.Client("smart-hub")
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.connect(broker, 1883, 60)

    def _init_db(self):
        conn = sqlite3.connect("smarthome.db", check_same_thread=False)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS readings (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp   TEXT,
                room        TEXT,
                device_id   TEXT,
                temperature REAL,
                humidity    REAL,
                occupancy   INTEGER,
                ac_on       INTEGER,
                fan_on      INTEGER
            )
        """)
        return conn

    def _on_connect(self, c, u, f, rc):
        print(f"Hub connected (rc={rc})")
        c.subscribe("home/+/telemetry", qos=1)
        c.subscribe("home/+/status",    qos=1)

    def _on_message(self, c, u, msg):
        try:
            data = json.loads(msg.payload)
            room = data.get("room", "unknown")

            # Update in-memory state
            self.rooms[room] = {**self.rooms.get(room, {}), **data}

            # Store in database
            self._store(data)

            # Evaluate automation rules
            for rule in self.rules:
                rule(room, data)

        except Exception as e:
            print(f"Hub error: {e}")

    def _store(self, d):
        self.db.execute("""
            INSERT INTO readings
            (timestamp, room, device_id, temperature, humidity, occupancy, ac_on, fan_on)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            datetime.utcnow().isoformat(),
            d.get("room"), d.get("device_id"),
            d.get("temperature"), d.get("humidity"),
            int(d.get("occupancy", False)),
            int(d.get("ac_on", False)),
            int(d.get("fan_on", False))
        ))
        self.db.commit()

    def send_command(self, room, action, value=None):
        payload = json.dumps({"action": action, "value": value})
        self.client.publish(f"home/{room}/commands", payload, qos=1)
        print(f"Command → {room}: {action}={value}")

    # ── Automation Rules ───────────────────────────────

    def rule_auto_ac(self, room, data):
        """Auto-enable AC if room temp exceeds 28°C and room is occupied."""
        temp    = data.get("temperature")
        occupied = data.get("occupancy", False)

        if temp and occupied and temp > 28:
            if not self.rooms[room].get("ac_on"):
                print(f"Rule: Auto-AC ON in {room} ({temp}°C)")
                self.send_command(room, "SET_AC", True)
        elif temp and (not occupied or temp < 25):
            if self.rooms[room].get("ac_on"):
                print(f"Rule: Auto-AC OFF in {room}")
                self.send_command(room, "SET_AC", False)

    def rule_unoccupied_off(self, room, data):
        """Turn off all appliances if room has been unoccupied for 10 minutes."""
        if not data.get("occupancy"):
            room_state = self.rooms.get(room, {})
            if room_state.get("ac_on") or room_state.get("fan_on"):
                print(f"Rule: Room {room} unoccupied — turning off appliances")
                self.send_command(room, "SET_AC",  False)
                self.send_command(room, "SET_FAN", False)

    def run(self):
        print("Smart Home Hub running...")
        self.client.loop_forever()

if __name__ == "__main__":
    hub = SmartHomeHub()
    hub.run()

Web Dashboard (Flask)

python
# dashboard.py
from flask import Flask, render_template, jsonify, request
from flask_socketio import SocketIO, emit
import paho.mqtt.client as mqtt
import json
import sqlite3

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
rooms_state = {}

# MQTT listener
def setup_mqtt():
    c = mqtt.Client("dashboard-listener")
    def on_msg(client, ud, msg):
        data = json.loads(msg.payload)
        room = data.get("room")
        if room:
            rooms_state[room] = data
            socketio.emit("room_update", data)  # Push to browsers via WebSocket
    c.on_connect = lambda c, u, f, r: c.subscribe("home/#", qos=1)
    c.on_message = on_msg
    c.connect("localhost", 1883, 60)
    c.loop_start()
    return c

@app.route('/')
def index():
    return render_template('dashboard.html')

@app.route('/api/rooms')
def get_rooms():
    return jsonify(rooms_state)

@app.route('/api/command', methods=['POST'])
def send_command():
    body = request.json
    room   = body.get("room")
    action = body.get("action")
    value  = body.get("value")
    # Forward to MQTT hub
    mqtt_client.publish(f"home/{room}/commands",
                        json.dumps({"action": action, "value": value}), qos=1)
    return jsonify({"ok": True})

@app.route('/api/history/<room>')
def get_history(room):
    conn = sqlite3.connect("smarthome.db")
    rows = conn.execute("""
        SELECT timestamp, temperature, humidity, occupancy
        FROM readings WHERE room = ?
        ORDER BY timestamp DESC LIMIT 100
    """, (room,)).fetchall()
    return jsonify([
        {"timestamp": r[0], "temperature": r[1],
         "humidity": r[2], "occupancy": bool(r[3])}
        for r in rows
    ])

mqtt_client = setup_mqtt()

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=5000)

Summary

  • ESP32 nodes publish sensor data and accept relay commands over MQTT
  • A Raspberry Pi hub runs automation rules, stores history, and serves the web dashboard
  • Automation rules evaluate each reading and send commands — no cloud involved
  • WebSockets (via flask-socketio) push real-time updates to the browser without polling
  • The STATUS_TOPIC with retain=True ensures new dashboard sessions immediately see device state
  • This architecture scales from a single room to a full building — add more ESP32 nodes without changing the hub

Next, we'll build an environmental monitoring station that uploads data to the cloud.