-----
The Air Quality Index (AQI) is an indicator as to how safe the air is to breath. There are many websites that provide this information, but we wanted a 24x7 visual readout in the house. Our rig does not measure proximity (in room) AQI. Instead it polls one of the many AQI websites every 15 minutes to get the reading for our local region.
-----
The hardware setup is as about as simple as it gets. Just get an ESP32 C3 (or pretty much any uC with WIFI) and connect the RGB LED to the pins programed in the software. We also 3D printed a fancy display box, but that is optional.
Thanks to the wonders of AI the program was super easy to write. We added a few debug statements and serial monitor outputs just to verify some things, but overall the AI did the heavy lifting. Here's the Arduino IDE script:
// Polls Open-Meteo Air Quality at set intervals
// and turns a RGB LED Green, Yellow, or Red based on the local AQI number.
// Blue is displayed if a valid AQI number not fetched (ie, no connection).
//
// Mostly written by AI, but see
// WhiskeyTangoHotel.Com for project details.
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// Wi-Fi Credentials
const char* ssid = "ur_wifi_ssid";
const char* password = "ur_wifi_password";
// Polling Frequency Configuration in seconds
const unsigned long pollIntervalSeconds = 900;
const unsigned long fetchInterval = pollIntervalSeconds * 1000;
// Open-Meteo Air Quality API for Austin, TX (Coordinates: 30.2672, -97.7431)
const char* apiURL = "https://air-quality-api.open-meteo.com/v1/air-quality?latitude=30.2672&longitude=-97.7431¤t=us_aqi";
// RGB LED GPIO Pins on ESP32-C3
const int redPin = 4;
const int greenPin = 5;
const int bluePin = 6;
// Timing Variables
unsigned long lastFetchTime = 0;
unsigned long lastBlinkTime = 0;
bool blinkState = false;
int currentAQI = -1; // -1 means no reading yet
String currentLedColor = "Off";
// Helper to set RGB pin states and track string color description
void setLED(bool red, bool green, bool blue, String colorName) {
digitalWrite(redPin, red ? HIGH : LOW);
digitalWrite(greenPin, green ? HIGH : LOW);
digitalWrite(bluePin, blue ? HIGH : LOW);
currentLedColor = colorName;
}
// Visual LED Self-Test Cycle (Red -> Yellow -> Green)
void runSelfTest() {
Serial.println("Running LED Self-Test...");
for (int i = 1; i <= 2; i++) {
setLED(true, false, false, "Self-Test: Red");
delay(300);
setLED(true, true, false, "Self-Test: Yellow");
delay(300);
setLED(false, true, false, "Self-Test: Green");
delay(300);
}
setLED(false, false, false, "Self-Test: OFF");
}
// Function to fetch AQI from Open-Meteo
int fetchAQI() {
if (WiFi.status() != WL_CONNECTED) return -1;
HTTPClient http;
http.begin(apiURL);
int httpCode = http.GET();
int parsedAQI = -1;
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
// Allocate a JSON document buffer
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
// Extract the US AQI value from the JSON tree
parsedAQI = doc["current"]["us_aqi"];
} else {
Serial.println("Failed to parse JSON response");
}
} else {
Serial.printf("HTTP GET failed, error code: %d\n", httpCode);
}
http.end();
return parsedAQI;
}
void printStatusToSerial() {
// Determine the color description based on current AQI for the poll log
String statusColor = "";
if (currentAQI < 0) {
statusColor = "Blue (Error)";
} else if (currentAQI <= 50) {
statusColor = "Green";
} else if (currentAQI <= 100) {
statusColor = "Yellow";
} else {
statusColor = "Red (Flashing)";
}
Serial.println("----------------------------------------");
Serial.print("Device IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Current AQI Number: ");
if (currentAQI < 0) {
Serial.println("Unavailable (Error)");
} else {
Serial.println(currentAQI);
}
Serial.print("Assigned LED Color: ");
Serial.println(statusColor);
Serial.println("----------------------------------------");
}
void setup() {
Serial.begin(115200);
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
// Turn off LEDs initially
setLED(false, false, false, "Off");
Serial.print("Connecting to Wi-Fi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi Connected!");
// Run self-test and perform initial fetch on boot
runSelfTest();
currentAQI = fetchAQI();
printStatusToSerial();
lastFetchTime = millis();
}
void loop() {
unsigned long currentMillis = millis();
// --- MANUAL OVERVIEW FOR TESTING (Optional) ---
// Uncomment line below if you want to force test an AQI value:
// currentAQI = 125;
// ----------------------------------------------
// Fetch new data based on the poll interval variable (60 seconds)
if (currentMillis - lastFetchTime >= fetchInterval) {
lastFetchTime = currentMillis;
// Trigger LED self-test to signal a poll update is happening
runSelfTest();
int newAQI = fetchAQI();
if (newAQI >= 0) {
currentAQI = newAQI;
}
// Print stats only when a new polling happens
printStatusToSerial();
}
// Handle LED States based on AQI value
if (currentAQI < 0) {
setLED(false, false, true, "Blue (Error)");
}
else if (currentAQI <= 50) {
setLED(false, true, false, "Green");
}
else if (currentAQI <= 100) {
setLED(true, true, false, "Yellow");
}
else {
if (currentMillis - lastBlinkTime >= 1000) {
lastBlinkTime = currentMillis;
blinkState = !blinkState;
}
setLED(blinkState, false, false, blinkState ? "Red (ON)" : "Red (OFF)");
}
}
-----

