mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
[aqi] Add extended_range option for over-range AQI values (#17570)
Co-authored-by: jas <jas@asspa.in> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
jas
Claude Opus 4.8
parent
c4975e1870
commit
dedca344f9
@@ -0,0 +1,35 @@
|
||||
"""Config-validation tests for the aqi sensor component."""
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome.components.aqi import CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE
|
||||
from esphome.components.aqi.sensor import _validate_extended_range
|
||||
|
||||
|
||||
def test_extended_range_rejected_with_caqi():
|
||||
"""extended_range has no meaning for CAQI (no spec maximum) and must be rejected."""
|
||||
with pytest.raises(Invalid, match="CAQI"):
|
||||
_validate_extended_range(
|
||||
{CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: True}
|
||||
)
|
||||
|
||||
|
||||
def test_extended_range_rejected_with_caqi_even_when_false():
|
||||
"""The option is not allowed at all with CAQI, regardless of its value."""
|
||||
with pytest.raises(Invalid, match="CAQI"):
|
||||
_validate_extended_range(
|
||||
{CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: False}
|
||||
)
|
||||
|
||||
|
||||
def test_extended_range_allowed_with_aqi():
|
||||
"""extended_range is valid for the US AQI calculation."""
|
||||
config = {CONF_CALCULATION_TYPE: "AQI", CONF_EXTENDED_RANGE: True}
|
||||
assert _validate_extended_range(config) is config
|
||||
|
||||
|
||||
def test_caqi_without_extended_range_ok():
|
||||
"""CAQI is fine as long as extended_range is not set."""
|
||||
config = {CONF_CALCULATION_TYPE: "CAQI"}
|
||||
assert _validate_extended_range(config) is config
|
||||
@@ -0,0 +1,16 @@
|
||||
# Declares the component graph the C++ unit test build needs so that the aqi
|
||||
# component's sources (which include sensor.h) compile. to_code is suppressed by
|
||||
# the test harness; this only pulls the sensor + aqi source/include paths in.
|
||||
# Loaded with plain yaml.safe_load, so avoid lambdas / ESPHome-tagged values here.
|
||||
sensor:
|
||||
- platform: template
|
||||
id: pm25_sensor
|
||||
name: "PM2.5"
|
||||
- platform: template
|
||||
id: pm10_sensor
|
||||
name: "PM10"
|
||||
- platform: aqi
|
||||
name: "AQI"
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: AQI
|
||||
@@ -20,3 +20,10 @@ sensor:
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: CAQI
|
||||
|
||||
- platform: aqi
|
||||
name: "Air Quality Index (AQI, extended)"
|
||||
pm_2_5: pm25_sensor
|
||||
pm_10_0: pm10_sensor
|
||||
calculation_type: AQI
|
||||
extended_range: true
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/aqi/aqi_calculator.h"
|
||||
#include "esphome/components/aqi/caqi_calculator.h"
|
||||
|
||||
namespace esphome::aqi::testing {
|
||||
|
||||
// US AQI (EPA 2024): PM2.5 225.5-500.4 -> 301-500, PM10 425-604 -> 301-500.
|
||||
|
||||
TEST(USAQI, LowRangeUnaffectedByExtendedFlag) {
|
||||
AQICalculator calc;
|
||||
// PM2.5 25 drives over PM10 50; well below the top band, so the flag changes nothing.
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 81);
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, true), 81);
|
||||
}
|
||||
|
||||
TEST(USAQI, HazardousInterpolatesNotPinnedAt301) {
|
||||
AQICalculator calc;
|
||||
// Regression guard: the old FLT_MAX top bucket collapsed every hazardous reading to 301.
|
||||
EXPECT_EQ(calc.get_aqi(225.5f, 0.0f, false), 301); // band start
|
||||
EXPECT_EQ(calc.get_aqi(250.0f, 0.0f, false), 319); // interpolated, not 301
|
||||
EXPECT_EQ(calc.get_aqi(500.4f, 0.0f, false), 500); // band top
|
||||
}
|
||||
|
||||
TEST(USAQI, DefaultClampsAtStandardMaximum) {
|
||||
AQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, false), 500);
|
||||
EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, false), 500);
|
||||
EXPECT_EQ(calc.get_aqi(0.0f, 604.0f, false), 500); // PM10 top breakpoint
|
||||
}
|
||||
|
||||
TEST(USAQI, ExtendedRangeExtrapolatesBeyond500) {
|
||||
AQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(600.0f, 0.0f, true), 572);
|
||||
EXPECT_EQ(calc.get_aqi(1000.0f, 0.0f, true), 862);
|
||||
EXPECT_EQ(calc.get_aqi(0.0f, 700.0f, true), 607); // PM10 extrapolated past 500
|
||||
}
|
||||
|
||||
TEST(USAQI, ExtendedRangeSaturatesUint16NoWraparound) {
|
||||
AQICalculator calc;
|
||||
// An absurd concentration would overflow uint16_t; it must saturate, not wrap to a small value.
|
||||
EXPECT_EQ(calc.get_aqi(100000.0f, 0.0f, true), 65535);
|
||||
}
|
||||
|
||||
TEST(USAQI, WorseOfTwoPollutantsWins) {
|
||||
AQICalculator calc;
|
||||
// PM10 604 -> 500 dominates PM2.5 25 -> 81.
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 604.0f, false), 500);
|
||||
}
|
||||
|
||||
// CAQI (CITEAIR): no maximum by spec -- the top ">100" class is open, so it is always unbounded
|
||||
// and the extended_range flag does not apply.
|
||||
|
||||
TEST(CAQI, LowRange) {
|
||||
CAQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(25.0f, 50.0f, false), 50);
|
||||
}
|
||||
|
||||
TEST(CAQI, ContinuousAt100NoPinAt101) {
|
||||
CAQICalculator calc;
|
||||
// Old code pinned everything above the top breakpoint to 101; now it reaches exactly 100.
|
||||
EXPECT_EQ(calc.get_aqi(110.1f, 0.0f, false), 100);
|
||||
}
|
||||
|
||||
TEST(CAQI, UnboundedAboveTopBand) {
|
||||
CAQICalculator calc;
|
||||
EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, false), 139);
|
||||
EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, false), 925);
|
||||
}
|
||||
|
||||
TEST(CAQI, ExtendedRangeFlagIsIgnored) {
|
||||
CAQICalculator calc;
|
||||
// CAQI is always unbounded, so the flag must make no difference either way.
|
||||
EXPECT_EQ(calc.get_aqi(200.0f, 0.0f, true), calc.get_aqi(200.0f, 0.0f, false));
|
||||
EXPECT_EQ(calc.get_aqi(2000.0f, 0.0f, true), calc.get_aqi(2000.0f, 0.0f, false));
|
||||
}
|
||||
|
||||
TEST(CAQI, SaturatesUint16NoWraparound) {
|
||||
CAQICalculator calc;
|
||||
// CAQI is unbounded, so an extreme reading can extrapolate past uint16_t; it must saturate,
|
||||
// not wrap around to a small (falsely "good") value.
|
||||
EXPECT_EQ(calc.get_aqi(200000.0f, 0.0f, false), 65535);
|
||||
}
|
||||
|
||||
} // namespace esphome::aqi::testing
|
||||
Reference in New Issue
Block a user