[aqi, hm3301, pmsx003] Air Quality Index improvements (#12203)

Co-authored-by: jas <jas@asspa.in>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
Jas Strong
2025-12-19 10:42:11 -08:00
committed by GitHub
parent eaca81c3ab
commit 940e619481
13 changed files with 109 additions and 38 deletions

View File

@@ -42,6 +42,7 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr

View File

@@ -0,0 +1,14 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -2,13 +2,11 @@
#include <cstdint>
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AbstractAQICalculator {
public:
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -1,10 +1,11 @@
#pragma once
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AQICalculator : public AbstractAQICalculator {
public:
@@ -28,6 +29,9 @@ class AQICalculator : public AbstractAQICalculator {
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
int grid_index = get_grid_index_(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = index_grid_[grid_index][0];
int aqi_hi = index_grid_[grid_index][1];
int conc_lo = array[grid_index][0];
@@ -46,5 +50,4 @@ class AQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "caqi_calculator.h"
#include "aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 };
@@ -12,18 +11,17 @@ class AQICalculatorFactory {
public:
AbstractAQICalculator *get_calculator(AQICalculatorType type) {
if (type == 0) {
return caqi_calculator_;
return &this->caqi_calculator_;
} else if (type == 1) {
return aqi_calculator_;
return &this->aqi_calculator_;
}
return nullptr;
}
protected:
CAQICalculator *caqi_calculator_ = new CAQICalculator();
AQICalculator *aqi_calculator_ = new AQICalculator();
CAQICalculator caqi_calculator_;
AQICalculator aqi_calculator_;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "esphome/core/log.h"
#include "abstract_aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class CAQICalculator : public AbstractAQICalculator {
public:
@@ -48,5 +47,4 @@ class CAQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -63,7 +63,7 @@ void HM3301Component::update() {
int16_t aqi_value = -1;
if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) {
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value);
}

View File

@@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "aqi_calculator_factory.h"
#include "esphome/components/aqi/aqi_calculator_factory.h"
namespace esphome {
namespace hm3301 {
@@ -19,7 +19,7 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; }
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
void setup() override;
void dump_config() override;
@@ -41,8 +41,8 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *pm_10_0_sensor_{nullptr};
sensor::Sensor *aqi_sensor_{nullptr};
AQICalculatorType aqi_calc_type_;
AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory();
aqi::AQICalculatorType aqi_calc_type_;
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
bool validate_checksum_(const uint8_t *data);
uint16_t get_sensor_value_(const uint8_t *data, uint8_t i);

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -16,23 +17,16 @@ from esphome.const import (
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["aqi"]
CODEOWNERS = ["@freekode"]
hm3301_ns = cg.esphome_ns.namespace("hm3301")
HM3301Component = hm3301_ns.class_(
"HM3301Component", cg.PollingComponent, i2c.I2CDevice
)
AQICalculatorType = hm3301_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
UNIT_INDEX = "index"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}
def _validate(config):
if CONF_AQI in config and CONF_PM_2_5 not in config:

View File

@@ -18,6 +18,8 @@ static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automaticall
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
void PMSX003Component::setup() {}
void PMSX003Component::dump_config() {
ESP_LOGCONFIG(TAG, "PMSX003:");
LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_);
@@ -39,21 +41,36 @@ void PMSX003Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
if (this->update_interval_ <= PMS_STABILISING_MS) {
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
} else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
}
this->check_uart_settings(9600);
}
void PMSX003Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Initialize sensor mode on first loop
if (this->initialised_ == 0) {
if (this->update_interval_ > PMS_STABILISING_MS) {
// Long update interval: use passive mode with sleep/wake cycles
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
} else {
// Short/zero update interval: use active continuous mode
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE);
}
this->initialised_ = 1;
}
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) {
if (this->initialised_ == 0) {
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
this->initialised_ = 1;
}
switch (this->state_) {
case PMSX003_STATE_IDLE:
// Power on the sensor now so it'll be ready when we hit the update time
@@ -248,6 +265,13 @@ void PMSX003Component::parse_data_() {
if (this->pm_particles_25um_sensor_ != nullptr)
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
// Calculate and publish AQI if sensor is configured
if (this->aqi_sensor_ != nullptr) {
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
int32_t aqi_value = calculator->get_aqi(pm_2_5_concentration, pm_10_0_concentration);
this->aqi_sensor_->publish_state(aqi_value);
}
if (this->type_ == PMSX003_TYPE_5003T) {
ESP_LOGD(TAG,
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "

View File

@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/aqi/aqi_calculator_factory.h"
namespace esphome {
namespace pmsx003 {
@@ -31,6 +32,7 @@ enum PMSX003State {
class PMSX003Component : public uart::UARTDevice, public Component {
public:
PMSX003Component() = default;
void setup() override;
void dump_config() override;
void loop() override;
@@ -72,6 +74,10 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
protected:
optional<bool> check_byte_();
void parse_data_();
@@ -115,6 +121,12 @@ class PMSX003Component : public uart::UARTDevice, public Component {
// Temperature and Humidity
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
// AQI
sensor::Sensor *aqi_sensor_{nullptr};
aqi::AQICalculatorType aqi_calc_type_;
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
};
} // namespace pmsx003

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
import esphome.config_validation as cv
from esphome.const import (
CONF_FORMALDEHYDE,
@@ -20,6 +21,7 @@ from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
@@ -35,11 +37,13 @@ from esphome.const import (
CODEOWNERS = ["@ximex"]
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["aqi"]
pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
UNIT_INDEX = "index"
TYPE_PMSX003 = "PMSX003"
TYPE_PMS5003T = "PMS5003T"
TYPE_PMS5003ST = "PMS5003ST"
@@ -77,6 +81,10 @@ def validate_pmsx003_sensors(value):
for key, types in SENSORS_TO_TYPE.items():
if key in value and value[CONF_TYPE] not in types:
raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!")
if CONF_AQI in value and CONF_PM_2_5 not in value:
raise cv.Invalid("AQI computation requires PM 2.5 sensor")
if CONF_AQI in value and CONF_PM_10_0 not in value:
raise cv.Invalid("AQI computation requires PM 10 sensor")
return value
@@ -192,6 +200,19 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
AQI_CALCULATION_TYPE, upper=True
),
}
),
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
}
)
@@ -278,4 +299,9 @@ async def to_code(config):
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))
if CONF_AQI in config:
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi_sensor(sens))
cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE]))
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))

View File

@@ -25,4 +25,7 @@ sensor:
name: Particulate Count >5.0um
pm_10_0um:
name: Particulate Count >10.0um
aqi:
name: AQI
calculation_type: AQI
update_interval: 30s