← Back to blog
IoT Homelab

Building an air quality monitor with BME680 and Raspberry Pi Zero 2W

How a brand-new Raspberry Pi Zero 2W became a fully automated indoor air quality station: BME680 sensor, Elegoo display, Telegram alerts, SQLite time-series database, and a hybrid IAQ algorithm — all running headless as a systemd service.

The starting point: a Pi Zero looking for a purpose

When the Raspberry Pi Zero 2W landed on my desk, my first reaction was excitement — and my second was the classic problem: what do I build with it? The Pi 5 was already running the homelab server, the trading bot, and this website. The Zero 2W needed its own identity.

I wanted a project that would actually be useful day-to-day, not just a blinking LED exercise. The idea of monitoring indoor air quality felt immediately practical: temperature and humidity matter for comfort, barometric pressure is interesting for weather correlation, and volatile organic compounds (VOCs) tell you things about your indoor environment that you simply cannot perceive with your senses. A BME680 sensor and a small display seemed like the perfect first project for such a compact board.

The hardware: BME680 sensor

The BME680 is an environmental sensor from Bosch Sensortec that packs four measurements into a single chip the size of a postage stamp. It communicates over I2C or SPI, runs on 3.3V, and consumes only a few milliwatts — perfect for a low-power board like the Zero 2W.

Parameter Range Accuracy Resolution
Temperature−40 to +85 °C±0.5 °C0.01 °C
Humidity0 – 100 %RH±3 %RH0.008 %RH
Pressure300 – 1100 hPa±1 hPa0.18 Pa
Gas (VOC)IAQ 0 – 500Relative

The gas sensor works by heating a metal-oxide element to a target temperature and measuring the resistance of the surrounding air. Clean air produces a high resistance; the presence of VOCs — cooking fumes, cleaning products, paint, CO2 buildup — reduces it. This resistance reading is the raw material for the Indoor Air Quality (IAQ) index.

Why not a dedicated CO2 sensor?

True CO2 sensors (NDIR or electrochemical) are significantly more expensive. The BME680 does not directly measure CO2, but the gas resistance correlates well enough with overall air quality for a home environment. For lab-grade CO2 monitoring you would need an SCD40 or MH-Z19; for practical homelab purposes, the BME680 is excellent value.

The display: Elegoo 3.5″ TFT LCD

Alongside the sensor I added an Elegoo 3.5-inch TFT LCD touchscreen. Even though the software runs completely headless — no keyboard, no desktop — a local display transforms the device from a black box into something you can actually glance at and get information from immediately, no phone required.

The screen connects via the 40-pin GPIO header and is driven through the SPI bus, leaving the I2C bus completely free for the BME680. Setting it up on Raspberry Pi OS required loading the correct framebuffer driver and configuring the display rotation — a small obstacle that the Elegoo documentation covers clearly. Once running, the Python dashboard writes directly to the framebuffer: no X11, no desktop environment, no unnecessary overhead.

Wiring it all together

The BME680 breakout uses the I2C bus (GPIO 2/3 on the Pi header), while the Elegoo display uses SPI (GPIO 10/11). Both coexist on the same board because they use separate communication buses and separate chip-select lines.

# BME680 wiring (I2C)
VCC  -->  3.3V  (Pin 1)
GND  -->  GND   (Pin 6)
SDA  -->  GPIO2 (Pin 3)
SCL  -->  GPIO3 (Pin 5)

# Verify the sensor is detected on the I2C bus
i2cdetect -y 1
# Should show address 0x76 or 0x77

Software architecture

The Python application is structured around a main acquisition loop that runs on a configurable interval (default: 30 seconds). Each cycle reads all four sensor values, applies calibration offsets, calculates the IAQ index, stores the reading in SQLite, updates the local display, and checks alert thresholds.

The IAQ algorithm

The trickiest part of any BME680 project is translating the raw gas resistance into a meaningful air quality number. Bosch provides a proprietary library (BSEC) for this, but it is closed-source and adds significant complexity. Instead, I implemented a hybrid open approach:

IAQ Score Rating Typical cause
0 – 50ExcellentFresh outdoor air
51 – 100GoodNormal indoor conditions
101 – 150ModerateCooking, cleaning products
151 – 200PoorPoor ventilation, paint fumes
201 – 300BadHeavy pollution, open windows advised
301+HazardousSensor fault or extreme event

Telegram alerts

The system sends a Telegram notification when any threshold is crossed: temperature above 28 °C, humidity below 30% or above 70%, IAQ above 150, or pressure dropping more than 5 hPa in an hour — a reliable precursor to rain. Alerts include a trend indicator so you know whether conditions are improving or worsening.

Data storage and the live dashboard

Every reading is stored in a local SQLite database with a timestamp. A lightweight API endpoint on the Pi 5 (this server) fetches the latest reading from the Zero 2W and exposes it as JSON — which is exactly what feeds the live BME680 widget in the Live Stats section of this site.

# Deploy and enable the service
sudo systemctl enable bme680.service
sudo systemctl start bme680.service

# Follow live logs
sudo journalctl -u bme680 -f

# Query the last 24 hours from SQLite
sqlite3 data/bme680.db \
  "SELECT timestamp, temperature, humidity, pressure, iaq
   FROM readings
   WHERE timestamp > datetime('now', '-24 hours')
   ORDER BY timestamp DESC
   LIMIT 10;"

Lessons learned

1. The sensor needs a warm-up period. On cold start, the BME680 gas readings are unreliable for the first 5–10 minutes while the metal-oxide element stabilizes. Ignoring this produces false “excellent air quality” readings right after boot. The fix: discard the first N readings and only start alerting once the sensor has been running for a configurable warm-up duration.

2. Self-heating is real. The BME680 heats its own gas sensor element, and this slightly raises the chip temperature. Running the sensor continuously without any sleep between readings produces temperature values 2–3 °C higher than ambient. The solution: apply a configurable offset, or use the sensor’s low-power mode with a sleep interval between cycles.

3. The Pi Zero 2W is surprisingly capable. This tiny board runs the full monitoring stack — sensor acquisition, SQLite writes, display rendering, Telegram notifications, and a JSON API — at under 8% CPU and 120 MB RAM. Its biggest limitation is I/O: the microSD card is the bottleneck for database writes, which is why I configured SQLite in WAL mode with a batched sync interval rather than a commit per reading.

4. Gas baseline persistence matters. If the baseline is recalculated from scratch on every reboot, the first hour of readings after each restart is skewed. Persisting the baseline to a JSON file and loading it on startup eliminates the problem entirely.

End result

The BME680 monitor has been running continuously since deployment with zero crashes. It has caught two notable air quality events — one during a long cooking session and one when a cleaning product was used in an adjacent room — and sent Telegram alerts in real time. Exactly what it was built for.

← Back to blog 🔗 View on GitHub