Projects / Air quality

Wi-Fi Air Quality Dashboard

Air quality index, TVOC and eCO₂ on any phone in the house.

Beginner⏱ 45 minutes3 ElectroTrical boards

Overview

An affordable air-quality monitor built from three small boards. The ET-S66 ENS160 metal-oxide sensor reports an air quality index (1–5), total VOCs and an equivalent CO₂ estimate; the ET-S61 AHT20 feeds it temperature and humidity so its readings stay accurate.

The ET-M01 joins your Wi-Fi and serves a simple page you can open from any phone or laptop on the same network. Two Qwiic cables, no soldering.

  • Air quality index 1–5 (UBA scale)
  • TVOC in ppb and eCO₂ in ppm
  • Temperature and humidity
  • Live web page on your home network

What you need

ElectroTrical boards

ET-M01ESP32-C3 Mini Dev BoardWi-Fi controller with Qwiic port
Coming soon
ET-S66ENS160 Air Quality SensorAQI, TVOC and eCO₂
Coming soon
ET-S61AHT20 Temperature + Humidity SensorTemperature and humidity, also used to compensate the ENS160
Coming soon

You'll also need

  • 2 Qwiic cables
  • USB-C cable and 5 V USB charger

Wiring

Addresses: ENS160 0x53, AHT20 0x38. The ET-M01's Qwiic port is on IO6 (SDA) and IO7 (SCL).

Qwiic chain

FromToNote
ET-M01 QwiicET-S66 Qwiic
ET-S66 QwiicET-S61 Qwiic

Build it

  1. 01

    Connect

    Chain the three boards with Qwiic cables.

  2. 02

    Set up the Arduino IDE

    Install the Espressif ESP32 package, select “ESP32C3 Dev Module” and set USB CDC On Boot to Enabled. Install Adafruit AHTX0 from Library Manager.

  3. 03

    Add your Wi-Fi

    Put your network name and password in WIFI_SSID and WIFI_PASS at the top of the sketch. The ESP32-C3 supports 2.4 GHz networks only.

  4. 04

    Upload and open the page

    Serial Monitor shows the board's IP address. Open it in a browser on the same network; the page refreshes every 5 seconds.

  5. 05

    Allow warm-up

    The ENS160 needs about 3 minutes after each power-up, and up to an hour on its very first start, before readings are valid. The page shows “warming up” until then.

Arduino code

Written for the ET-M01 ESP32-C3 Mini Dev Board. Install these libraries from the Arduino Library Manager:

  • Adafruit AHTX0
air_dashboard.ino
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_AHTX0.h>

const char* WIFI_SSID = "your-network";
const char* WIFI_PASS = "your-password";

const uint8_t ENS = 0x53;
Adafruit_AHTX0 aht;
WebServer server(80);

float tC = 0, rh = 0;
int aqi = 0, tvoc = 0, eco2 = 0;
const char* state = "warming up";

void ensWrite(uint8_t reg, const uint8_t* d, int n) {
  Wire.beginTransmission(ENS); Wire.write(reg); Wire.write(d, n); Wire.endTransmission();
}
uint8_t ensRead(uint8_t reg, uint8_t* d, int n) {
  Wire.beginTransmission(ENS); Wire.write(reg); Wire.endTransmission(false);
  Wire.requestFrom(ENS, (uint8_t)n);
  for (int i = 0; i < n; i++) d[i] = Wire.read();
  return d[0];
}

void readSensors() {
  sensors_event_t h, t;
  aht.getEvent(&h, &t);
  tC = t.temperature; rh = h.relative_humidity;

  // compensation: temperature in Kelvin x 64, humidity x 512 (little-endian)
  uint16_t tk = (uint16_t)((tC + 273.15) * 64), hr = (uint16_t)(rh * 512);
  uint8_t comp[4] = { (uint8_t)tk, (uint8_t)(tk >> 8), (uint8_t)hr, (uint8_t)(hr >> 8) };
  ensWrite(0x13, comp, 4);

  uint8_t st;
  ensRead(0x20, &st, 1);                       // DEVICE_STATUS
  int validity = (st >> 2) & 0x03;             // 0 ok, 1 warm-up, 2 initial start-up
  state = validity == 0 ? "ok" : validity == 1 ? "warming up" : "first start-up (up to 1 h)";
  if (st & 0x02) {                             // NEWDAT
    uint8_t d[5];
    ensRead(0x21, d, 5);                       // AQI, TVOC (2), eCO2 (2)
    aqi = d[0] & 0x07;
    tvoc = d[1] | (d[2] << 8);
    eco2 = d[3] | (d[4] << 8);
  }
}

void handleRoot() {
  static const char* label[] = {"-", "Excellent", "Good", "Moderate", "Poor", "Unhealthy"};
  String html = "<!doctype html><meta name=viewport content='width=device-width'>"
    "<meta http-equiv=refresh content=5><body style='font:18px sans-serif;max-width:420px;margin:30px auto'>"
    "<h2>Air quality</h2>";
  html += "<p style='font-size:42px;margin:0'><b>" + String(aqi) + "</b> " + label[aqi] + "</p>";
  html += "<p>TVOC " + String(tvoc) + " ppb<br>eCO2 " + String(eco2) + " ppm<br>";
  html += String(tC, 1) + " &deg;C &middot; " + String(rh, 0) + " %RH</p>";
  html += "<p style='color:#888'>Sensor: " + String(state) + "</p>";
  server.send(200, "text/html", html);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(6, 7);
  if (!aht.begin()) Serial.println("AHT20 not found (0x38)");
  uint8_t mode = 0x02;                         // OPMODE = standard
  ensWrite(0x10, &mode, 1);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.print("\nOpen http://"); Serial.println(WiFi.localIP());
  server.on("/", handleRoot);
  server.begin();
}

void loop() {
  static unsigned long last = 0;
  if (millis() - last > 2000) { last = millis(); readSensors(); }
  server.handleClient();
}

Troubleshooting

Stuck on “first start-up”

Normal on the very first power-up of a new ENS160 — leave it running for an hour. After that, warm-up takes about 3 minutes.

eCO₂ reads high in a clean room

eCO₂ is estimated from VOCs, not measured. For true CO₂ use the ET-S02 SCD41 (see the Indoor Air Quality Monitor project).

No IP address appears

Check the Wi-Fi name and password, and that the network is 2.4 GHz.

Take it further

  • Add the ET-D07 LED ring as a green/amber/red air light
  • Expose the readings to Home Assistant with ESPHome
  • Log readings to the ET-A14 FRAM for a 24-hour graph

Turning this into a product?

We design custom boards that combine these sensors on one PCB, with firmware and production.