Skip to main content

Build a Real-Time Sensor Dashboard with Raspberry Pi, Python, and Flask

A step-by-step tutorial to build a live temperature and humidity dashboard using a DHT22 sensor, Python, Flask, and Server-Sent Events — viewable from any device on your network.

Published
May 13, 2026
Reading
6 min
Views
703
Build a Real-Time Sensor Dashboard with Raspberry Pi, Python, and Flask

If you've ever wanted to see your room's temperature on a live webpage — from your phone, across the house — this tutorial walks you through exactly that. By the end you'll have a real-time dashboard that reads a DHT22 sensor every 2 seconds and streams data live using Server-Sent Events (SSE). No JavaScript frameworks. No WebSockets. Just Python, Flask, and about 30 lines of HTML.

This is a great weekend project, a solid classroom demo, and a practical introduction to IoT concepts that actually matter.


What You'll Build

  • A Flask web server running on the Raspberry Pi

  • A DHT22 sensor reading temperature and humidity every 2 seconds

  • A live dashboard any device on your Wi-Fi can visit

  • A scrolling history chart of the last 60 readings


Hardware You'll Need

  • Raspberry Pi (any model with GPIO — Pi 3, 4, or Zero 2W all work)

  • DHT22 temperature & humidity sensor (~$4 on Amazon or Adafruit)

  • 10kΩ pull-up resistor

  • Jumper wires and a breadboard

Wiring: Connect the DHT22 DATA pin to GPIO 4 (physical pin 7). Put the 10kΩ resistor between DATA and the 3.3V rail. VCC → 3.3V, GND → GND.


Step 1 — Install Dependencies

SSH into your Pi and run:

sudo apt update && sudo apt install python3-pip python3-venv -y
mkdir sensor-dashboard && cd sensor-dashboard
python3 -m venv venv && source venv/bin/activate
pip install flask Adafruit-DHT

If Adafruit-DHT fails on newer Pi OS versions, use the community fork:

pip install Adafruit-CircuitPython-DHT
sudo apt install libgpiod2

Step 2 — Read the Sensor

Create sensor.py. This module handles all hardware communication and keeps a rolling history of the last 60 readings in a thread-safe deque:

import Adafruit_DHT
import time
from collections import deque
from threading import Lock

SENSOR   = Adafruit_DHT.DHT22
GPIO_PIN = 4

# Thread-safe rolling buffer — stores last 60 readings
history = deque(maxlen=60)
lock    = Lock()

def read_sensor():
    """Read temperature and humidity. Returns (temp_c, humidity) or (None, None)."""
    humidity, temperature = Adafruit_DHT.read_retry(SENSOR, GPIO_PIN)
    if humidity is not None and temperature is not None:
        return round(temperature, 1), round(humidity, 1)
    return None, None

def sensor_loop():
    """Background thread: reads sensor every 2 seconds and appends to history."""
    while True:
        temp, hum = read_sensor()
        if temp is not None:
            with lock:
                history.append({
                    'time': time.strftime('%H:%M:%S'),
                    'temp': temp,
                    'hum':  hum
                })
        time.sleep(2)

Step 3 — The Flask Server with Server-Sent Events

Create app.py. The key technique is Server-Sent Events (SSE) — a one-way channel where the server pushes updates to the browser automatically, no polling needed:

from flask import Flask, Response, render_template_string
import json
import time
import threading
from sensor import sensor_loop, history, lock

app = Flask(__name__)

# Start background sensor thread
threading.Thread(target=sensor_loop, daemon=True).start()

# ── SSE stream endpoint ────────────────────────────────────────────────────
@app.route('/stream')
def stream():
    """Push new sensor data to connected browsers via SSE."""
    def generate():
        last_count = 0
        while True:
            with lock:
                current = len(history)
                if current > last_count and current > 0:
                    latest  = history[-1]
                    yield f"data: {json.dumps(latest)}\n\n"
                    last_count = current
            time.sleep(0.5)

    return Response(
        generate(),
        mimetype='text/event-stream',
        headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
    )

# ── History endpoint (initial chart load) ─────────────────────────────────
@app.route('/history')
def get_history():
    with lock:
        data = list(history)
    return Response(json.dumps(data), mimetype='application/json')

# ── Dashboard HTML ────────────────────────────────────────────────────────
DASHBOARD = """
<!DOCTYPE html>
<html>
<head>
  <title>Sensor Dashboard</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body  { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 2rem; }
    h1    { font-size: 1.5rem; color: #60a5fa; margin-bottom: 2rem; }
    .cards{ display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 2rem; }
    .card { background: #1e293b; border-radius: 1rem; padding: 1.5rem 2rem; min-width: 160px; }
    .lbl  { font-size: .8rem; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; }
    .val  { font-size: 2.5rem; font-weight: 700; margin-top: .25rem; }
    canvas{ background: #1e293b; border-radius: 1rem; max-width: 100%; }
  </style>
</head>
<body>
  <h1>Live Sensor Dashboard</h1>
  <div class="cards">
    <div class="card"><div class="lbl">Temperature</div><div class="val" id="temp" style="color:#f97316">--</div></div>
    <div class="card"><div class="lbl">Humidity</div>   <div class="val" id="hum"  style="color:#34d399">--</div></div>
    <div class="card"><div class="lbl">Last Update</div><div class="val" id="ts"   style="font-size:1.4rem">--</div></div>
  </div>
  <canvas id="chart" height="100"></canvas>

  <script>
    const chart = new Chart(document.getElementById('chart').getContext('2d'), {
      type: 'line',
      data: {
        labels: [],
        datasets: [
          { label: 'Temperature (°C)', data: [], borderColor: '#f97316', tension: 0.4, fill: false },
          { label: 'Humidity (%)',       data: [], borderColor: '#34d399', tension: 0.4, fill: false }
        ]
      },
      options: { animation: false, scales: { y: { beginAtZero: false } } }
    });

    function addPoint(d) {
      chart.data.labels.push(d.time);
      chart.data.datasets[0].data.push(d.temp);
      chart.data.datasets[1].data.push(d.hum);
      if (chart.data.labels.length > 60) {
        chart.data.labels.shift();
        chart.data.datasets.forEach(ds => ds.data.shift());
      }
    }

    // Load existing history on page open
    fetch('/history').then(r => r.json()).then(rows => {
      rows.forEach(addPoint);
      chart.update();
    });

    // Subscribe to live updates
    const es = new EventSource('/stream');
    es.onmessage = e => {
      const d = JSON.parse(e.data);
      document.getElementById('temp').textContent = d.temp + ' °C';
      document.getElementById('hum').textContent  = d.hum  + ' %';
      document.getElementById('ts').textContent   = d.time;
      addPoint(d);
      chart.update();
    };
  </script>
</body>
</html>
"""

@app.route('/')
def index():
    return render_template_string(DASHBOARD)

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

Step 4 — Run It

source venv/bin/activate
python app.py

Flask will print the local network address. Open that address on any phone or laptop on the same Wi-Fi — you'll see the live dashboard with values updating every 2 seconds.


Step 5 — Auto-Start on Boot

Create a systemd service so the dashboard starts automatically when the Pi powers on — no SSH required after setup:

sudo nano /etc/systemd/system/sensor-dashboard.service

Paste the following (update the path if your username isn't pi):

[Unit]
Description=Sensor Dashboard
After=network.target

[Service]
WorkingDirectory=/home/pi/sensor-dashboard
ExecStart=/home/pi/sensor-dashboard/venv/bin/python app.py
Restart=always
User=pi

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable sensor-dashboard
sudo systemctl start  sensor-dashboard

# Check it's running
sudo systemctl status sensor-dashboard

How It All Fits Together

The architecture is deliberately simple but teaches real patterns:

  • Background thread — sensor_loop() runs independently from Flask, writing to a shared deque protected by a Lock. This is the standard Python pattern for producer-consumer concurrency.

  • SSE stream — The /stream route keeps an open HTTP connection and yields a new JSON payload whenever the sensor thread adds a reading. The browser's native EventSource handles reconnection automatically — no library needed.

  • No database — Data lives in memory on purpose. For classroom demos, simplicity beats persistence. Swap the deque for sqlite3 writes if you want readings to survive a reboot.


Extensions to Try

  • Add more sensors — Wire up a soil moisture sensor or PIR motion detector and add a new card to the dashboard HTML.

  • Threshold alerts — Send a Telegram message or email when temperature exceeds a limit using the requests library and a bot token.

  • SQLite logging — Write each reading to a database so history survives reboots and you can query weekly trends.

  • Prettier UI — Replace the inline styles with Tailwind CDN and make the dashboard properly mobile-first.


Built this, ran into an issue, or added a cool extension? Drop a comment below — I'd love to see what you're monitoring.

Share this article

Nathaniel Oteng

About Nathaniel Oteng

AI Engineer, Software Developer, and CS/CTE Educator specializing in AI, Machine Learning, Robotics, and Climate Science. I build production systems and teach across K-12, higher education, and industry, solving real-world problems and sharing what I learn through writing.

Field notes

Get the next one in your inbox.

A short letter when I build or teach something worth sharing. Unsubscribe any time.

Comments

Display settings

Accessibility

Adjust the site to suit you. Choices are saved on this device.

Text size