[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:
@@ -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
|
||||
|
||||
14
esphome/components/aqi/__init__.py
Normal file
14
esphome/components/aqi/__init__.py
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user