IoT Projects

Project: Environmental Monitoring Station

Build a multi-sensor weather station with DHT22, BME280, and MQ-135, log data to ThingSpeak and InfluxDB, send SMS alerts, and display real-time readings on a Grafana dashboard.

Project: Environmental Monitoring Station

This project builds a complete outdoor/indoor environmental monitoring station — multiple sensors, local display, cloud logging, alerting, and a live Grafana dashboard.


System Overview

┌─────────────────────────────────────┐
│      ESP32 Monitoring Node          │
│                                     │
│  Sensors:                           │
│  ├── DHT22     → Temperature/Humidity│
│  ├── BME280    → Pressure/Altitude  │
│  ├── MQ-135    → Air Quality (CO2eq)│
│  ├── BH1750    → Light Level (lux)  │
│  └── Rain Sensor → Precipitation   │
│                                     │
│  Display:                           │
│  └── SSD1306 OLED (128×64)         │
│                                     │
│  Power: Solar + LiPo + TP4056      │
└────────────────┬────────────────────┘
                 │ MQTT over WiFi
                 ▼
┌────────────────────────────────────┐
│     Cloud Pipeline                 │
│  ├── ThingSpeak (quick charts)     │
│  ├── InfluxDB (time-series)        │
│  ├── Grafana (dashboard)           │
│  └── Twilio (SMS alerts)           │
└────────────────────────────────────┘

Complete Device Firmware (MicroPython)

python
# environmental_station.py
import dht, utime, network, ujson, machine, gc
from machine import Pin, I2C, ADC, PWM
from umqtt.simple import MQTTClient

# ── Config ─────────────────────────────────────────
from config import (
    WIFI_SSID, WIFI_PASSWORD,
    MQTT_BROKER, DEVICE_ID, LOCATION,
    THINGSPEAK_KEY
)

MQTT_TOPIC       = f"environment/{LOCATION}/{DEVICE_ID}"
SLEEP_MINUTES    = 5
ALERT_TEMP_HIGH  = 38.0
ALERT_AQI_BAD    = 200  # ppm threshold

# ── Hardware ────────────────────────────────────────
dht_pin   = Pin(4)
dht22     = dht.DHT22(dht_pin)

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400_000)

# BME280
try:
    import bme280
    bme = bme280.BME280(i2c=i2c)
    HAS_BME = True
except Exception as e:
    print(f"BME280 not found: {e}")
    HAS_BME = False

# BH1750 light sensor
BH1750_ADDR = 0x23
i2c.writeto(BH1750_ADDR, b'\x10')  # Continuous high-res mode

# MQ-135 air quality (ADC)
mq_adc = ADC(Pin(34))
mq_adc.atten(ADC.ATTN_11DB)

# OLED display
try:
    import ssd1306
    oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
    HAS_OLED = True
except:
    HAS_OLED = False

# ── Reading functions ───────────────────────────────
def read_dht22():
    for _ in range(3):
        try:
            dht22.measure()
            return dht22.temperature(), dht22.humidity()
        except OSError:
            utime.sleep_ms(500)
    return None, None

def read_bme280():
    if not HAS_BME:
        return None, None, None
    try:
        t, p, h = bme.read_compensated_data()
        return t / 100, p / 25600, h / 1024
    except:
        return None, None, None

def read_light():
    utime.sleep_ms(200)
    data = i2c.readfrom(BH1750_ADDR, 2)
    return (data[0] << 8 | data[1]) / 1.2  # lux

def read_air_quality():
    # Oversample 16×, trim outliers, convert to rough ppm
    samples = sorted([mq_adc.read() for _ in range(16)])
    raw = sum(samples[2:-2]) / 12  # Trimmed mean
    voltage = raw / 4095 * 3.3
    if voltage < 0.1: return None
    Rs = (3.3 - voltage) / voltage * 10_000
    ppm = 116.6 * (Rs / 76.63) ** -2.77
    return round(ppm, 0)

def read_battery():
    # Voltage divider: 100kΩ / 100kΩ → ×2
    adc_batt = ADC(Pin(35))
    adc_batt.atten(ADC.ATTN_11DB)
    raw = sum(adc_batt.read() for _ in range(8)) / 8
    voltage = raw / 4095 * 3.3 * 2  # Divider correction
    return round(voltage, 2)

# ── Display update ──────────────────────────────────
def update_display(temp, hum, pres, lux, aqi):
    if not HAS_OLED:
        return
    oled.fill(0)
    oled.fill_rect(0, 0, 128, 11, 1)
    oled.text(f" {LOCATION}", 0, 2, 0)

    if temp:  oled.text(f"T: {temp:.1f}C  H:{hum:.0f}%",  0, 14, 1)
    if pres:  oled.text(f"P: {pres:.1f}hPa",              0, 26, 1)
    if lux:   oled.text(f"L: {lux:.0f} lux",              0, 38, 1)
    if aqi:
        quality = "GOOD" if aqi < 100 else ("FAIR" if aqi < 200 else "POOR")
        oled.text(f"AQ: {quality}",                        0, 50, 1)

    oled.show()

# ── WiFi ────────────────────────────────────────────
def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(WIFI_SSID, WIFI_PASSWORD)
    for _ in range(20):
        if wlan.isconnected(): return wlan
        utime.sleep_ms(500)
    return None

# ── ThingSpeak upload ───────────────────────────────
def upload_thingspeak(temp, hum, pres, lux, aqi, battery):
    try:
        import urequests
        url = (f"https://api.thingspeak.com/update?"
               f"api_key={THINGSPEAK_KEY}"
               f"&field1={temp or ''}&field2={hum or ''}"
               f"&field3={pres or ''}&field4={lux or ''}"
               f"&field5={aqi or ''}&field6={battery or ''}")
        r = urequests.get(url, timeout=10)
        r.close()
        return True
    except:
        return False

# ── MQTT publish ────────────────────────────────────
def publish_mqtt(wlan, data):
    try:
        client = MQTTClient(DEVICE_ID, MQTT_BROKER, keepalive=30)
        client.connect()
        client.publish(MQTT_TOPIC, ujson.dumps(data), retain=True)
        client.disconnect()
        return True
    except Exception as e:
        print(f"MQTT error: {e}")
        return False

# ── Main ────────────────────────────────────────────
def main():
    gc.collect()

    # Read all sensors
    temp, hum   = read_dht22()
    _, pres, _  = read_bme280()
    lux         = read_light()
    aqi         = read_air_quality()
    battery_v   = read_battery()
    battery_pct = max(0, min(100, int((battery_v - 3.3) / 0.9 * 100)))

    print(f"T={temp}°C H={hum}% P={pres}hPa L={lux}lx AQ={aqi}ppm Bat={battery_v}V")

    # Update local display
    update_display(temp, hum, pres, lux, aqi)

    # Build payload
    payload = {
        "device_id":   DEVICE_ID,
        "location":    LOCATION,
        "temperature": temp,
        "humidity":    hum,
        "pressure":    pres,
        "light_lux":   round(lux, 1) if lux else None,
        "air_quality": aqi,
        "battery_v":   battery_v,
        "battery_pct": battery_pct,
        "timestamp":   utime.time()
    }

    # Upload when WiFi available
    wlan = connect_wifi()
    if wlan:
        publish_mqtt(wlan, payload)
        upload_thingspeak(temp, hum, pres, lux, aqi, battery_pct)
        wlan.active(False)  # Disable WiFi to save power
    else:
        print("WiFi unavailable — skipping upload")

    # Deep sleep until next reading
    print(f"Sleeping {SLEEP_MINUTES} minutes...")
    machine.deepsleep(SLEEP_MINUTES * 60 * 1000)

main()

Alert System (Python — Server Side)

python
# alerts.py — monitors MQTT and sends SMS alerts via Twilio
import paho.mqtt.client as mqtt
import json
from twilio.rest import Client as TwilioClient
import os
import time

TWILIO_SID   = os.environ["TWILIO_ACCOUNT_SID"]
TWILIO_TOKEN = os.environ["TWILIO_AUTH_TOKEN"]
TWILIO_FROM  = os.environ["TWILIO_PHONE_FROM"]
ALERT_PHONE  = os.environ["ALERT_PHONE_TO"]

twilio = TwilioClient(TWILIO_SID, TWILIO_TOKEN)

THRESHOLDS = {
    "temperature": {"max": 38.0, "min": 0.0},
    "humidity":    {"max": 90.0, "min": 20.0},
    "air_quality": {"max": 200.0},
    "battery_pct": {"min": 15},
}

last_alert = {}
ALERT_COOLDOWN = 3600  # 1 hour between same-type alerts

def send_sms(message):
    try:
        twilio.messages.create(
            body=message, from_=TWILIO_FROM, to=ALERT_PHONE
        )
        print(f"SMS sent: {message[:50]}")
    except Exception as e:
        print(f"SMS failed: {e}")

def check_alerts(data):
    device  = data.get("device_id", "unknown")
    location = data.get("location", "?")
    now     = time.time()

    alerts_sent = []
    for metric, limits in THRESHOLDS.items():
        value = data.get(metric)
        if value is None:
            continue

        alert_key = f"{device}_{metric}"
        if now - last_alert.get(alert_key, 0) < ALERT_COOLDOWN:
            continue  # Cooldown active

        if "max" in limits and value > limits["max"]:
            msg = f"⚠️ IoT Alert [{location}]\n{metric}: {value} exceeds max {limits['max']}"
            send_sms(msg)
            last_alert[alert_key] = now
            alerts_sent.append(metric)

        elif "min" in limits and value < limits["min"]:
            msg = f"⚠️ IoT Alert [{location}]\n{metric}: {value} below min {limits['min']}"
            send_sms(msg)
            last_alert[alert_key] = now
            alerts_sent.append(metric)

    return alerts_sent

def on_message(client, userdata, msg):
    try:
        data = json.loads(msg.payload)
        alerts = check_alerts(data)
        if alerts:
            print(f"Alerts triggered: {alerts}")
    except:
        pass

client = mqtt.Client("alert-system")
client.on_connect = lambda c, u, f, r: c.subscribe("environment/#", qos=1)
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.loop_forever()

Grafana Dashboard Setup

InfluxDB datasource panels:

Panel 1: Temperature Gauge
  Type: Gauge
  Min: -10°C  Max: 50°C
  Thresholds: Green < 28, Yellow < 35, Red ≥ 35

Panel 2: Humidity + Temperature (time series, dual Y-axis)
  Query A: temperature field (left axis)
  Query B: humidity field (right axis)

Panel 3: Air Quality Status
  Type: Stat
  Mappings: 0-100=GOOD, 101-200=FAIR, 201+=POOR

Panel 4: Battery Level
  Type: Bar gauge
  Min: 0%  Max: 100%
  Alert: < 20%

Panel 5: Pressure (last 24h)
  Type: Time series
  Show: Pressure trend (drop > 5 hPa in 3h = rain likely)

Summary

  • The station reads 5+ environmental parameters every 5 minutes using deep sleep for battery efficiency
  • OLED display provides local readout without any network dependency
  • ThingSpeak gives instant cloud charts; InfluxDB + Grafana for production dashboards
  • Twilio SMS alerts are rate-limited with cooldowns to prevent alert fatigue
  • Deep sleep + solar power enables indefinite battery-free outdoor operation
  • The pattern is reusable — swap sensors, adjust thresholds, change cloud platform without restructuring

Next, we'll build an asset tracking system with GPS.