ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
caqi_calculator.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
7
8namespace esphome::aqi {
9
11 public:
12 // The CAQI (CITEAIR) scale defines no maximum: its top "Very high" class is simply ">100". We
13 // therefore always extrapolate the top band past 100 without limit, so the extended_range flag
14 // (which lifts the AQI calculator's fixed 500 cap) has no meaning here and is ignored.
15 uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool /*extended_range*/) override {
16 float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
17 float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
18 float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
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 = 4;
25
26 static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}};
27
28 static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
29 // clang-format off
30 {0.0f, 15.1f},
31 {15.1f, 30.1f},
32 {30.1f, 55.1f},
33 {55.1f, 110.1f}
34 // clang-format on
35 };
36
37 static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
38 // clang-format off
39 {0.0f, 25.1f},
40 {25.1f, 50.1f},
41 {50.1f, 90.1f},
42 {90.1f, 180.1f}
43 // clang-format on
44 };
45
46 static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
47 int grid_index = get_grid_index(value, array);
48 if (grid_index == -1) {
49 return -1.0f;
50 }
51
52 float aqi_lo = INDEX_GRID[grid_index][0];
53 float aqi_hi = INDEX_GRID[grid_index][1];
54 float conc_lo = array[grid_index][0];
55 float conc_hi = array[grid_index][1];
56
57 // The top band is open-ended (see get_grid_index), so for concentrations above the last
58 // breakpoint this linear fit extrapolates past 100 unbounded, matching CAQI's open ">100" class.
59 return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
60 }
61
62 static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
63 for (int i = 0; i < NUM_LEVELS; i++) {
64 // The top band is open-ended: any value at or above its lower breakpoint falls into it.
65 const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]);
66 if (in_range) {
67 return i;
68 }
69 }
70 return -1;
71 }
72};
73
74} // namespace esphome::aqi
static constexpr int INDEX_GRID[NUM_LEVELS][2]
static constexpr float PM10_0_GRID[NUM_LEVELS][2]
uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool) override
static constexpr float PM2_5_GRID[NUM_LEVELS][2]
static float calculate_index(float value, const float array[NUM_LEVELS][2])
static int get_grid_index(float value, const float array[NUM_LEVELS][2])
static constexpr int NUM_LEVELS