From 7e080920122246bd37863b117ccafd7c8b74a0ed Mon Sep 17 00:00:00 2001 From: Anna Oake Date: Wed, 17 Dec 2025 20:19:18 +0100 Subject: [PATCH 01/10] [cc1101] Fix default frequencies (#12539) --- esphome/components/cc1101/cc1101.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 5b6eb545bc..1fe402d6c6 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -99,11 +99,11 @@ CC1101Component::CC1101Component() { this->state_.FS_AUTOCAL = 1; // Default Settings - this->set_frequency(433920); - this->set_if_frequency(153); - this->set_filter_bandwidth(203); + this->set_frequency(433920000); + this->set_if_frequency(153000); + this->set_filter_bandwidth(203000); this->set_channel(0); - this->set_channel_spacing(200); + this->set_channel_spacing(200000); this->set_symbol_rate(5000); this->set_sync_mode(SyncMode::SYNC_MODE_NONE); this->set_carrier_sense_above_threshold(true); From 195b1c632369011fe1fe6b841a75126ce9b84758 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Wed, 17 Dec 2025 20:40:31 +0000 Subject: [PATCH 02/10] [pm1006] Fix "never" update interval detection (#12529) --- esphome/components/pm1006/sensor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index c693cfea19..8ff21ab069 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -7,10 +7,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, DEVICE_CLASS_PM25, ICON_BLUR, + SCHEDULER_DONT_RUN, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ) -from esphome.core import TimePeriodMilliseconds CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] @@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All( def validate_interval_uart(config): - require_tx = False - interval = config.get(CONF_UPDATE_INTERVAL) - - if isinstance(interval, TimePeriodMilliseconds): - # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects - require_tx = True - uart.final_validate_device_schema( - "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + "pm1006", + baud_rate=9600, + require_rx=True, + require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN, )(config) From 636be92c97f6c6dd7da511a6420768981253f5e2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:07:42 -0500 Subject: [PATCH 03/10] [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535) Co-authored-by: Claude --- esphome/components/bme68x_bsec2_i2c/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py index d6fb7fa9be..c8ca0ba022 100644 --- a/esphome/components/bme68x_bsec2_i2c/__init__.py +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"] AUTO_LOAD = ["bme68x_bsec2"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( From 3e38a5e630978ab076a34223948d22ec53922c75 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:37:59 -0500 Subject: [PATCH 04/10] [esp32_camera] Fix I2C driver conflict with other components (#12533) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d9d9bc0a56..2ad48173f1 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -3,7 +3,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( @@ -352,6 +352,8 @@ async def to_code(config): cg.add_define("USE_CAMERA") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) From 7ca11764abf1091d2f009599f49dcece33b0d05e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:34:04 -0500 Subject: [PATCH 05/10] [template.alarm_control_panel] Fix compile without binary_sensor (#12548) Co-authored-by: Claude --- .../alarm_control_panel/template_alarm_control_panel.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index bdd3747372..2038d8f1b0 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode { ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, }; +#ifdef USE_BINARY_SENSOR struct SensorDataStore { bool last_chime_state; }; @@ -49,7 +50,6 @@ struct SensorInfo { uint8_t store_index; }; -#ifdef USE_BINARY_SENSOR struct AlarmSensor { binary_sensor::BinarySensor *sensor; SensorInfo info; @@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; + // Per sensor data store + std::vector sensor_data_; + uint8_t next_store_index_ = 0; #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl uint32_t trigger_time_; // a list of codes std::vector codes_; - // Per sensor data store - std::vector sensor_data_; // requires a code to arm bool requires_code_to_arm_ = false; bool supports_arm_home_ = false; bool supports_arm_night_ = false; bool sensors_ready_ = false; - uint8_t next_store_index_ = 0; // check if the code is valid bool is_code_valid_(optional code); From f0d0ea60a727f020aafe26471ba22b5168b6e73a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 18 Dec 2025 19:42:47 -0600 Subject: [PATCH 06/10] [esp32_ble, esp32_ble_tracker] Fix crash, error messages when `ble.disable` called during boot (#12560) --- esphome/components/esp32_ble/ble.cpp | 16 ++++++++++++---- esphome/components/esp32_ble/ble.h | 12 +++++++++--- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 5 ++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index a0ed9ee90c..a279f7d2a4 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_dismantle_() { esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); - return false; + // ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine + if (err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); + return false; + } + ESP_LOGD(TAG, "Already disabled"); } err = esp_bluedroid_deinit(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); - return false; + // ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine + if (err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); + return false; + } + ESP_LOGD(TAG, "Already deinitialized"); } #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 393ec2e911..1999c870f8 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -212,17 +212,23 @@ extern ESP32BLE *global_ble; template class BLEEnabledCondition : public Condition { public: - bool check(const Ts &...x) override { return global_ble->is_active(); } + bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); } }; template class BLEEnableAction : public Action { public: - void play(const Ts &...x) override { global_ble->enable(); } + void play(const Ts &...x) override { + if (global_ble != nullptr) + global_ble->enable(); + } }; template class BLEDisableAction : public Action { public: - void play(const Ts &...x) override { global_ble->disable(); } + void play(const Ts &...x) override { + if (global_ble != nullptr) + global_ble->disable(); + } }; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d3c5edfb94..45e343c0d2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -185,7 +185,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); void ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { - ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); + // If scanner is already idle, there's nothing to stop - this is not an error + if (this->scanner_state_ != ScannerState::IDLE) { + ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); + } return; } // Reset timeout state machine when stopping scan From 3a888326d8056867380b8b26dffea1b24246d1af Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:13:35 -0500 Subject: [PATCH 07/10] Bump version to 2025.12.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 7dfcbd6b6f..4c533ec87f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.12.0 +PROJECT_NUMBER = 2025.12.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 111396cab5..8f9a3497ff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0" +__version__ = "2025.12.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 940e6194816f25401e57ec76fd33fff54f814141 Mon Sep 17 00:00:00 2001 From: Jas Strong Date: Fri, 19 Dec 2025 10:42:11 -0800 Subject: [PATCH 08/10] [aqi, hm3301, pmsx003] Air Quality Index improvements (#12203) Co-authored-by: jas 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> --- CODEOWNERS | 1 + esphome/components/aqi/__init__.py | 14 ++++++++ .../{hm3301 => aqi}/abstract_aqi_calculator.h | 6 ++-- .../{hm3301 => aqi}/aqi_calculator.h | 11 +++--- .../{hm3301 => aqi}/aqi_calculator_factory.h | 14 ++++---- .../{hm3301 => aqi}/caqi_calculator.h | 6 ++-- esphome/components/hm3301/hm3301.cpp | 2 +- esphome/components/hm3301/hm3301.h | 8 ++--- esphome/components/hm3301/sensor.py | 10 ++---- esphome/components/pmsx003/pmsx003.cpp | 34 ++++++++++++++++--- esphome/components/pmsx003/pmsx003.h | 12 +++++++ esphome/components/pmsx003/sensor.py | 26 ++++++++++++++ tests/components/pmsx003/common.yaml | 3 ++ 13 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 esphome/components/aqi/__init__.py rename esphome/components/{hm3301 => aqi}/abstract_aqi_calculator.h (64%) rename esphome/components/{hm3301 => aqi}/aqi_calculator.h (94%) rename esphome/components/{hm3301 => aqi}/aqi_calculator_factory.h (55%) rename esphome/components/{hm3301 => aqi}/caqi_calculator.h (94%) diff --git a/CODEOWNERS b/CODEOWNERS index fc27253d23..21be3e36d1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/esphome/components/aqi/__init__.py b/esphome/components/aqi/__init__.py new file mode 100644 index 0000000000..4b979ab406 --- /dev/null +++ b/esphome/components/aqi/__init__.py @@ -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, +} diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/aqi/abstract_aqi_calculator.h similarity index 64% rename from esphome/components/hm3301/abstract_aqi_calculator.h rename to esphome/components/aqi/abstract_aqi_calculator.h index 038828e9de..7836c76cdc 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/aqi/abstract_aqi_calculator.h @@ -2,13 +2,11 @@ #include -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 diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h similarity index 94% rename from esphome/components/hm3301/aqi_calculator.h rename to esphome/components/aqi/aqi_calculator.h index aa01060d2c..959d6a2438 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -1,10 +1,11 @@ #pragma once + #include #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 diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/aqi/aqi_calculator_factory.h similarity index 55% rename from esphome/components/hm3301/aqi_calculator_factory.h rename to esphome/components/aqi/aqi_calculator_factory.h index 55608b6e51..db7eaab1bb 100644 --- a/esphome/components/hm3301/aqi_calculator_factory.h +++ b/esphome/components/aqi/aqi_calculator_factory.h @@ -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 diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h similarity index 94% rename from esphome/components/hm3301/caqi_calculator.h rename to esphome/components/aqi/caqi_calculator.h index 3f338776d8..d493dcdf39 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -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 diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index a19d9dd09f..00fb85397c 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -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); } diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 6779b4e195..e155ed6b4b 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -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); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 5eb1773518..389da97b1e 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -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: diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index eb10d19c91..3bdb5219ed 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -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, " diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index ba607b4487..229972e2e5 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -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 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 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index bebd3a01ee..b2d6744547 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -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])) diff --git a/tests/components/pmsx003/common.yaml b/tests/components/pmsx003/common.yaml index 3c60995804..9dd79723d1 100644 --- a/tests/components/pmsx003/common.yaml +++ b/tests/components/pmsx003/common.yaml @@ -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 From 26c16f4ca25e1d4ac562ad484d4c1a9e7b5fedcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:18:33 -1000 Subject: [PATCH 09/10] Bump voluptuous from 0.15.2 to 0.16.0 (#12573) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62352ce754..5c3d82e219 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cryptography==45.0.1 -voluptuous==0.15.2 +voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 From 59b38d79b40a7956b9db1a42c5ceb644aee7c9b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:18:52 -1000 Subject: [PATCH 10/10] Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 in the docker-actions group (#12574) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index bf7fa0c262..84d79cda17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10194aa599..b41b118504 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to docker hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -178,7 +178,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub'