← All projects

EnviroPi

Deployed

A swarm of Raspberry Pi sensor nodes, one central server, and a Discord ping before the lab overheats.

Why it exists

Rooms drift. HVAC quits, a space heater gets left on, a server closet cooks, a greenhouse freezes, and nothing tells you until you walk in. EnviroPi is the self-hosted answer: put a cheap sensor node in each room you care about, get one dashboard for all of them, and get a Discord message the moment a threshold is crossed. Ours watches the lab for the day the air conditioner quits. That morning is the chart below.

EnviroPi temperature dashboard: 24-hour chart from two nodes, steady overnight then a sharp climb after 6 AM
The temperature dashboard, captured the day this page was written: steady overnight, then a hard climb after 6 AM. The background gradient is the health scale. Node names are placeholders.

What it is

Each node is a Pi Zero wearing a Pimoroni Enviro+ hat: temperature, humidity, pressure, light, and sound, sampled and pushed every 15 seconds. The node's little LCD cycles between a network-status view and a climate view with trend arrows. A central Flask server stores everything, charts it for anyone on the network (no login needed to view), and evaluates alert rules as each batch arrives. Two nodes are on duty; parts for the third are on the bench.

EnviroPi sensor overview: one card per node showing temperature, feels like, humidity, wet bulb, pressure, light, and sound
The overview: one card per node, latest reading for every variable, and a metric/imperial toggle that follows the viewer, not the account.

How it works

 [Enviro+ node]──┐
 [Enviro+ node]──┼── HTTP POST /api/ingest ──▶ [central server] ──▶ [database]
 [Enviro+ node]──┘    (per-device API key)         │      │
      │                                            │      └──▶ Discord webhook
      └── 0.96" LCD: network + climate views       └──▶ dashboards + admin UI

On the node. The device agent is one Python process, run by systemd. Each 15-second cycle it reads the BME280 (temperature, humidity, pressure), the LTR559 (light), and the onboard mic (sound level), stamps an NTP-disciplined UTC timestamp, appends to a durable on-disk queue, and pushes the queue to the server in acknowledged batches. A separate thread owns the LCD, cycling between a network-status view and a climate view with per-poll trend arrows. The whole agent stays Python 3.7-compatible, because that's what a Pi Zero from the parts bin ships with.

On the server. A Flask application factory with the work split into blueprints and services: public read-only dashboards and a JSON read API (Chart.js on top), a key-authenticated ingest endpoint, an admin-only management UI for devices, keys, alerts, and users, and an alert engine that evaluates every accepted batch and delivers to a Discord webhook. Each reading carries two timestamps, the node's NTP-synced clock and the server's receive time, so clock drift and backfills are visible instead of silent. SQLite in development, PostgreSQL in production, same code on both, Alembic managing the schema.

The numbers no sensor gives us

The raw sensor values are the start, not the product. A few of the numbers on the dashboard are computed:

A corrected temperature. The BME280 sits close enough to the Pi's CPU to read a few degrees warm, so the agent reads the CPU temperature alongside it and corrects with comp = raw - (cpu - raw) / factor. The factor is per-board (1.8 on one of our nodes, 2.69 on the other, same hat), and a bundled calibration tool computes it: run it on a calibrated reference node, run it with --match on the new board sitting next to it, and it prints the factor to install.

An honest humidity. Relative humidity is relative to temperature, so a warm-skewed reading is also a dry-skewed one. The agent rescales raw humidity by the ratio of saturation vapor pressures at the raw and corrected temperatures: same moisture, right temperature.

Wet-bulb temperature. Derived at ingest from corrected temperature and humidity using Stull's 2011 approximation, then stored as a first-class variable: it charts, it alerts, and its history backfills like anything a sensor sent. Wet bulb is the heat-safety number; it bounds how well evaporative cooling can work.

Feels like. The NWS heat index (Rothfusz regression), computed in the browser from each node's latest temperature and humidity. Display-only by design: it's a comfort estimate, not a measurement, so it isn't stored.

EnviroPi wet-bulb dashboard: 24-hour chart of derived wet-bulb temperature from two nodes
Wet bulb charted like any sensor, even though no sensor measures it.

The parts that took actual thought

Nothing gets lost. A node deletes a reading from its local queue only after the server explicitly acknowledges it, and the ingest insert is idempotent, so retries never duplicate data. Validated the honest way: the server once went down for 18 hours, and the nodes backfilled about 21,500 readings when it came back. The charts have no gap.

Alerts that don't spam. A rule fires once on the rising edge of a breach, stays latched while the breach holds, and re-arms only after the value crosses back past a deadband. Delivery is isolated from ingest: a slow or dead webhook never delays a push, and the alert event is recorded regardless.

Keys, not trust. Nodes are pre-registered and push with per-device API keys: 256-bit random tokens, stored only as SHA-256 hashes, shown in plaintext exactly once. A device can hold multiple keys, so rotation needs no downtime. Guests can see every chart without an account; writing anything requires the admin login.

Quiet on the radio. Some rooms need to be RF-silent, so the deploy scripts can hard-disable a board's WiFi and Bluetooth at the firmware level (it survives reboots and updates). One current node has no wireless hardware at all and uplinks through a USB ethernet adapter.