ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
aqi_calculator.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
7
8// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
9
10namespace esphome::aqi {
11
13 public:
14 uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) override {
15 float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID, extended_range);
16 float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID, extended_range);
17 float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
18 // extended_range lets the index run past the standard maximum, so clamp to the sensor's range.
19 aqi = std::min(aqi, static_cast<float>(std::numeric_limits<uint16_t>::max()));
20 return static_cast<uint16_t>(std::lround(aqi));
21 }
22
23 protected:
24 static constexpr int NUM_LEVELS = 6;
25
26 static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
27
28 static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
29 // clang-format off
30 {0.0f, 9.1f},
31 {9.1f, 35.5f},
32 {35.5f, 55.5f},
33 {55.5f, 125.5f},
34 {125.5f, 225.5f},
35 {225.5f, 500.4f} // EPA 2024: AQI 301-500 maps to PM2.5 225.5-500.4 ug/m3
36 // clang-format on
37 };
38
39 static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
40 // clang-format off
41 {0.0f, 55.0f},
42 {55.0f, 155.0f},
43 {155.0f, 255.0f},
44 {255.0f, 355.0f},
45 {355.0f, 425.0f},
46 {425.0f, 604.0f} // EPA: AQI 301-500 maps to PM10 425-604 ug/m3 (top of the 401-500 band)
47 // clang-format on
48 };
49
50 static float calculate_index(float value, const float array[NUM_LEVELS][2], bool extended_range) {
51 int grid_index = get_grid_index(value, array);
52 if (grid_index == -1) {
53 return -1.0f;
54 }
55 float aqi_lo = INDEX_GRID[grid_index][0];
56 float aqi_hi = INDEX_GRID[grid_index][1];
57 float conc_lo = array[grid_index][0];
58 float conc_hi = array[grid_index][1];
59
60 float index = (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
61
62 // Concentrations above the highest breakpoint run the linear fit past aqi_hi. By default we
63 // clamp to the standard maximum; with extended_range we keep the extrapolated "over-range"
64 // value so heavy pollution reports numbers beyond what the standard defines.
65 if (grid_index == NUM_LEVELS - 1 && !extended_range && index > aqi_hi) {
66 return aqi_hi;
67 }
68 return index;
69 }
70
71 static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
72 for (int i = 0; i < NUM_LEVELS; i++) {
73 // The top band is open-ended: any value at or above its lower breakpoint falls into it,
74 // and calculate_index() decides whether to clamp or extrapolate.
75 const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]);
76 if (in_range) {
77 return i;
78 }
79 }
80 return -1;
81 }
82};
83
84} // namespace esphome::aqi
static constexpr float PM2_5_GRID[NUM_LEVELS][2]
static constexpr int NUM_LEVELS
static int get_grid_index(float value, const float array[NUM_LEVELS][2])
static float calculate_index(float value, const float array[NUM_LEVELS][2], bool extended_range)
uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) override
static constexpr float PM10_0_GRID[NUM_LEVELS][2]
static constexpr int INDEX_GRID[NUM_LEVELS][2]