An ESP32 air-quality monitor with five sensors
I wanted to know what the air in the house was actually like: how stuffy it gets, how much dust is floating about, and whether cooking fills the place with anything nasty.
The answer was an ESP32 running ESPHome, with as many sensors hung off it as it would take. It’s my first ESPHome project on an ESP32, and I built it in October 2021.
The sensors
It’s built on a Wemos D1 mini32, the ESP32 version of the little D1 mini board, and it has five sensors:
- PMS5003: a laser particle counter. A fan pulls air past a laser and it counts what scatters the light, giving PM1, PM2.5 and PM10 in µg/m³.
- MH-Z19: a proper NDIR CO₂ sensor. It shines infrared through a little chamber and measures how much the CO₂ absorbs.
- SGP30: a metal-oxide gas sensor for total volatile organic compounds (TVOC), plus an “equivalent CO₂” (eCO₂) figure that it estimates from those gases.
- BME280: temperature, humidity and pressure.
- TSL2561: ambient light, in lux.
That would give two CO₂ readings, but the SGP30’s is only a guess from other gases, and the MH-Z19 measures the real thing. So I don’t use the SGP30’s eCO₂ at all; it’s only there for the TVOC.
The SGP30 is fed the BME280’s temperature and humidity, which it uses to compensate its readings. The BME280 itself reads about a degree high, because the ESP32 warms up the inside of the box, so after checking it against another thermometer I gave it a −1 °C offset.
Two UARTs and no logs
The two chattiest sensors, the PMS5003 and the MH-Z19, both talk over serial, which means both need a UART. The MH-Z19 gets the ESP32’s second UART. The PMS5003 is wired to GPIO3, which is the RX pin of the main serial port, the one ESPHome normally uses for logging. So logging over serial is switched off (baud_rate: 0), and the logs come over Wi-Fi instead. It’s a fair trade: you only really need the USB serial when something has gone badly wrong.
The MH-Z19 has its automatic baseline calibration turned off. Left on, it assumes it sees fresh outdoor air (about 400 ppm) at some point every day and quietly recalibrates to match. That’s fine in an office that empties every night, but less so in a house that’s rarely aired all at once. Instead there’s a “CO₂ Sensor Zero Calibration” switch in Home Assistant. You take the box outside, or open all the windows, wait, and press the switch, and it zeroes itself to 400 ppm.
Turning it into one word
Numbers in µg/m³ don’t mean much at a glance, so the firmware also keeps a 24-hour moving average of PM2.5 (one sample a minute, 1,440 of them) and maps it onto a text sensor:
| 24 h PM2.5 (µg/m³) | Air quality |
|---|---|
| below 10 | Good |
| 10–20 | Fair |
| 20–25 | Moderate |
| 25–50 | Poor |
| 50–75 | Very Poor |
| above 75 | Extremely Poor |
Those bands are the European Environment Agency’s air-quality index bands for PM2.5. There’s also a “PM2.5 median” sensor, which takes the median of 30 readings every 30 seconds, to knock out the odd spike if you want to drive an automation from it.
Where it stands
It’s running ESPHome 2021.10.2 and feeding Home Assistant with temperature, humidity, pressure, light, CO₂, TVOC, three sizes of particulate, and a one-word verdict on the lot.
The biggest lesson so far is about the study door. With it shut, especially during a meeting, the CO₂ climbs faster than I’d have expected. Open the door afterwards and it quickly drops back to normal, and the room stops feeling stuffy. So the door stays open a lot more these days.