From e63428ddce7125318503ebfb40462f823b0bfd56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:31:16 +0100 Subject: [PATCH 01/33] [nextion] Keep add_waveform_data() available without a waveform sensor (#19590) --- esphome/components/nextion/nextion.h | 2 -- esphome/components/nextion/nextion_commands.cpp | 2 -- tests/components/nextion/common_waveform_lambda.yaml | 8 ++++++++ .../nextion/test-waveform-lambda.esp32-idf.yaml | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 tests/components/nextion/common_waveform_lambda.yaml create mode 100644 tests/components/nextion/test-waveform-lambda.esp32-idf.yaml diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 6c9c8760f8..320aec048a 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -614,7 +614,6 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: */ void disable_component_touch(const char *component); -#ifdef USE_NEXTION_WAVEFORM /** * Add waveform data to a waveform component * @param component_id The integer component id. @@ -624,7 +623,6 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value); void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value); -#endif // USE_NEXTION_WAVEFORM /** * Display a picture at coordinates. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index a356d54e2f..dd3a97065c 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -217,7 +217,6 @@ void Nextion::set_component_value(const char *component, int32_t value) { this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value); } -#ifdef USE_NEXTION_WAVEFORM void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, value); @@ -227,7 +226,6 @@ void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, value); } -#endif // USE_NEXTION_WAVEFORM void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x); diff --git a/tests/components/nextion/common_waveform_lambda.yaml b/tests/components/nextion/common_waveform_lambda.yaml new file mode 100644 index 0000000000..70b1c62d87 --- /dev/null +++ b/tests/components/nextion/common_waveform_lambda.yaml @@ -0,0 +1,8 @@ +# Waveform commands from a lambda with no waveform sensor configured, so +# USE_NEXTION_WAVEFORM is not defined. +display: + - platform: nextion + id: main_lcd + lambda: |- + id(main_lcd).open_waveform_channel(37, 0, 10); + id(main_lcd).add_waveform_data(37, 0, 42); diff --git a/tests/components/nextion/test-waveform-lambda.esp32-idf.yaml b/tests/components/nextion/test-waveform-lambda.esp32-idf.yaml new file mode 100644 index 0000000000..53535ab6c4 --- /dev/null +++ b/tests/components/nextion/test-waveform-lambda.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + waveform_lambda: !include common_waveform_lambda.yaml From 9df7e825d30bb119afe9d9cb59cfff02928c9d57 Mon Sep 17 00:00:00 2001 From: Janne Snabb Date: Thu, 24 Sep 2026 19:59:03 +0300 Subject: [PATCH 02/33] [core] Keep static task TCB in internal RAM (#19426) --- esphome/core/static_task.cpp | 31 +++++++++++++++++-------------- esphome/core/static_task.h | 25 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/esphome/core/static_task.cpp b/esphome/core/static_task.cpp index 4301108315..87629c2507 100644 --- a/esphome/core/static_task.cpp +++ b/esphome/core/static_task.cpp @@ -14,11 +14,8 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size } if (this->stack_buffer_ != nullptr && (stack_size > this->stack_size_ || use_psram != this->use_psram_)) { - // Existing buffer is too small or wrong memory type; deallocate to reallocate below - RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL - : RAMAllocator::ALLOC_INTERNAL); - allocator.deallocate(this->stack_buffer_, this->stack_size_); - this->stack_buffer_ = nullptr; + // Existing buffer is too small or wrong memory type; free it to reallocate below + this->stack_buffer_.reset(); } if (this->stack_buffer_ == nullptr) { @@ -26,13 +23,23 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size this->use_psram_ = use_psram; RAMAllocator allocator(use_psram ? RAMAllocator::ALLOC_EXTERNAL : RAMAllocator::ALLOC_INTERNAL); - this->stack_buffer_ = allocator.allocate(stack_size); + this->stack_buffer_ = allocator.make_unique_array_for_overwrite(stack_size); } if (this->stack_buffer_ == nullptr) { return false; } - this->handle_ = xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_, &this->tcb_); + if (this->tcb_ == nullptr) { + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + this->tcb_ = allocator.make_unique(); + } + if (this->tcb_ == nullptr) { + this->deallocate(); + return false; + } + + this->handle_ = + xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_.get(), this->tcb_.get()); if (this->handle_ == nullptr) { this->deallocate(); return false; @@ -65,13 +72,9 @@ bool StaticTask::deallocate() { if (!this->destroy()) { return false; } - if (this->stack_buffer_ != nullptr) { - RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL - : RAMAllocator::ALLOC_INTERNAL); - allocator.deallocate(this->stack_buffer_, this->stack_size_); - this->stack_buffer_ = nullptr; - this->stack_size_ = 0; - } + this->stack_buffer_.reset(); + this->stack_size_ = 0; + this->tcb_.reset(); return true; } diff --git a/esphome/core/static_task.h b/esphome/core/static_task.h index e2996abeda..10c55f70c6 100644 --- a/esphome/core/static_task.h +++ b/esphome/core/static_task.h @@ -5,49 +5,58 @@ #include #include +#include "esphome/core/helpers.h" + #include namespace esphome { /** Helper for FreeRTOS static task management. - * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. + * Stores the TCB in internal RAM and manages the stack buffer with create/destroy methods. * Call destroy() and deallocate() from another task: a task cannot free the stack it is still running on. */ class StaticTask { public: + StaticTask() = default; + // Non-copyable, non-movable — the task handle and its buffers are not transferable + StaticTask(const StaticTask &) = delete; + StaticTask &operator=(const StaticTask &) = delete; + StaticTask(StaticTask &&) = delete; + StaticTask &operator=(StaticTask &&) = delete; + /// @brief Check if the task has been created and not yet destroyed. bool is_created() const { return this->handle_ != nullptr; } /// @brief Get the FreeRTOS task handle. TaskHandle_t get_handle() const { return this->handle_; } - /// @brief Allocate stack and create task. + /// @brief Allocate the TCB (always internal RAM) and the stack, then create the task. /// @param fn Task function /// @param name Task name (for debug) /// @param stack_size Stack size in bytes (StackType_t is a byte on ESP-IDF) /// @param param Parameter passed to task function /// @param priority FreeRTOS task priority /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM - /// @return true on success + /// @return true on success; false if either allocation fails or the task cannot be created bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool use_psram); - /// @brief Delete the task, keeping the stack buffer allocated for reuse by a subsequent create() call. + /// @brief Delete the task, keeping the TCB and stack buffer allocated for reuse by a subsequent create() call. /// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is /// suspended here so that it cannot be scheduled again, and it is given no chance to clean up. /// @return true if the task was deleted; false if it is still running on another core, in which case the /// caller should try again later. bool destroy(); - /// @brief Delete the task (if created) and free the stack buffer. - /// @return true if the stack buffer was freed; false if the task is still running on another core, in + /// @brief Delete the task (if created) and free the TCB and stack buffer. + /// @return true if the buffers were freed; false if the task is still running on another core, in /// which case the caller should try again later. bool deallocate(); protected: TaskHandle_t handle_{nullptr}; - StaticTask_t tcb_; - StackType_t *stack_buffer_{nullptr}; + RAMUniquePtr tcb_; + RAMUniquePtr stack_buffer_; uint32_t stack_size_{0}; bool use_psram_{false}; }; From 211fd3ce542f10705480f579cbc9e954ee3e9fd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Sep 2026 18:24:10 +0100 Subject: [PATCH 03/33] [core] Dispatch controller updates through generated direct calls (#19588) --- esphome/codegen.py | 1 - esphome/components/api/__init__.py | 5 +- esphome/components/api/api_server.cpp | 3 - esphome/components/api/api_server.h | 47 ++++++++------- esphome/components/api/subscribe_state.h | 1 - .../prometheus/prometheus_handler.h | 2 +- esphome/components/web_server/__init__.py | 5 +- esphome/components/web_server/web_server.cpp | 2 - esphome/components/web_server/web_server.h | 49 ++++++++------- esphome/core/__init__.py | 11 ++-- esphome/core/component_iterator.h | 2 +- esphome/core/config.py | 26 +++++--- esphome/core/controller.h | 19 ------ esphome/core/controller_dispatch.h | 50 ++++++++++++++++ esphome/core/controller_registry.cpp | 11 ---- esphome/core/controller_registry.h | 59 ++----------------- esphome/core/defines.h | 1 - esphome/cpp_types.py | 1 - esphome/writer.py | 23 ++++---- script/helpers.py | 6 +- tests/components/api/__init__.py | 2 - tests/unit_tests/core/test_config.py | 32 +++++++++- tests/unit_tests/test_codegen.py | 1 - 23 files changed, 178 insertions(+), 181 deletions(-) delete mode 100644 esphome/core/controller.h create mode 100644 esphome/core/controller_dispatch.h delete mode 100644 esphome/core/controller_registry.cpp diff --git a/esphome/codegen.py b/esphome/codegen.py index 5debb52b4e..3dfec65f23 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401 Application, Component, ComponentPtr, - Controller, EntityBase, EntityCategory, ESPTime, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 854bceecfa..2291dc54f8 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -81,7 +81,7 @@ def AUTO_LOAD(config: ConfigType) -> list[str]: api_ns = cg.esphome_ns.namespace("api") -APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) +APIServer = api_ns.class_("APIServer", cg.Component) HomeAssistantServiceCallAction = api_ns.class_( "HomeAssistantServiceCallAction", automation.Action ) @@ -462,8 +462,7 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Track controller registration for StaticVector sizing - CORE.register_controller() + CORE.register_controller(var) # Request a log listener slot for API log streaming request_log_listener() diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 310cdcf67b..2a89d31a74 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -5,7 +5,6 @@ #include "api_connection.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -61,8 +60,6 @@ void APIServer::socket_failed_(const LogString *msg) { } void APIServer::setup() { - ControllerRegistry::register_controller(this); - #ifdef USE_API_NOISE // Always reserve the slot: flash preferences are positional on esp8266, so // a yaml key build must keep the layout of a runtime key build diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index b8a71c93e7..49cf77ca5a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -14,7 +14,7 @@ #include "esphome/components/socket/socket.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/controller.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/log.h" #include "esphome/core/string_ref.h" #ifdef USE_PROVISIONING @@ -49,8 +49,7 @@ struct SavedNoisePsk { bool load_saved_noise_psk(noise::psk_t &out); #endif -class APIServer final : public Component, - public Controller +class APIServer final : public Component #ifdef USE_CAMERA , public camera::CameraListener @@ -94,58 +93,58 @@ class APIServer final : public Component, void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj); #endif #ifdef USE_COVER - void on_cover_update(cover::Cover *obj) override; + void on_cover_update(cover::Cover *obj); #endif #ifdef USE_FAN - void on_fan_update(fan::Fan *obj) override; + void on_fan_update(fan::Fan *obj); #endif #ifdef USE_LIGHT - void on_light_update(light::LightState *obj) override; + void on_light_update(light::LightState *obj); #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj) override; + void on_sensor_update(sensor::Sensor *obj); #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj) override; + void on_switch_update(switch_::Switch *obj); #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj) override; + void on_text_sensor_update(text_sensor::TextSensor *obj); #endif #ifdef USE_CLIMATE - void on_climate_update(climate::Climate *obj) override; + void on_climate_update(climate::Climate *obj); #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj) override; + void on_number_update(number::Number *obj); #endif #ifdef USE_DATETIME_DATE - void on_date_update(datetime::DateEntity *obj) override; + void on_date_update(datetime::DateEntity *obj); #endif #ifdef USE_DATETIME_TIME - void on_time_update(datetime::TimeEntity *obj) override; + void on_time_update(datetime::TimeEntity *obj); #endif #ifdef USE_DATETIME_DATETIME - void on_datetime_update(datetime::DateTimeEntity *obj) override; + void on_datetime_update(datetime::DateTimeEntity *obj); #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj) override; + void on_text_update(text::Text *obj); #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj) override; + void on_select_update(select::Select *obj); #endif #ifdef USE_LOCK - void on_lock_update(lock::Lock *obj) override; + void on_lock_update(lock::Lock *obj); #endif #ifdef USE_VALVE - void on_valve_update(valve::Valve *obj) override; + void on_valve_update(valve::Valve *obj); #endif #ifdef USE_MEDIA_PLAYER - void on_media_player_update(media_player::MediaPlayer *obj) override; + void on_media_player_update(media_player::MediaPlayer *obj); #endif #ifdef USE_WATER_HEATER - void on_water_heater_update(water_heater::WaterHeater *obj) override; + void on_water_heater_update(water_heater::WaterHeater *obj); #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); @@ -188,13 +187,13 @@ class APIServer final : public Component, #endif #ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); #endif #ifdef USE_EVENT - void on_event(event::Event *obj) override; + void on_event(event::Event *obj); #endif #ifdef USE_UPDATE - void on_update(update::UpdateEntity *obj) override; + void on_update(update::UpdateEntity *obj); #endif #ifdef USE_ZWAVE_PROXY void on_zwave_proxy_request(const ZWaveProxyRequest &msg); diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 6b1ae9651d..36e96e1081 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -4,7 +4,6 @@ #ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -#include "esphome/core/controller.h" namespace esphome::api { class APIConnection; diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5574039e41..9dcbf7e686 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -6,8 +6,8 @@ #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" -#include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/progmem.h" #ifdef USE_CLIMATE #include "esphome/core/log.h" diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2459163786..3ec365cf38 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -62,7 +62,7 @@ DEFAULT_PORT = 80 web_server_ns = cg.esphome_ns.namespace("web_server") -WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) +WebServer = web_server_ns.class_("WebServer", cg.Component) sorting_groups = {} @@ -378,8 +378,7 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) - # Track controller registration for StaticVector sizing - CORE.register_controller() + CORE.register_controller(var) version = config[CONF_VERSION] diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 49943f8a71..4906a2050a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -5,7 +5,6 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -365,7 +364,6 @@ json::SerializationBuffer<> WebServer::get_config_json() { } void WebServer::setup() { - ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3e50eeeec0..3236ccfd3c 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -6,8 +6,8 @@ #include "esphome/components/web_server_base/web_server_base.h" #ifdef USE_WEBSERVER #include "esphome/core/component.h" -#include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/progmem.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList final : public std::list'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -301,7 +301,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj) override; + void on_switch_update(switch_::Switch *obj); /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -319,7 +319,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj); /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -329,7 +329,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_FAN - void on_fan_update(fan::Fan *obj) override; + void on_fan_update(fan::Fan *obj); /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -339,7 +339,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_LIGHT - void on_light_update(light::LightState *obj) override; + void on_light_update(light::LightState *obj); /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -349,7 +349,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj) override; + void on_text_sensor_update(text_sensor::TextSensor *obj); /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -359,7 +359,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_COVER - void on_cover_update(cover::Cover *obj) override; + void on_cover_update(cover::Cover *obj); /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -369,7 +369,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj) override; + void on_number_update(number::Number *obj); /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -378,7 +378,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_DATE - void on_date_update(datetime::DateEntity *obj) override; + void on_date_update(datetime::DateEntity *obj); /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -387,7 +387,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_TIME - void on_time_update(datetime::TimeEntity *obj) override; + void on_time_update(datetime::TimeEntity *obj); /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -396,7 +396,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_DATETIME_DATETIME - void on_datetime_update(datetime::DateTimeEntity *obj) override; + void on_datetime_update(datetime::DateTimeEntity *obj); /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -405,7 +405,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj) override; + void on_text_update(text::Text *obj); /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -414,7 +414,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj) override; + void on_select_update(select::Select *obj); /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -423,7 +423,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_CLIMATE - void on_climate_update(climate::Climate *obj) override; + void on_climate_update(climate::Climate *obj); /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -432,7 +432,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_LOCK - void on_lock_update(lock::Lock *obj) override; + void on_lock_update(lock::Lock *obj); /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -442,7 +442,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_VALVE - void on_valve_update(valve::Valve *obj) override; + void on_valve_update(valve::Valve *obj); /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -452,7 +452,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -462,7 +462,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_WATER_HEATER - void on_water_heater_update(water_heater::WaterHeater *obj) override; + void on_water_heater_update(water_heater::WaterHeater *obj); /// Handle a water_heater request under '/water_heater//'. void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -484,8 +484,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand static json::SerializationBuffer<> radio_frequency_all_json_generator(WebServer *web_server, void *source); #endif +#ifdef USE_MEDIA_PLAYER + // Not exposed over HTTP; the stub only satisfies ControllerContract + void on_media_player_update(media_player::MediaPlayer *) {} +#endif + #ifdef USE_EVENT - void on_event(event::Event *obj) override; + void on_event(event::Event *obj); static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source); @@ -495,7 +500,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #endif #ifdef USE_UPDATE - void on_update(update::UpdateEntity *obj) override; + void on_update(update::UpdateEntity *obj); /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 5fcad90a81..fb75285081 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -52,8 +52,8 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing -KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" +# Key for the controllers (APIServer, WebServer) that receive entity state updates +KEY_CONTROLLER_REGISTRY_CONTROLLERS = "controller_registry_controllers" # CORE.data key for the "is_rp2040 deprecation warning already fired this # run" flag. Mirrors the ``cv.only_on_rp2040`` dedupe pattern; cleared @@ -1209,10 +1209,9 @@ class EsphomeCore: if not self.platform_counts[platform_name]: self.platform_counts[platform_name] = 1 - def register_controller(self) -> None: - """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" - controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) - self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 + def register_controller(self, controller: "MockObj") -> None: + """Register a controller that receives every entity state update.""" + self.data.setdefault(KEY_CONTROLLER_REGISTRY_CONTROLLERS, []).append(controller) @property def cpp_main_section(self): diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index fac09e9e14..3c11d4a880 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/controller.h" +#include "esphome/core/entity_includes.h" #include "esphome/core/helpers.h" #ifdef USE_CAMERA diff --git a/esphome/core/config.py b/esphome/core/config.py index 051a136aab..4d3fb83312 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -46,7 +46,7 @@ from esphome.const import ( ) from esphome.core import ( CORE, - KEY_CONTROLLER_REGISTRY_COUNT, + KEY_CONTROLLER_REGISTRY_CONTROLLERS, CoroPriority, coroutine_with_priority, ) @@ -674,12 +674,22 @@ async def _add_platform_defines() -> None: @coroutine_with_priority(CoroPriority.FINAL) -async def _add_controller_registry_define() -> None: - # Generate StaticVector size for ControllerRegistry - controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) - if controller_count > 0: - cg.add_define("USE_CONTROLLER_REGISTRY") - cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) +async def _add_controller_registry_dispatch() -> None: + # controller_dispatch.h defines ControllerRegistry::notify_*() as direct + # calls on the controllers returned by esphome_controllers(), emitted as + # static auto esphome_controllers() { return std::tuple{a, b}; } + controllers = CORE.data.get(KEY_CONTROLLER_REGISTRY_CONTROLLERS) + if not controllers: + return + cg.add_define("USE_CONTROLLER_REGISTRY") + controllers = cg.ArrayInitializer(*controllers) + cg.add_global(cg.RawStatement("#include ")) + cg.add_global( + cg.RawStatement( + f"static auto esphome_controllers() {{ return std::tuple{controllers}; }}" + ) + ) + cg.add_global(cg.RawStatement('#include "esphome/core/controller_dispatch.h"')) @coroutine_with_priority(CoroPriority.FINAL) @@ -755,7 +765,7 @@ async def to_code(config: ConfigType) -> None: ) CORE.add_job(_add_platform_defines) - CORE.add_job(_add_controller_registry_define) + CORE.add_job(_add_controller_registry_dispatch) CORE.add_job(_add_looping_components) CORE.add_job(_add_automations, config) diff --git a/esphome/core/controller.h b/esphome/core/controller.h deleted file mode 100644 index 09975b465f..0000000000 --- a/esphome/core/controller.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/core/entity_includes.h" - -namespace esphome { - -class Controller { - public: -// Controller virtual methods (generated from entity_types.h) -// NOLINTBEGIN(bugprone-macro-parentheses) -#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback -#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) virtual void on_##callback(type *obj){}; -#include "esphome/core/entity_types.h" -#undef ENTITY_TYPE_ -#undef ENTITY_CONTROLLER_TYPE_ - // NOLINTEND(bugprone-macro-parentheses) -}; - -} // namespace esphome diff --git a/esphome/core/controller_dispatch.h b/esphome/core/controller_dispatch.h new file mode 100644 index 0000000000..fc6a03556d --- /dev/null +++ b/esphome/core/controller_dispatch.h @@ -0,0 +1,50 @@ +#pragma once + +// Included once by the generated main.cpp, after it returns the registered controllers as a tuple: +// +// static auto esphome_controllers() { return std::tuple{api_apiserver_id, web_server_webserver_id}; } +// #include "esphome/core/controller_dispatch.h" +// +// Defines ControllerRegistry::notify_*() as direct calls on those controllers. Excluded from +// esphome.h and the clang-tidy all-headers file, so nothing else includes it. + +#include +#include + +#include "esphome/core/controller_registry.h" + +namespace esphome { + +// NOLINTBEGIN(bugprone-macro-parentheses) + +/// A controller provides a plain on_*_update() member for every entity type in the build. +template +concept ControllerContract = requires(T &controller) { + controller; // keeps the requirement list non-empty when no entity type has a callback +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + controller.on_##callback(static_cast(nullptr)); +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ +}; + +template constexpr bool controllers_satisfy_contract(std::tuple *) { + return (ControllerContract> && ...); +} +static_assert(controllers_satisfy_contract(static_cast(nullptr)), + "A registered controller is missing an on_*_update() callback for an entity type in this build " + "(ControllerContract in esphome/core/controller_dispatch.h)"); + +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + void ControllerRegistry::notify_##callback(type *obj) { \ + std::apply([obj](auto *...controller) { (controller->on_##callback(obj), ...); }, esphome_controllers()); \ + } +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + +// NOLINTEND(bugprone-macro-parentheses) + +} // namespace esphome diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp deleted file mode 100644 index 907e0f923d..0000000000 --- a/esphome/core/controller_registry.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "esphome/core/controller_registry.h" - -#ifdef USE_CONTROLLER_REGISTRY - -namespace esphome { - -StaticVector ControllerRegistry::controllers; - -} // namespace esphome - -#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index c6113116ff..8b1b6c7f9d 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -5,37 +5,18 @@ #ifdef USE_CONTROLLER_REGISTRY #include "esphome/core/entity_includes.h" -#include "esphome/core/helpers.h" namespace esphome { -class Controller; - -/** Global registry for Controllers to receive entity state updates. +/** Fan-out of entity state updates to the controllers (APIServer, WebServer). * - * This singleton registry allows Controllers (APIServer, WebServer) to receive - * entity state change notifications without storing per-entity callbacks. - * - * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), - * entities call ControllerRegistry::notify_*_update() which iterates the small list - * of registered controllers (typically 2: API and WebServer). - * - * Each notify method directly iterates controllers and calls the virtual method, - * avoiding function pointer indirection for minimal dispatch overhead. - * - * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) - * Typical config (25 entities): ~780 bytes saved - * Large config (80 entities): ~2,540 bytes saved + * Entities call ControllerRegistry::notify_*_update() instead of holding + * per-entity controller callbacks. The functions are only declared here; + * controller_dispatch.h, included by the generated main.cpp, defines them as + * direct calls on the controllers registered through CORE.register_controller(). */ class ControllerRegistry { public: - /** Register a controller to receive entity state updates. - * - * Controllers should call this in their setup() method. - * Typically only APIServer and WebServer register. - */ - static void register_controller(Controller *controller) { controllers.push_back(controller); } - // Notify method declarations (generated from entity_types.h) // NOLINTBEGIN(bugprone-macro-parentheses) #define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback @@ -45,38 +26,8 @@ class ControllerRegistry { #undef ENTITY_TYPE_ #undef ENTITY_CONTROLLER_TYPE_ // NOLINTEND(bugprone-macro-parentheses) - - protected: - static StaticVector controllers; }; } // namespace esphome -// Include controller.h AFTER the class definition so notify methods can be -// defined inline. This is safe because controller_registry.h is only ever -// included from .cpp files, never from other headers. -#include "esphome/core/controller.h" - -namespace esphome { - -// Inline notify methods — each is a tiny loop over 1-2 controllers. -// Defining them here (rather than in controller_registry.cpp) allows the -// compiler to inline them into the single call site in each entity's -// notify_frontend_(), eliminating an unnecessary function-call frame. - -// NOLINTBEGIN(bugprone-macro-parentheses) -#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback -#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ - inline void ControllerRegistry::notify_##callback(type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##callback(obj); \ - } \ - } -#include "esphome/core/entity_types.h" -#undef ENTITY_TYPE_ -#undef ENTITY_CONTROLLER_TYPE_ -// NOLINTEND(bugprone-macro-parentheses) - -} // namespace esphome - #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7bc08ea577..e1ae1d5fb6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -629,7 +629,6 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis -#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_AREA_COUNT 10 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 45d6559b3f..04892f5fea 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -37,7 +37,6 @@ optional = global_ns.namespace("std").class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") -Controller = esphome_ns.class_("Controller") GPIOPin = esphome_ns.class_("GPIOPin") InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin) gpio_ns = esphome_ns.namespace("gpio") diff --git a/esphome/writer.py b/esphome/writer.py index 0b9e7669ef..9ab5d644c5 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -211,6 +211,16 @@ VERSION_H_TARGET = "esphome/core/version.h" BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp" ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h" +# Headers that must not be included bare from esphome.h or the clang-tidy +# all-headers file: X-macro files, headers main.cpp includes itself, and +# deprecated headers that only resolve when their new component is loaded. +ESPHOME_H_EXCLUDE = { + Path(ENTITY_TYPES_H_TARGET), + # main.cpp includes it after defining esphome_controllers() + Path("esphome/core/controller_dispatch.h"), + # moved to components/ring_buffer/, removed in 2026.11.0 + Path("esphome/core/ring_buffer.h"), +} ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -236,20 +246,9 @@ def copy_src_tree(): source_files_l.sort() # Build #include list for esphome.h - # X-macro files are included multiple times with different macro definitions - # and must not be included bare in esphome.h - # Deprecated headers that re-export from a relocated component must not be - # auto-included, since their #include of the new path only resolves when the - # new component is loaded by a consumer. - esphome_h_exclude = { - Path(ENTITY_TYPES_H_TARGET), - Path( - "esphome/core/ring_buffer.h" - ), # moved to components/ring_buffer/, removed in 2026.11.0 - } include_l = [] for target, _ in source_files_l: - if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude: + if target.suffix in HEADER_FILE_EXTENSIONS and target not in ESPHOME_H_EXCLUDE: include_l.append(f'#include "{target}"') include_l.append("") include_s = "\n".join(include_l) diff --git a/script/helpers.py b/script/helpers.py index a8a237118f..fb2c964bdd 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -429,11 +429,9 @@ def build_all_include(header_files: list[str] | None = None) -> None: if line ] - from esphome.writer import ENTITY_TYPES_H_TARGET + from esphome.writer import ESPHOME_H_EXCLUDE - # X-macro files are included multiple times with different macro definitions - # and must not be included bare in the all-include header - exclude = {ENTITY_TYPES_H_TARGET} + exclude = {path.as_posix() for path in ESPHOME_H_EXCLUDE} headers = [f'#include "{h}"' for h in header_files if h not in exclude] headers.sort() headers.append("") diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py index 2aa558726c..a5e3731c75 100644 --- a/tests/components/api/__init__.py +++ b/tests/components/api/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.core import CORE from tests.testing_helpers import ComponentManifestOverride @@ -12,6 +11,5 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: cg.add_define("API_MAX_SEND_QUEUE", 8) cg.add_define("MAX_API_CONNECTIONS", 1) cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") - CORE.register_controller() # api_server registers with the controller registry manifest.to_code = to_code_testing diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 07cff003cd..0db7039668 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config_validation as cv, core +import esphome.codegen as cg from esphome.components.safe_mode import to_code as safe_mode_to_code from esphome.const import ( CONF_AREA, @@ -23,7 +24,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, Toolchain, ) -from esphome.core import CORE, config +from esphome.core import CORE, KEY_CONTROLLER_REGISTRY_CONTROLLERS, config from esphome.core.config import ( Area, make_app_name_cpp, @@ -455,6 +456,35 @@ async def test_add_looping_components_with_entries() -> None: assert "(1 * HasLoopOverride::value)" in text +@pytest.mark.asyncio +async def test_add_controller_registry_dispatch_without_controllers() -> None: + """Nothing is emitted when no controller registered.""" + CORE.data.pop(KEY_CONTROLLER_REGISTRY_CONTROLLERS, None) + + await config._add_controller_registry_dispatch() + + assert "USE_CONTROLLER_REGISTRY" not in {d.name for d in CORE.defines} + assert not [s for s in CORE.global_statements if "controller" in str(s)] + + +@pytest.mark.asyncio +async def test_add_controller_registry_dispatch_with_controllers() -> None: + """Registered controllers become one tuple plus the dispatch include.""" + CORE.register_controller(cg.MockObj("api_apiserver_id")) + CORE.register_controller(cg.MockObj("web_server_webserver_id")) + + await config._add_controller_registry_dispatch() + + assert "USE_CONTROLLER_REGISTRY" in {d.name for d in CORE.defines} + statements = [str(s) for s in CORE.global_statements] + assert "#include " in statements + assert ( + "static auto esphome_controllers() { return std::tuple{api_apiserver_id, web_server_webserver_id}; }" + in statements + ) + assert '#include "esphome/core/controller_dispatch.h"' in statements + + def test_valid_include_with_angle_brackets() -> None: """Test valid_include accepts angle bracket includes.""" assert valid_include("") == "" diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 8d01fef7c2..c5c971b22d 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -70,7 +70,6 @@ from esphome.cpp_generator import _extract_component_ns "arduino_json_ns", "JsonObject", "JsonObjectConst", - "Controller", "GPIOPin", ), ) From a9111a89933d46246da9afd608a5d6860c9d9ae3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Sep 2026 18:36:45 +0100 Subject: [PATCH 04/33] [ota] Treat a client that leaves after the handshake as a clean close (#19401) --- .../components/esphome/ota/ota_esphome.cpp | 31 ++++++++++++++- esphome/components/esphome/ota/ota_esphome.h | 2 + tests/integration/test_host_ota.py | 39 ++++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index d353d01d20..6248474624 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -467,6 +467,8 @@ void ESPHomeOTAComponent::handle_data_() { if (this->extended_proto_()) { // Read ota type, 1 byte if (!this->data_readall_(buf, 1)) { + if (this->client_left_before_start_()) + return; this->log_read_error_(LOG_STR("OTA type")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -476,6 +478,9 @@ void ESPHomeOTAComponent::handle_data_() { // Read size, 4 bytes MSB first if (!this->data_readall_(buf, 4)) { + // The first request byte is the type on the extended protocol; a close after it was a cut-off request + if (!this->extended_proto_() && this->client_left_before_start_()) + return; this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -542,6 +547,8 @@ void ESPHomeOTAComponent::handle_data_() { // there is no would-block retry here and failures are already logged. read = this->noise_read_data_(buf, requested); if (read <= 0) { + if (this->remote_closed_) + this->log_remote_closed_(LOG_STR("data")); error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -666,7 +673,11 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { return false; } } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed"); + // A partial message is a cut-off request, not a clean close; the caller reports the clean one + this->remote_closed_ = at == 0; + if (at > 0) { + ESP_LOGW(TAG, "Remote closed after %u of %zu bytes", (unsigned) at, len); + } return false; } else { at += read; @@ -712,7 +723,22 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); } -void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } +bool ESPHomeOTAComponent::client_left_before_start_() { + // Key probes and scanners hang up right after the handshake; nothing started, so no error status or callback + if (!this->remote_closed_) + return false; + ESP_LOGD(TAG, "Client left after the handshake"); + this->cleanup_connection_(); + return true; +} + +void ESPHomeOTAComponent::log_read_error_(const LogString *what) { + if (this->remote_closed_) { + this->log_remote_closed_(what); + return; + } + ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); +} void ESPHomeOTAComponent::log_start_(const LogString *phase) { char peername[socket::SOCKADDR_STR_LEN]; @@ -793,6 +819,7 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->handshake_buf_pos_ = 0; this->ota_state_ = OTAState::IDLE; this->ota_features_ = 0; + this->remote_closed_ = false; this->backend_ = nullptr; #ifdef USE_OTA_PASSWORD this->cleanup_auth_(); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 92ba094c8d..6f04b78da5 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -134,6 +134,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); + bool client_left_before_start_(); void log_start_(const LogString *phase); void log_remote_closed_(const LogString *during); void cleanup_connection_(); @@ -186,6 +187,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { OTAState ota_state_{OTAState::IDLE}; uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; + bool remote_closed_{false}; // the peer hung up cleanly during a blocking read #ifdef USE_OTA_PASSWORD uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use diff --git a/tests/integration/test_host_ota.py b/tests/integration/test_host_ota.py index 88eb0168e3..56a685eac3 100644 --- a/tests/integration/test_host_ota.py +++ b/tests/integration/test_host_ota.py @@ -167,6 +167,33 @@ class _Device: assert self.proc.returncode is None, "process died on rejected OTA" +def _handshake_then_close(port: int, noise_psk: str) -> None: + """Negotiate and complete the Noise handshake like a key probe, then + hang up without sending an OTA type.""" + with socket.create_connection((LOCALHOST, port), timeout=5.0) as sock: + espota2.send_check(sock, espota2.MAGIC_BYTES, "magic bytes") + _, version = espota2.receive_exactly(sock, 2, "version", espota2.RESPONSE_OK) + features_to_send = ( + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + | espota2.CLIENT_FEATURE_SUPPORTS_NOISE + ) + espota2.send_check(sock, features_to_send, "features") + espota2.receive_exactly(sock, 1, "features", espota2.RESPONSE_FEATURE_FLAGS) + (features,) = espota2.receive_exactly(sock, 1, "feature flags", None) + assert features & espota2.SERVER_FEATURE_SUPPORTS_NOISE + prologue = ( + espota2.NOISE_PROLOGUE_INIT + + bytes(espota2.MAGIC_BYTES) + + bytes([espota2.RESPONSE_OK, version, features_to_send]) + + bytes([espota2.RESPONSE_FEATURE_FLAGS, features]) + ) + noise = espota2.NoiseSocketWrapper(sock, noise_psk, prologue) + noise.do_handshake() + espota2.receive_exactly(noise, 1, "auth", espota2.RESPONSE_AUTH_OK) + + async def _provision_key( dev: _Device, api_client_connected: APIClientConnectedFactory ) -> None: @@ -221,16 +248,24 @@ async def test_host_ota_encrypted( compile_esphome: CompileFunction, reserved_tcp_port: tuple[int, socket.socket], ) -> None: - """Encrypted self-OTA succeeds; a plaintext upload to the same device fails.""" + """A client that leaves right after the handshake, as a key probe does, + is a clean close, not an OTA error; a plaintext upload is refused; an + encrypted self-OTA succeeds.""" pytest.importorskip("aioesphomeapi.noise") dev = _Device( *await _build( yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port ) ) - async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, _lines): + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, lines): dev.proc = proc await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await asyncio.get_running_loop().run_in_executor( + None, _handshake_then_close, dev.ota_port, API_KEY + ) + # The error path logs its warning instead of this line, never after it + await _wait_for_line(lines, "Client left after the handshake") + assert not [line for line in lines if "[W][esphome.ota" in line] await dev.refused_ota( None, None, "plaintext upload to an encrypted device must fail" ) From 16757a84e682899c4e08f9a4b359adc25187d030 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 24 Sep 2026 18:57:16 +0100 Subject: [PATCH 05/33] [nrf52] add device power manager support to deep sleep component (#19494) --- esphome/components/deep_sleep/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index dc03708645..d5d7077583 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -422,8 +422,12 @@ async def to_code(config: ConfigType) -> None: if CONF_TOUCH_WAKEUP in config: cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) - if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations: - zephyr_add_prj_conf("POWEROFF", True) + if CORE.using_zephyr: + # Devices are suspended when CPU is entering a low power state + # https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management + zephyr_add_prj_conf("PM_DEVICE", True) + if "zigbee" not in CORE.loaded_integrations: + zephyr_add_prj_conf("POWEROFF", True) cg.add_define("USE_DEEP_SLEEP") From 057bf88e4af52287741c3d0afb21074d749ae1f9 Mon Sep 17 00:00:00 2001 From: oxynatOr <98734567+oxynatOr@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:06:07 +0200 Subject: [PATCH 06/33] [mcp3204] Read a conversion in one SPI transaction (#19423) Co-authored-by: Claude Opus 5 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/mcp3204/mcp3204.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 33abbe847a..ef8b568453 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -18,19 +18,18 @@ void MCP3204::dump_config() { } float MCP3204::read_data(uint8_t pin, bool differential) { - uint8_t command, b0, b1; - - command = (1 << 6) | // start bit - ((differential ? 0 : 1) << 5) | // single or differential bit - ((pin & 0x07) << 2); // pin + const uint8_t command = (1 << 6) | // start bit + ((differential ? 0 : 1) << 5) | // single or differential bit + ((pin & 0x07) << 2); // pin + // One full-duplex transaction: command out, 12-bit result back in bytes 1 and 2. + // Word aligned so ESP-IDF DMA uses the buffer in place; only ESP32-P4 also checks the length and bounces. + alignas(4) uint8_t buffer[3] = {command, 0x00, 0x00}; this->enable(); - this->transfer_byte(command); - b0 = this->transfer_byte(0x00); - b1 = this->transfer_byte(0x00); + this->transfer_array(buffer, sizeof(buffer)); this->disable(); - uint16_t digital_value = encode_uint16(b0, b1) >> 4; + uint16_t digital_value = encode_uint16(buffer[1], buffer[2]) >> 4; return float(digital_value) / 4096.000f * this->reference_voltage_; // in V } From f70880ce9620443d3b70a8ffe7b51642df1aa29f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:53:09 -0400 Subject: [PATCH 07/33] Bump astral-sh/setup-uv from 10.1.0 to 10.2.0 (#19594) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/sync-device-classes.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index c4c1ab072f..d0bd1de9ef 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -29,7 +29,7 @@ jobs: - name: Set up uv # ``--system`` (below) installs into the setup-python interpreter; # no venv is created or restored by this workflow. - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pull-request-only workflow: a save could never be shared and diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08cd852f02..2343f8c5ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: # detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs # that ``. venv/bin/activate`` see an identical layout. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can @@ -413,7 +413,7 @@ jobs: - name: Set up uv # Only needed on cache miss to populate the venv. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can @@ -1274,7 +1274,7 @@ jobs: # install step (order-of-magnitude faster on cold boots, # with its own wheel cache). actions/setup-python still # provides the interpreter. - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 84d5e229d9..a0725374f9 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -47,7 +47,7 @@ jobs: # setup-python interpreter so subsequent ``prek`` / # ``script/run-in-env.py`` steps find the deps without a # ``uv run`` prefix. - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pin uv version so the action does not have to fetch the From 4ccc5fa37e5e0755acf5babef7642aabcfa20594 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:53:25 -0400 Subject: [PATCH 08/33] Bump astral-sh/setup-uv from 10.1.0 to 10.2.0 in /.github/actions/restore-python (#19593) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index fa42372ac8..affe8fc2d6 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -32,7 +32,7 @@ runs: # detects the activated venv via ``VIRTUAL_ENV`` so the venv layout # downstream jobs rely on is preserved. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can From 73ca04579cf54d1eb6192c39cae9949c24b6f493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:53:46 -0400 Subject: [PATCH 09/33] Update pyparsing requirement from >=3.3.2 to >=3.3.3 (#19592) Signed-off-by: dependabot[bot] 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 c810755817..f678a9e5e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ ninja==1.13.2 # native esp8266 arduino toolchain build driver filelock==4.0.1 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this -pyparsing >= 3.3.2 +pyparsing >= 3.3.3 # For autocompletion argcomplete>=3.7.2 From 3ef0acd4b21561933569267704b103354ecf6197 Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Thu, 24 Sep 2026 14:54:58 -0700 Subject: [PATCH 10/33] [sgp30][sgp4x] Migrate CONF_HUMIDITY_SOURCE to const.py (#19436) --- esphome/components/const/__init__.py | 1 + esphome/components/sgp30/sensor.py | 2 +- esphome/components/sgp4x/sensor.py | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 31b0a0c097..b8bb9db8bf 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -23,6 +23,7 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection" CONF_ENABLED = "enabled" CONF_GYROSCOPE_ODR = "gyroscope_odr" CONF_GYROSCOPE_RANGE = "gyroscope_range" +CONF_HUMIDITY_SOURCE = "humidity_source" CONF_IAQ = "iaq" CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_IS_WRGB = "is_wrgb" diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index beb549ce70..75244abee8 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor +from esphome.components.const import CONF_HUMIDITY_SOURCE import esphome.config_validation as cv from esphome.const import ( CONF_BASELINE, @@ -31,7 +32,6 @@ SGP30Component = sgp30_ns.class_( CONF_ECO2_BASELINE = "eco2_baseline" CONF_TVOC_BASELINE = "tvoc_baseline" CONF_UPTIME = "uptime" -CONF_HUMIDITY_SOURCE = "humidity_source" CONFIG_SCHEMA = ( diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 1a88d190e4..bf8fbe1a97 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -1,6 +1,10 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor -from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX +from esphome.components.const import ( + CONF_HUMIDITY_SOURCE, + CONF_NOX_INDEX, + CONF_VOC_INDEX, +) import esphome.config_validation as cv from esphome.const import ( CONF_ALGORITHM_TUNING, @@ -33,8 +37,6 @@ SGP4xComponent = sgp4x_ns.class_( sensirion_common.SensirionI2CDevice, ) -CONF_HUMIDITY_SOURCE = "humidity_source" - def validate_sensors(config: ConfigType) -> ConfigType: if CONF_VOC_INDEX not in config and CONF_NOX_INDEX not in config: From 253793c4df4bf96c22f8161a18bf824f073ed5d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Sep 2026 23:56:28 +0100 Subject: [PATCH 11/33] [web_server] Mask the value of a password text entity, not only its state (#19385) --- esphome/components/web_server/web_server.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4906a2050a..6e48e2700f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1413,8 +1413,11 @@ json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::st json::JsonBuilder builder; JsonObject root = builder.root(); - const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str(); - set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config); + // A password entity shows the mask and prefills the input with nothing, so the secret never + // reaches the JSON and the mask cannot be written back as the value + const bool password = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD; + set_json_icon_state_value(root, obj, "text", password ? "********" : value.c_str(), password ? "" : value.c_str(), + start_config); root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); From 4a2f17bc8ba30bad7143c29c086425af5dd79057 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 00:24:34 +0100 Subject: [PATCH 12/33] [noise] Run the ESP8266 at 160 MHz during the handshake (#19233) --- esphome/components/noise/noise_handshake.cpp | 2 + .../uart/uart_component_esp8266.cpp | 49 ++++++++++++------- .../components/uart/uart_component_esp8266.h | 6 +-- esphome/core/helpers.h | 26 ++++++++++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/esphome/components/noise/noise_handshake.cpp b/esphome/components/noise/noise_handshake.cpp index cc7fa603c4..64846dcb67 100644 --- a/esphome/components/noise/noise_handshake.cpp +++ b/esphome/components/noise/noise_handshake.cpp @@ -95,6 +95,7 @@ NoiseResponderHandshake::Action NoiseResponderHandshake::action() const { } int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) { + CpuFrequencyBoost boost; NoiseBuffer mbuf; noise_buffer_init(mbuf); noise_buffer_set_input(mbuf, data, len); @@ -103,6 +104,7 @@ int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) { int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t &out_len) { out_len = 0; + CpuFrequencyBoost boost; NoiseBuffer mbuf; noise_buffer_init(mbuf); noise_buffer_set_output(mbuf, out, capacity); diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 2f8b4dbd11..1d492fce47 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP8266 #include "uart_component_esp8266.h" +#include #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -251,23 +252,35 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); } } +// A byte can arrive while a CpuFrequencyBoost has an 80 MHz build at 160 MHz; the clock select bit doubles +// the bit time then. The whole byte is read inside the ISR, so the clock cannot change partway through. +__attribute__((always_inline)) static inline uint32_t rx_bit_time(uint32_t bit_time) { +#if F_CPU != 160000000L + // NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- CPU2X is MMIO at a fixed address + return bit_time << (CPU2X & 1); +#else + return bit_time; +#endif +} + void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { - uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; + const uint32_t bit_time = rx_bit_time(arg->bit_time_); + uint32_t wait = bit_time + bit_time / 3 - 500; const uint32_t start = arch_get_cpu_cycle_count(); uint8_t rec = 0; // Manually unroll the loop for (int i = 0; i < arg->data_bits_; i++) - rec |= arg->read_bit_(&wait, start) << i; + rec |= arg->read_bit_(&wait, start, bit_time) << i; /* If parity is enabled, just read it and ignore it. */ /* TODO: Should we check parity? Or is it too slow for nothing added..*/ if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD) - arg->read_bit_(&wait, start); + arg->read_bit_(&wait, start, bit_time); // Stop bit - arg->wait_(&wait, start); + arg->wait_(&wait, start, bit_time); if (arg->stop_bits_ == 2) - arg->wait_(&wait, start); + arg->wait_(&wait, start, bit_time); arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; @@ -296,37 +309,39 @@ void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { } { + // Transmit runs from the main loop and never overlaps a CpuFrequencyBoost InterruptLock lock; - uint32_t wait = this->bit_time_; + const uint32_t bit_time = this->bit_time_; + uint32_t wait = bit_time; const uint32_t start = arch_get_cpu_cycle_count(); // Start bit - this->write_bit_(false, &wait, start); + this->write_bit_(false, &wait, start, bit_time); for (int i = 0; i < this->data_bits_; i++) { bool bit = data & (1 << i); - this->write_bit_(bit, &wait, start); + this->write_bit_(bit, &wait, start, bit_time); if (need_parity_bit) parity_bit ^= bit; } if (need_parity_bit) - this->write_bit_(parity_bit, &wait, start); + this->write_bit_(parity_bit, &wait, start, bit_time); // Stop bit - this->write_bit_(true, &wait, start); + this->write_bit_(true, &wait, start, bit_time); if (this->stop_bits_ == 2) - this->wait_(&wait, start); + this->wait_(&wait, start, bit_time); } } -void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { +void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) { while (arch_get_cpu_cycle_count() - start < *wait) ; - *wait += this->bit_time_; + *wait += bit_time; } -bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { - this->wait_(wait, start); +bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) { + this->wait_(wait, start, bit_time); return this->rx_pin_.digital_read(); } -void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { +void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time) { this->tx_pin_.digital_write(bit); - this->wait_(wait, start); + this->wait_(wait, start, bit_time); } uint8_t ESP8266SoftwareSerial::read_byte() { if (this->rx_in_pos_ == this->rx_out_pos_) diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 469885b6b6..54bcad3993 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -28,9 +28,9 @@ class ESP8266SoftwareSerial { protected: static void gpio_intr(ESP8266SoftwareSerial *arg); - void wait_(uint32_t *wait, const uint32_t &start); - bool read_bit_(uint32_t *wait, const uint32_t &start); - void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); + void wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time); + bool read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time); + void write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time); uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index cfc92932a9..13ba0a9791 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2066,6 +2066,32 @@ class LwIPLock { #endif }; +#if defined(USE_ESP8266) && F_CPU != 160000000L +// Forward decl from +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" bool system_update_cpu_freq(uint8_t freq); +#endif + +/** Runs the CPU at 160 MHz while alive. ESP8266 built for 80 MHz only; elsewhere it compiles to nothing. + * + * The core resets the clock before every loop() pass, so a scope must stay within one pass, must not nest and + * must not yield to the main loop. Peripheral clocks are unchanged, but the cycle counter runs twice as fast, so + * code that times itself against F_CPU, including ISRs that fire while a scope is open, must read CPU2X. + */ +class CpuFrequencyBoost { + public: + CpuFrequencyBoost(const CpuFrequencyBoost &) = delete; + CpuFrequencyBoost &operator=(const CpuFrequencyBoost &) = delete; +#if defined(USE_ESP8266) && F_CPU != 160000000L + CpuFrequencyBoost() { system_update_cpu_freq(160); } + ~CpuFrequencyBoost() { system_update_cpu_freq(80); } +#else + // Not = default, so clang-tidy does not flag unused variables at call sites + CpuFrequencyBoost() {} + ~CpuFrequencyBoost() {} +#endif +}; + /** Helper class to request `loop()` to be called as fast as possible. * * Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher From 8db901cbfccb50aed0675733574e368d1d2eafb4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 25 Sep 2026 05:00:33 +0100 Subject: [PATCH 13/33] [nrf52] set WDT timeout to 30sec for Adafruit bootloader (#19596) Co-authored-by: J. Nick Koston --- esphome/components/nrf52/dfu.cpp | 1 + esphome/components/zephyr/hal.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index 24dee99726..0122ed9b31 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -28,6 +28,7 @@ void DeviceFirmwareUpdate::setup() { this->reset_pin_->digital_write(true); } else { NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET; + arch_feed_wdt(); App.reboot(); } } diff --git a/esphome/components/zephyr/hal.cpp b/esphome/components/zephyr/hal.cpp index ad8ed5c95c..10e8340a40 100644 --- a/esphome/components/zephyr/hal.cpp +++ b/esphome/components/zephyr/hal.cpp @@ -27,7 +27,11 @@ void arch_init() { if (device_is_ready(WDT)) { static wdt_timeout_cfg wdt_config{}; wdt_config.flags = WDT_FLAG_RESET_SOC; -#ifdef USE_ZIGBEE +#ifndef USE_BOOTLOADER_MCUBOOT + // Adafruit bootloader doesn't feed the WDT while + // erasing flash during a firmware update, so a shorter timeout can break the update. + wdt_config.window.max = 30000; +#elif defined(USE_ZIGBEE) // zboss thread uses a lot of CPU cycles during startup wdt_config.window.max = 10000; #else From 91210c5ef543878ff6c685bcc5b1d1ba7d981018 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 08:37:03 +0100 Subject: [PATCH 14/33] [button] Use register_apply_action for button.press (#19598) --- esphome/components/button/__init__.py | 18 ++++-------------- esphome/components/button/automation.h | 19 ------------------- tests/components/button/common.yaml | 6 ++++++ 3 files changed, 10 insertions(+), 33 deletions(-) delete mode 100644 esphome/components/button/automation.h diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index ee24002b8a..bdb438a99d 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -16,14 +16,14 @@ from esphome.const import ( DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] @@ -40,8 +40,6 @@ button_ns = cg.esphome_ns.namespace("button") Button = button_ns.class_("Button", cg.EntityBase) ButtonPtr = Button.operator("ptr") -PressAction = button_ns.class_("PressAction", automation.Action) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -123,17 +121,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True +automation.register_apply_action( + "button.press", BUTTON_PRESS_SCHEMA, automation.ApplyCall("press()") ) -async def button_press_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h deleted file mode 100644 index 683328b394..0000000000 --- a/esphome/components/button/automation.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/components/button/button.h" -#include "esphome/core/automation.h" -#include "esphome/core/component.h" - -namespace esphome::button { - -template class PressAction final : public Action { - public: - explicit PressAction(Button *button) : button_(button) {} - - void play(const Ts &...x) override { this->button_->press(); } - - protected: - Button *button_; -}; - -} // namespace esphome::button diff --git a/tests/components/button/common.yaml b/tests/components/button/common.yaml index d5978601f4..352a4c2923 100644 --- a/tests/components/button/common.yaml +++ b/tests/components/button/common.yaml @@ -4,3 +4,9 @@ button: id: some_button on_press: - logger.log: Button pressed + +esphome: + on_boot: + - button.press: some_button + - button.press: + id: some_button From cc5ed932471da138e010b21a426f28ff8d698fba Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:29:16 +0200 Subject: [PATCH 15/33] [zigbee] move init back into setup (#19427) --- esphome/components/zigbee/zigbee_esp32.cpp | 34 +++++++++++++++++--- esphome/components/zigbee/zigbee_esp32.h | 36 ++++++++++++++++++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index 4f9c70da75..65ceae8a4b 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -272,7 +272,9 @@ static void ezb_task(void *pv_parameters) { vTaskDelete(NULL); } -ZigbeeComponent::ZigbeeComponent() { +void ZigbeeComponent::setup() { + global_zigbee = this; + esp_zigbee_platform_config_t platform_config = { .storage_partition_name = "nvs", .radio_config = EZB_DEFAULT_RADIO_CONFIG(), @@ -299,11 +301,7 @@ ZigbeeComponent::ZigbeeComponent() { this->mark_failed(); return; } - this->dev_desc_ = ezb_af_create_device_desc(); -} -void ZigbeeComponent::setup() { - global_zigbee = this; #ifdef USE_WIFI if (esp_coex_wifi_i154_enable() != ESP_OK) { this->mark_failed(); @@ -341,6 +339,15 @@ void ZigbeeComponent::setup() { }; ezb_af_set_node_power_desc(&desc); + // Finish zigbee data model + for (auto &attr_value : this->attr_values_) { + ezb_zcl_attr_desc_t attr_desc = attr_value.attr_desc; + void *value_p = &attr_value.value; + ezb_zcl_attr_desc_set_value(attr_desc, value_p); + } + // free memory + std::vector().swap(this->attr_values_); + // Start the Zigbee task with priority 1 to ensure main loop can still run even if Zigbee is busy xTaskCreate(ezb_task, "Zigbee_main", 4096, NULL, 1, NULL); this->disable_loop(); // loop is only needed for processing events, so disable until we join a network @@ -383,6 +390,23 @@ void ZigbeeComponent::dump_config() { YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER)); } } + +bool ZigbeeComponent::string_attr_exists_(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id) { + ezb_af_ep_desc_t ep_desc = ezb_af_device_get_endpoint_desc(this->dev_desc_, endpoint_id); + if (ep_desc == NULL) { + return false; + } + ezb_zcl_cluster_desc_t cluster_desc = ezb_af_endpoint_get_cluster_desc(ep_desc, cluster_id, role); + if (cluster_desc == NULL) { + return false; + } + if (ezb_zcl_cluster_get_attr_desc(cluster_desc, attr_id, EZB_ZCL_STD_MANUF_CODE) == NULL) { + return false; + } + ESP_LOGW(TAG, "Attribute 0x%04X already exists in endpoint %u cluster 0x%04X. Can't add new value", attr_id, + endpoint_id, cluster_id); + return true; +} } // namespace esphome::zigbee #endif diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h index c19fc3ad63..e84ed9817c 100644 --- a/esphome/components/zigbee/zigbee_esp32.h +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -30,11 +30,15 @@ static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26 uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size = false); +struct AttrValue { + ezb_zcl_attr_desc_t attr_desc; + uint8_t value[4]; +}; + class ZigbeeAttribute; class ZigbeeComponent final : public Component { public: - ZigbeeComponent(); void setup() override; void loop() override; void dump_config() override; @@ -88,7 +92,8 @@ class ZigbeeComponent final : public Component { // automations // key tuple could be replaced by single 64 (48) bit int with bit fields for endpoint, cluster, role and attr_id std::map, ZigbeeAttribute *> attributes_; - ezb_af_device_desc_t dev_desc_; + std::vector attr_values_; + ezb_af_device_desc_t dev_desc_ = ezb_af_create_device_desc(); CallbackManager join_cb_{}; LazyCallbackManager start_cb_{}; bool start_reported_{false}; @@ -96,6 +101,8 @@ class ZigbeeComponent final : public Component { std::atomic joined_ = false; std::atomic join_pending_ = false; std::atomic factory_new_ = false; + // TODO: remove when esp-zigbee-lib fixes set_value before init + bool string_attr_exists_(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id); }; template @@ -110,10 +117,22 @@ void ZigbeeComponent::add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint1 // The size byte of the zcl_str must be set to the maximum value, // even though the initial string may be shorter. if constexpr (std::is_same::value) { + if (this->string_attr_exists_(endpoint_id, cluster_id, role, attr_id)) { + if (attr != nullptr) { + this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr; + } + return; + } auto zcl_str = get_zcl_string(value.c_str(), max_size, true); add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str); delete[] zcl_str; } else if constexpr (std::is_convertible::value) { + if (this->string_attr_exists_(endpoint_id, cluster_id, role, attr_id)) { + if (attr != nullptr) { + this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr; + } + return; + } auto zcl_str = get_zcl_string(value, max_size, true); add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str); delete[] zcl_str; @@ -133,7 +152,18 @@ void ZigbeeComponent::add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint if (cluster_desc == NULL) { return; } - esphome_zb_cluster_add_or_update_attr(cluster_id, cluster_desc, attr_id, value_p); + // TODO: revert when esp-zigbee-lib fixes set_value before init + ezb_zcl_attr_desc_t attr_desc = ezb_zcl_cluster_get_attr_desc(cluster_desc, attr_id, EZB_ZCL_STD_MANUF_CODE); + + if (attr_desc != NULL) { + static_assert(sizeof(*value_p) <= 4); + AttrValue attr_value; + attr_value.attr_desc = attr_desc; + memcpy(&attr_value.value, value_p, sizeof(*value_p)); + attr_values_.push_back(attr_value); + } else { + esphome_zb_cluster_add_attr(cluster_id, cluster_desc, attr_id, value_p); + } if (attr != nullptr) { this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr; From 6fa56e18bd9e54d30ee9855f78fa77f72cfa3c84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:18:52 +0100 Subject: [PATCH 16/33] [sendspin] Use register_apply_action for the switch, static delay and transition_finished actions (#19608) --- esphome/components/sendspin/__init__.py | 25 ++-------- esphome/components/sendspin/automation.h | 25 ---------- esphome/components/sendspin/image/__init__.py | 22 +-------- .../components/sendspin/image/automation.h | 20 -------- .../sendspin/media_source/__init__.py | 49 +++++-------------- .../sendspin/media_source/automations.h | 26 ---------- esphome/components/sendspin/sendspin_hub.cpp | 7 +++ esphome/components/sendspin/sendspin_hub.h | 3 ++ .../sendspin/common-media_source.yaml | 6 +++ 9 files changed, 35 insertions(+), 148 deletions(-) delete mode 100644 esphome/components/sendspin/automation.h delete mode 100644 esphome/components/sendspin/image/automation.h delete mode 100644 esphome/components/sendspin/media_source/automations.h diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 49bee10936..42dae464ed 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -21,8 +21,8 @@ from esphome.const import ( CONF_VERSION, CONF_WIDTH, ) -from esphome.core import CORE, ID -from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.core import CORE +from esphome.cpp_generator import MockObj from esphome.types import ConfigType # mdns for autodiscovery @@ -109,13 +109,6 @@ SendspinHub = sendspin_ns.class_( ) -SendspinSwitchCommandAction = sendspin_ns.class_( - "SendspinSwitchCommandAction", - automation.Action, - cg.Parented.template(SendspinHub), -) - - @dataclass class SendspinConfiguration: artwork_support: bool = False @@ -240,21 +233,11 @@ SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All( ) -@automation.register_action( +automation.register_apply_action( "sendspin.switch", - SendspinSwitchCommandAction, SENDSPIN_SIMPLE_ACTION_SCHEMA, - synchronous=True, + automation.ApplyCall("switch_client()"), ) -async def sendspin_switch_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/sendspin/automation.h b/esphome/components/sendspin/automation.h deleted file mode 100644 index 0b408b1235..0000000000 --- a/esphome/components/sendspin/automation.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#ifdef USE_ESP32 - -#include "esphome/core/automation.h" -#include "sendspin_hub.h" - -namespace esphome::sendspin_ { - -#ifdef USE_SENDSPIN_CONTROLLER -template class SendspinSwitchCommandAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { - // Clear any EXTERNAL_SOURCE state so the switch command is followed - this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED); - this->parent_->send_client_command(sendspin::SendspinControllerCommand::SWITCH); - } -}; -#endif // USE_SENDSPIN_CONTROLLER - -} // namespace esphome::sendspin_ - -#endif // USE_ESP32 diff --git a/esphome/components/sendspin/image/__init__.py b/esphome/components/sendspin/image/__init__.py index 3c6c82b009..e26e8204aa 100644 --- a/esphome/components/sendspin/image/__init__.py +++ b/esphome/components/sendspin/image/__init__.py @@ -16,7 +16,6 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import ID -from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType from .. import ( @@ -198,16 +197,8 @@ async def to_code(config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) -SendspinImageTransitionFinishedAction = sendspin_ns.class_( - "SendspinImageTransitionFinishedAction", - automation.Action, - cg.Parented.template(SendspinImageSlot), -) - - -@automation.register_action( +automation.register_apply_action( "sendspin.image.transition_finished", - SendspinImageTransitionFinishedAction, automation.maybe_simple_id( cv.Schema( { @@ -215,14 +206,5 @@ SendspinImageTransitionFinishedAction = sendspin_ns.class_( } ) ), - synchronous=True, + automation.ApplyCall("transition_finished()"), ) -async def sendspin_image_transition_finished_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> cg.MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/sendspin/image/automation.h b/esphome/components/sendspin/image/automation.h deleted file mode 100644 index 154e62a4b2..0000000000 --- a/esphome/components/sendspin/image/automation.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#if defined(USE_ESP32) && defined(USE_SENDSPIN_ARTWORK) - -#include "esphome/core/automation.h" -#include "sendspin_image.h" - -namespace esphome::sendspin_ { - -template -class SendspinImageTransitionFinishedAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->transition_finished(); } -}; - -} // namespace esphome::sendspin_ - -#endif diff --git a/esphome/components/sendspin/media_source/__init__.py b/esphome/components/sendspin/media_source/__init__.py index 6a9f1f18ba..743d0bb62c 100644 --- a/esphome/components/sendspin/media_source/__init__.py +++ b/esphome/components/sendspin/media_source/__init__.py @@ -8,8 +8,6 @@ from esphome.const import ( CONF_SAMPLE_RATE, CONF_TASK_STACK_IN_PSRAM, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType from .. import ( @@ -41,18 +39,6 @@ SendspinMediaSource = sendspin_ns.class_( media_source.MediaSource, ) -EnableStaticDelayAdjustmentAction = sendspin_ns.class_( - "EnableStaticDelayAdjustmentAction", - automation.Action, - cg.Parented.template(SendspinMediaSource), -) - -DisableStaticDelayAdjustmentAction = sendspin_ns.class_( - "DisableStaticDelayAdjustmentAction", - automation.Action, - cg.Parented.template(SendspinMediaSource), -) - def _resolve_codecs(config: ConfigType) -> ConfigType: """Validate the codec preference list, filling in the default when it is not set.""" @@ -144,25 +130,16 @@ SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA = automation.maybe_simple_id( ) ) - -@automation.register_action( - "sendspin.media_source.enable_static_delay_adjustment", - EnableStaticDelayAdjustmentAction, - SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, - synchronous=True, -) -@automation.register_action( - "sendspin.media_source.disable_static_delay_adjustment", - DisableStaticDelayAdjustmentAction, - SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, - synchronous=True, -) -async def sendspin_static_delay_adjustment_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +for _name, _call in ( + ( + "sendspin.media_source.enable_static_delay_adjustment", + "set_static_delay_adjustable(true)", + ), + ( + "sendspin.media_source.disable_static_delay_adjustment", + "set_static_delay_adjustable(false)", + ), +): + automation.register_apply_action( + _name, SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, automation.ApplyCall(_call) + ) diff --git a/esphome/components/sendspin/media_source/automations.h b/esphome/components/sendspin/media_source/automations.h deleted file mode 100644 index f5c35f107a..0000000000 --- a/esphome/components/sendspin/media_source/automations.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#if defined(USE_ESP32) && defined(USE_SENDSPIN_PLAYER) && defined(USE_SENDSPIN_CONTROLLER) - -#include "esphome/core/automation.h" -#include "sendspin_media_source.h" - -namespace esphome::sendspin_ { - -template -class EnableStaticDelayAdjustmentAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(true); } -}; - -template -class DisableStaticDelayAdjustmentAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(false); } -}; - -} // namespace esphome::sendspin_ - -#endif diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 58ec57c768..15d358d50f 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -284,6 +284,13 @@ void SendspinHub::send_client_command(sendspin::SendspinControllerCommand comman } } +// THREAD CONTEXT: Main loop (invoked from the sendspin.switch action) +void SendspinHub::switch_client() { + // Clear any EXTERNAL_SOURCE state so the switch command is followed + this->update_state(sendspin::SendspinClientState::SYNCHRONIZED); + this->send_client_command(sendspin::SendspinControllerCommand::SWITCH); +} + // THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop()) void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) { this->controller_state_callbacks_.call(state); diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index b00fdc436e..8ab70d61ef 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -181,6 +181,9 @@ class SendspinHub final : public Component, void send_client_command(sendspin::SendspinControllerCommand command, std::optional volume = std::nullopt, std::optional mute = std::nullopt); + /// @brief Sends the SWITCH controller command; exposed as the sendspin.switch action. + void switch_client(); + template void add_controller_state_callback(F &&callback) { this->controller_state_callbacks_.add(std::forward(callback)); } diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 1977b79c04..87dfb0add6 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -9,3 +9,9 @@ media_source: static_delay_adjustable: true fixed_delay: 480us decode_memory: internal + +wifi: + on_connect: + then: + - sendspin.media_source.enable_static_delay_adjustment: media_source_id + - sendspin.media_source.disable_static_delay_adjustment: media_source_id From 28343e1bc9efe5d0eaf95dc4531c7a6517719158 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:20:13 +0100 Subject: [PATCH 17/33] [mqtt] Use register_apply_action for mqtt.publish, mqtt.enable and mqtt.disable (#19602) --- esphome/components/mqtt/__init__.py | 75 +++++++++++---------------- esphome/components/mqtt/mqtt_client.h | 37 ------------- tests/components/mqtt/common.yaml | 7 +++ 3 files changed, 36 insertions(+), 83 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f4be684318..37694fcfc5 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -117,10 +117,7 @@ mqtt_ns = cg.esphome_ns.namespace("mqtt") MQTTMessage = mqtt_ns.struct("MQTTMessage") MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason") MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) -MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) -MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action) -MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action) MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) @@ -503,22 +500,26 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action( - "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True -) -async def mqtt_publish_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string) - cg.add(var.set_topic(template_)) +# A bare literal is ambiguous between the std::string and (const char *, size_t) publish +# overloads, so constants and inlined `return "...";` lambdas are both spelled as std::string. +def _std_string(config: ConfigType, value: str) -> str: + rendered = automation.flash_string(config, value) + return rendered if CORE.is_esp8266 else f"std::string({rendered})" - template_ = await cg.templatable(config[CONF_PAYLOAD], args, cg.std_string) - cg.add(var.set_payload(template_)) - template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) - cg.add(var.set_qos(template_)) - template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_) - cg.add(var.set_retain(template_)) - return var + +automation.register_apply_action( + "mqtt.publish", + MQTT_PUBLISH_ACTION_SCHEMA, + automation.ApplyCall( + "publish({}, {}, {}, {})", + ( + (CONF_TOPIC, "std::string", _std_string), + (CONF_PAYLOAD, "std::string", _std_string), + (CONF_QOS, cg.uint8), + (CONF_RETAIN, cg.bool_), + ), + ), +) MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema( @@ -608,34 +609,16 @@ automation.register_apply_condition( ) -@automation.register_action( - "mqtt.enable", - MQTTEnableAction, - cv.Schema( - { - cv.GenerateID(): cv.use_id(MQTTClientComponent), - } - ), - synchronous=True, -) -async def mqtt_enable_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) - - -@automation.register_action( - "mqtt.disable", - MQTTDisableAction, - cv.Schema( - { - cv.GenerateID(): cv.use_id(MQTTClientComponent), - } - ), - synchronous=True, -) -async def mqtt_disable_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +for _name, _call in (("mqtt.enable", "enable()"), ("mqtt.disable", "disable()")): + automation.register_apply_action( + _name, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), + automation.ApplyCall(_call), + ) _platform_filter = filter_source_files_from_platform( diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 2fd9a5a550..ced9c84e10 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -378,23 +378,6 @@ class MQTTDisconnectTrigger final : public Trigger { } }; -template class MQTTPublishAction final : public Action { - public: - MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(std::string, topic) - TEMPLATABLE_VALUE(std::string, payload) - TEMPLATABLE_VALUE(uint8_t, qos) - TEMPLATABLE_VALUE(bool, retain) - - void play(const Ts &...x) override { - this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), - this->retain_.value(x...)); - } - - protected: - MQTTClientComponent *parent_; -}; - template class MQTTPublishJsonAction final : public Action { public: MQTTPublishJsonAction(MQTTClientComponent *parent) : parent_(parent) {} @@ -417,26 +400,6 @@ template class MQTTPublishJsonAction final : public Action class MQTTEnableAction final : public Action { - public: - MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} - - void play(const Ts &...x) override { this->parent_->enable(); } - - protected: - MQTTClientComponent *parent_; -}; - -template class MQTTDisableAction final : public Action { - public: - MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} - - void play(const Ts &...x) override { this->parent_->disable(); } - - protected: - MQTTClientComponent *parent_; -}; - } // namespace esphome::mqtt #endif // USE_MQTT diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index a1d27cdbd5..5ed997bfed 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -101,6 +101,13 @@ button: payload: Hello qos: 2 retain: true + - mqtt.publish: + topic: !lambda return "some/topic/button/lambda"; + payload: !lambda |- + std::string payload = "Hello"; + return payload; + qos: !lambda return 1; + retain: !lambda return false; climate: - platform: thermostat From 898ab7c7011df90a1dd25ec7529de504f2237dc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:21:18 +0100 Subject: [PATCH 18/33] [safe_mode] Use register_apply_action for safe_mode.mark_successful (#19605) --- esphome/components/safe_mode/__init__.py | 20 ++++--------------- esphome/components/safe_mode/automation.h | 12 ----------- .../components/safe_mode/common-enabled.yaml | 3 +++ 3 files changed, 7 insertions(+), 28 deletions(-) delete mode 100644 esphome/components/safe_mode/automation.h diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 9bc8a263c8..5d03ed70a8 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -10,8 +10,8 @@ from esphome.const import ( CONF_STORAGE, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.cpp_generator import MockObj, RawExpression, TemplateArgsType +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import RawExpression from esphome.types import ConfigType CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] @@ -22,7 +22,6 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) -MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) def _remove_id_if_disabled(value: ConfigType) -> ConfigType: @@ -53,26 +52,15 @@ CONFIG_SCHEMA = cv.All( ) -@automation.register_action( +automation.register_apply_action( "safe_mode.mark_successful", - MarkSuccessfulAction, cv.Schema( { cv.GenerateID(): cv.use_id(SafeModeComponent), } ), - synchronous=True, + automation.ApplyCall("mark_successful()"), ) -async def safe_mode_mark_successful_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg) - cg.add(var.set_parent(parent)) - return var _CALLBACK_AUTOMATIONS = ( diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h deleted file mode 100644 index e2858dff34..0000000000 --- a/esphome/components/safe_mode/automation.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include "esphome/core/automation.h" -#include "safe_mode.h" - -namespace esphome::safe_mode { - -template class MarkSuccessfulAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->mark_successful(); } -}; - -} // namespace esphome::safe_mode diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml index 43025c60db..b2a10b6d93 100644 --- a/tests/components/safe_mode/common-enabled.yaml +++ b/tests/components/safe_mode/common-enabled.yaml @@ -3,6 +3,7 @@ wifi: password: password1 safe_mode: + id: safe_mode_1 boot_is_good_after: 2min num_attempts: 3 reboot_timeout: 2min @@ -20,3 +21,5 @@ switch: esphome: on_boot: - safe_mode.mark_successful + - safe_mode.mark_successful: + id: safe_mode_1 From f7dd9c21c5919d9be36e2c0cce801bf0000e1d44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:21:47 +0100 Subject: [PATCH 19/33] [lock] Use register_apply_action for lock.lock, lock.unlock and lock.open (#19601) --- esphome/components/lock/__init__.py | 32 +++++++++------------------- esphome/components/lock/automation.h | 30 -------------------------- tests/components/lock/common.yaml | 3 ++- 3 files changed, 12 insertions(+), 53 deletions(-) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 4ceb6206fd..aa5bec98d5 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -12,13 +12,13 @@ from esphome.const import ( CONF_ON_UNLOCK, CONF_WEB_SERVER, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, SafeExpType CODEOWNERS = ["@esphome/core"] @@ -29,9 +29,6 @@ Lock = lock_ns.class_("Lock", cg.EntityBase) LockPtr = Lock.operator("ptr") LockCall = lock_ns.class_("LockCall") -UnlockAction = lock_ns.class_("UnlockAction", automation.Action) -LockAction = lock_ns.class_("LockAction", automation.Action) -OpenAction = lock_ns.class_("OpenAction", automation.Action) LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action) LockStateForwarder = lock_ns.class_("LockStateForwarder") @@ -134,23 +131,14 @@ LOCK_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA, synchronous=True -) -@automation.register_action( - "lock.lock", LockAction, LOCK_ACTION_SCHEMA, synchronous=True -) -@automation.register_action( - "lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True -) -async def lock_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +for _name, _call in ( + ("lock.unlock", "unlock()"), + ("lock.lock", "lock()"), + ("lock.open", "open()"), +): + automation.register_apply_action( + _name, LOCK_ACTION_SCHEMA, automation.ApplyCall(_call) + ) automation.register_apply_condition( diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 13153bc1df..e578a05643 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -6,36 +6,6 @@ namespace esphome::lock { -template class LockAction final : public Action { - public: - explicit LockAction(Lock *a_lock) : lock_(a_lock) {} - - void play(const Ts &...x) override { this->lock_->lock(); } - - protected: - Lock *lock_; -}; - -template class UnlockAction final : public Action { - public: - explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} - - void play(const Ts &...x) override { this->lock_->unlock(); } - - protected: - Lock *lock_; -}; - -template class OpenAction final : public Action { - public: - explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} - - void play(const Ts &...x) override { this->lock_->open(); } - - protected: - Lock *lock_; -}; - /// Callback forwarder that triggers an Automation<> only when a specific lock state is entered. /// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. template struct LockStateForwarder { diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 2b360ce3fb..397130dee5 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -3,7 +3,8 @@ esphome: then: - lock.lock: test_lock1 - lock.unlock: test_lock1 - - lock.open: test_lock1 + - lock.open: + id: test_lock1 - if: condition: lock.is_locked: test_lock1 From a17285afc799fec4932cfe26ae96afb4890a7aa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:23:03 +0100 Subject: [PATCH 20/33] [fan] Use register_apply_action for fan.toggle and fan.turn_off (#19600) --- esphome/components/fan/__init__.py | 19 ++----------------- esphome/components/fan/automation.h | 18 ------------------ tests/components/fan/common.yaml | 3 +++ 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index d00375ae96..526499fe07 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -61,8 +61,6 @@ RESTORE_MODES = { } # Actions -TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) -ToggleAction = fan_ns.class_("ToggleAction", automation.Action) CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) FanStateTrigger = fan_ns.class_( @@ -310,21 +308,8 @@ FAN_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True -) -async def fan_toggle_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) - - -@automation.register_action( - "fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True -) -async def fan_turn_off_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) - +automation.register_apply_action("fan.toggle", FAN_ACTION_SCHEMA, call="toggle") +automation.register_apply_action("fan.turn_off", FAN_ACTION_SCHEMA, call="turn_off") automation.register_apply_action( "fan.turn_on", diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 5f0843581b..4c587904b7 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -6,24 +6,6 @@ namespace esphome::fan { -template class TurnOffAction final : public Action { - public: - explicit TurnOffAction(Fan *state) : state_(state) {} - - void play(const Ts &...x) override { this->state_->turn_off().perform(); } - - Fan *state_; -}; - -template class ToggleAction final : public Action { - public: - explicit ToggleAction(Fan *state) : state_(state) {} - - void play(const Ts &...x) override { this->state_->toggle().perform(); } - - Fan *state_; -}; - template class CycleSpeedAction final : public Action { public: explicit CycleSpeedAction(Fan *state) : state_(state) {} diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml index 4867804394..12c7b6c872 100644 --- a/tests/components/fan/common.yaml +++ b/tests/components/fan/common.yaml @@ -104,6 +104,9 @@ button: fan.is_on: test_fan then: - fan.turn_off: test_fan + - fan.toggle: test_fan + - fan.turn_off: + id: test_fan - if: condition: fan.is_off: From 3fb48e0bfa724fc2d0150a2c9f8db1f3c585ed90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:23:33 +0100 Subject: [PATCH 21/33] [script] Use register_apply_action for script.stop (#19599) --- esphome/components/script/__init__.py | 10 ++-------- esphome/components/script/script.h | 10 ---------- tests/components/script/common.yaml | 2 ++ 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 6c16d002ad..2c79e9ed0a 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -9,7 +9,6 @@ CODEOWNERS = ["@esphome/core"] script_ns = cg.esphome_ns.namespace("script") Script = script_ns.class_("Script", automation.Trigger.template()) ScriptExecuteAction = script_ns.class_("ScriptExecuteAction", automation.Action) -ScriptStopAction = script_ns.class_("ScriptStopAction", automation.Action) ScriptWaitAction = script_ns.class_("ScriptWaitAction", automation.Action, cg.Component) SingleScript = script_ns.class_("SingleScript", Script) RestartScript = script_ns.class_("RestartScript", Script) @@ -206,16 +205,11 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): return var -@automation.register_action( +automation.register_apply_action( "script.stop", - ScriptStopAction, maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), - synchronous=True, + automation.ApplyCall("stop()"), ) -async def script_stop_action_to_code(config, action_id, template_arg, args): - full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) - template_arg = cg.TemplateArguments(full_id.type, *template_arg) - return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index d83555016d..b9ea75e0bc 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -257,16 +257,6 @@ template class ScriptExecuteAction, T Args args_; }; -template class ScriptStopAction final : public Action { - public: - ScriptStopAction(C *script) : script_(script) {} - - void play(const Ts &...x) override { this->script_->stop(); } - - protected: - C *script_; -}; - /** Wait for a script to finish before continuing. * * Uses queue-based storage to safely handle concurrent executions. diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml index f4818e2296..6b7290277a 100644 --- a/tests/components/script/common.yaml +++ b/tests/components/script/common.yaml @@ -15,6 +15,8 @@ esphome: strings: ["a", "b"] - script.wait: my_script - script.stop: my_script + - script.stop: + id: my_script_queued - if: condition: - script.is_running: my_script From 370d76d01379cc53a10107c9945e933862f5fd0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:26:57 +0100 Subject: [PATCH 22/33] [deep_sleep] Use register_apply_action for deep_sleep.prevent and deep_sleep.allow (#19607) --- esphome/components/deep_sleep/__init__.py | 40 +++++-------------- .../deep_sleep/deep_sleep_component.h | 13 ------ .../deep_sleep/common-esp32-all.yaml | 1 + .../deep_sleep/common-esp32-ext1.yaml | 1 + tests/components/deep_sleep/common-esp32.yaml | 1 + tests/components/deep_sleep/common.yaml | 4 ++ .../test-ota-rollback.nrf52-mcumgr.yaml | 1 + .../deep_sleep/test.bk72xx-ard.yaml | 1 + .../deep_sleep/test.esp8266-ard.yaml | 1 + .../deep_sleep/test.nrf52-adafruit.yaml | 1 + 10 files changed, 20 insertions(+), 44 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index d5d7077583..06286933b9 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -214,16 +214,6 @@ def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod: deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) -PreventDeepSleepAction = deep_sleep_ns.class_( - "PreventDeepSleepAction", - automation.Action, - cg.Parented.template(DeepSleepComponent), -) -AllowDeepSleepAction = deep_sleep_ns.class_( - "AllowDeepSleepAction", - automation.Action, - cg.Parented.template(DeepSleepComponent), -) WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") WAKEUP_PIN_MODES = { @@ -490,27 +480,15 @@ async def deep_sleep_enter_to_code( return var -@automation.register_action( - "deep_sleep.prevent", - PreventDeepSleepAction, - automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), - synchronous=True, -) -@automation.register_action( - "deep_sleep.allow", - AllowDeepSleepAction, - automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), - synchronous=True, -) -async def deep_sleep_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +for _name, _call in ( + ("deep_sleep.prevent", "prevent_deep_sleep()"), + ("deep_sleep.allow", "allow_deep_sleep()"), +): + automation.register_apply_action( + _name, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + automation.ApplyCall(_call), + ) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 0bbca4c5c4..2afe19c069 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -121,8 +121,6 @@ class Ext1WakeTrigger : public Trigger<>, public Component { template class EnterDeepSleepAction; -template class PreventDeepSleepAction; - /** This component allows setting up the node to go into deep sleep mode to conserve battery. * * To set this component up, first set *when* the deep sleep should trigger using set_run_cycles @@ -298,15 +296,4 @@ template class EnterDeepSleepAction final : public Action #endif }; -template -class PreventDeepSleepAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } -}; - -template class AllowDeepSleepAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } -}; - } // namespace esphome::deep_sleep diff --git a/tests/components/deep_sleep/common-esp32-all.yaml b/tests/components/deep_sleep/common-esp32-all.yaml index 9dc2f87258..68ef8f7b58 100644 --- a/tests/components/deep_sleep/common-esp32-all.yaml +++ b/tests/components/deep_sleep/common-esp32-all.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: default: 10s gpio_wakeup_reason: 30s diff --git a/tests/components/deep_sleep/common-esp32-ext1.yaml b/tests/components/deep_sleep/common-esp32-ext1.yaml index c531d44743..e22436dd64 100644 --- a/tests/components/deep_sleep/common-esp32-ext1.yaml +++ b/tests/components/deep_sleep/common-esp32-ext1.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: default: 10s gpio_wakeup_reason: 30s diff --git a/tests/components/deep_sleep/common-esp32.yaml b/tests/components/deep_sleep/common-esp32.yaml index e670787cc0..248f527616 100644 --- a/tests/components/deep_sleep/common-esp32.yaml +++ b/tests/components/deep_sleep/common-esp32.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: default: 10s gpio_wakeup_reason: 30s diff --git a/tests/components/deep_sleep/common.yaml b/tests/components/deep_sleep/common.yaml index 7a1a709965..6c6222ff87 100644 --- a/tests/components/deep_sleep/common.yaml +++ b/tests/components/deep_sleep/common.yaml @@ -4,6 +4,10 @@ esphome: - deep_sleep.prevent - delay: 1s - deep_sleep.allow + - deep_sleep.prevent: + id: deep_sleep_1 + - deep_sleep.allow: + id: deep_sleep_1 - if: condition: lambda: 'return false;' diff --git a/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml b/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml index 485490576d..b59b927e0a 100644 --- a/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml +++ b/tests/components/deep_sleep/test-ota-rollback.nrf52-mcumgr.yaml @@ -6,6 +6,7 @@ packages: deep_sleep: !include common.yaml deep_sleep: + id: deep_sleep_1 run_duration: 10s zephyr_ble_server: diff --git a/tests/components/deep_sleep/test.bk72xx-ard.yaml b/tests/components/deep_sleep/test.bk72xx-ard.yaml index bdbd27c902..df0c85d708 100644 --- a/tests/components/deep_sleep/test.bk72xx-ard.yaml +++ b/tests/components/deep_sleep/test.bk72xx-ard.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: 30s sleep_duration: 12h on_wake: diff --git a/tests/components/deep_sleep/test.esp8266-ard.yaml b/tests/components/deep_sleep/test.esp8266-ard.yaml index e4c592c095..8e5b1ca4f7 100644 --- a/tests/components/deep_sleep/test.esp8266-ard.yaml +++ b/tests/components/deep_sleep/test.esp8266-ard.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: 10s sleep_duration: 50s on_wake: diff --git a/tests/components/deep_sleep/test.nrf52-adafruit.yaml b/tests/components/deep_sleep/test.nrf52-adafruit.yaml index 6362142be2..bfc89303a9 100644 --- a/tests/components/deep_sleep/test.nrf52-adafruit.yaml +++ b/tests/components/deep_sleep/test.nrf52-adafruit.yaml @@ -1,4 +1,5 @@ deep_sleep: + id: deep_sleep_1 run_duration: 10s sleep_duration: 50s From 2d8e43ef7805bde7c7a2ca74422c1ca2c954a8de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:42:52 +0100 Subject: [PATCH 23/33] [ota] Add allow_plaintext_upload for devices that never migrated to encryption (#19396) --- esphome/__main__.py | 5 ++ esphome/components/esphome/ota/__init__.py | 45 ++++++++-- esphome/espota2.py | 47 ++++++++-- tests/component_tests/ota/test_esphome_ota.py | 36 +++++++- .../test_esphome_ota_migration_password.yaml | 14 +++ .../ota/allow_plaintext_upload.yaml | 13 +++ ...te-allow_plaintext_upload.esp8266-ard.yaml | 2 + tests/unit_tests/test_espota2_noise.py | 85 ++++++++++++++++++- tests/unit_tests/test_main.py | 82 +++++++++++++++++- 9 files changed, 309 insertions(+), 20 deletions(-) create mode 100644 tests/component_tests/ota/test_esphome_ota_migration_password.yaml create mode 100644 tests/components/ota/allow_plaintext_upload.yaml create mode 100644 tests/components/ota/validate-allow_plaintext_upload.esp8266-ard.yaml diff --git a/esphome/__main__.py b/esphome/__main__.py index 30e97f55eb..d68319e63a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1343,8 +1343,12 @@ def _upload_via_native_api( # fall back to a plaintext upload noise_psk = None plaintext_fallback = False + allow_plaintext_upload = False if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: noise_psk = encryption_conf.get(CONF_KEY) + allow_plaintext_upload = bool( + encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD) + ) if not noise_psk: raise EsphomeError( "OTA encryption is configured but no key was resolved; " @@ -1395,6 +1399,7 @@ def _upload_via_native_api( ota_type, noise_psk, plaintext_fallback=plaintext_fallback, + allow_plaintext_upload=allow_plaintext_upload, ) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index bace13c17f..e14f5e7e86 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -2,7 +2,7 @@ import logging import esphome.codegen as cg from esphome.components.noise import ( - encryption_schema, + ENCRYPTION_SCHEMA, new_psk_progmem, static_encryption_key, ) @@ -27,6 +27,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD import esphome.final_validate as fv from esphome.types import ConfigType @@ -231,15 +232,38 @@ def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) - encryption_conf[CONF_KEY] = api_key +# Uploader side options live only on the ota block; the api block keeps the +# shared schema +_ENCRYPTION_SCHEMA = ENCRYPTION_SCHEMA.extend( + { + cv.Optional(CONF_ALLOW_PLAINTEXT_UPLOAD): cv.boolean, + } +) + + +def _encryption_schema(config: ConfigType | None) -> ConfigType: + # Only a bare `encryption:` block is keyless; `false` or a list must fail + return _ENCRYPTION_SCHEMA({} if config is None else config) + + # Also called on merged same-port configs in final validate, where schemas # do not run def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType: - if CONF_PASSWORD in config and CONF_ENCRYPTION in config: - raise cv.Invalid( - f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the " - f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'" - ) - return config + if ( + CONF_PASSWORD not in config + or (encryption := config.get(CONF_ENCRYPTION)) is None + ): + return config + # The migration install may still have to answer the old firmware's + # password prompt on the plaintext leg; the password is not built in + if encryption.get(CONF_ALLOW_PLAINTEXT_UPLOAD): + return config + raise cv.Invalid( + f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the " + f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}' " + f"(or set '{CONF_ALLOW_PLAINTEXT_UPLOAD}: true' for the one install that " + f"migrates a device still asking for it)" + ) def _consume_ota_sockets(config: ConfigType) -> ConfigType: @@ -269,7 +293,7 @@ CONFIG_SCHEMA = cv.All( ): cv.port, cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean, cv.Optional(CONF_PASSWORD): cv.sensitive(), - cv.Optional(CONF_ENCRYPTION): encryption_schema, + cv.Optional(CONF_ENCRYPTION): _encryption_schema, cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" ), @@ -304,7 +328,10 @@ async def to_code(config: ConfigType) -> None: # An empty password opts in to the auth code path so set_auth_password() can be # called at runtime (e.g. to rotate the password from a lambda). When `password:` # is omitted entirely, the auth path is excluded to save flash on small devices. - if CONF_PASSWORD in config: + # A password is never built in next to encryption: validation only lets + # the two coexist for the migration install, where the password answers + # the running firmware and the build is authenticated by the key + if CONF_PASSWORD in config and CONF_ENCRYPTION not in config: cg.add_define("USE_OTA_PASSWORD") if config[CONF_PASSWORD]: cg.add(var.set_auth_password(config[CONF_PASSWORD])) diff --git a/esphome/espota2.py b/esphome/espota2.py index c683ffa323..952f88fc4b 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -211,6 +211,27 @@ class OTAEncryptionFallback(OTAError): """The encrypted attempt failed and the caller may retry in plaintext.""" +# Uploader side option under `ota: encryption:`; the ota component imports the +# name so the upload path never loads the component module +CONF_ALLOW_PLAINTEXT_UPLOAD = "allow_plaintext_upload" +ALLOW_PLAINTEXT_UPLOAD_NOTICE = ( + f"'{CONF_ALLOW_PLAINTEXT_UPLOAD}' is set; expected once, on the install that " + "migrates a device which never encrypted. If this device encrypted before, " + "something on the network stripped the offer: remove the option and check " + "the network." +) +# Logged only once the device is seen encrypting, so the migration install +# itself is never nagged and the user learns exactly when removal is safe +ALLOW_PLAINTEXT_UPLOAD_REMOVE_WARNING = f""" +****************************************************************** +* This device offers OTA encryption and accepted the key, so +* '{CONF_ALLOW_PLAINTEXT_UPLOAD}' under 'ota: encryption:' has done +* its job. Remove it from the configuration now, together with +* any 'password:' on that block: leaving the option in place lets +* an attacker on the network strip the encryption offer and +* downgrade a future upload to plaintext. +******************************************************************""" + # Remove before 2027.3.0 PLAINTEXT_FALLBACK_NOTICE = ( "A device with an api encryption key offers encryption after this " @@ -512,6 +533,7 @@ def perform_ota( ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, plaintext_fallback: bool = False, + allow_plaintext_upload: bool = False, ) -> None: # Validate up front; an out-of-range value would only surface as a # ValueError deep inside send_check, bypassing OTAError handling @@ -577,25 +599,32 @@ def perform_ota( features = 0 if noise_psk and not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): - if plaintext_fallback: - # Remove before 2027.3.0: older firmware that cannot encrypt still - # gets its update on this connection + # Remove before 2027.3.0: drop `or plaintext_fallback` and + # PLAINTEXT_FALLBACK_NOTICE here; allow_plaintext_upload stays + if allow_plaintext_upload or plaintext_fallback: + # The running firmware cannot encrypt; it still gets this update, + # and the build being sent offers encryption for the next one _LOGGER.warning( "The device did not offer OTA encryption; continuing in plaintext. %s", - PLAINTEXT_FALLBACK_NOTICE, + ALLOW_PLAINTEXT_UPLOAD_NOTICE + if allow_plaintext_upload + else PLAINTEXT_FALLBACK_NOTICE, ) noise_psk = None else: # Fail closed: an attacker could otherwise strip the offer and # capture the image (wifi credentials, api key) + # Remove before 2027.3.0: installing without the block no longer + # falls back then; advise 'allow_plaintext_upload: true' instead raise OTAError( "An OTA encryption key is configured but the device did not " "offer encryption; refusing to send the image in plaintext. " "The running firmware predates ESPHome 2026.9.0 or has no " "'api: encryption: key'. With an api key, install once " "without the 'ota: encryption:' block (that build offers " - "encryption), then restore it; otherwise flash by serial or " - "the web_server OTA platform." + f"encryption), then restore it; otherwise set '{CONF_ALLOW_PLAINTEXT_UPLOAD}: " + "true' under 'ota: encryption:' for this one install, or flash by " + "serial or the web_server OTA platform." ) if noise_psk: # The prologue binds every negotiation byte both sides saw, so any @@ -619,6 +648,8 @@ def perform_ota( raise OTAEncryptionFallback(str(err)) from err raise _LOGGER.info("Encrypted connection established") + if allow_plaintext_upload: + _LOGGER.warning(ALLOW_PLAINTEXT_UPLOAD_REMOVE_WARNING) if ota_type != OTA_TYPE_UPDATE_APP: # Any non-app OTA type requires the extended protocol and the @@ -824,6 +855,7 @@ def run_ota_impl_( ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, plaintext_fallback: bool = False, + allow_plaintext_upload: bool = False, ) -> tuple[int, str | None]: from esphome.core import CORE @@ -899,6 +931,7 @@ def run_ota_impl_( ota_type, encryption.noise_psk, encryption.plaintext_fallback, + allow_plaintext_upload=allow_plaintext_upload, ) except OTAEncryptionFallback as err: # Same address and attempt budget: not a network retry @@ -940,6 +973,7 @@ def run_ota( ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, plaintext_fallback: bool = False, + allow_plaintext_upload: bool = False, ) -> tuple[int, str | None]: try: return run_ota_impl_( @@ -950,6 +984,7 @@ def run_ota( ota_type, noise_psk, plaintext_fallback, + allow_plaintext_upload, ) except OTAError as err: _LOGGER.error(err) diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index d8bcd7e275..cd66503c1b 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -12,10 +12,11 @@ from esphome import config_validation as cv from esphome.components.esphome.ota import ( AUTO_LOAD, FILTER_SOURCE_FILES, + _encryption_schema, _validate_no_password_with_encryption, ota_esphome_final_validate, ) -from esphome.components.noise import static_encryption_key +from esphome.components.noise import encryption_schema, static_encryption_key from esphome.const import ( CONF_API, CONF_ENCRYPTION, @@ -29,6 +30,7 @@ from esphome.const import ( CONF_VERSION, ) from esphome.core import CORE, ID +from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD import esphome.final_validate as fv @@ -198,6 +200,21 @@ def test_encryption_without_any_key_rejected() -> None: fv.full_config.reset(token) +def test_encryption_schema_allow_plaintext_upload() -> None: + """The opt in is an ota only option with no default, so a merged block + that does not mention it cannot clear it; the shared api schema does not + know it.""" + assert _encryption_schema(None) == {} + assert _encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True}) == { + CONF_ALLOW_PLAINTEXT_UPLOAD: True + } + assert _encryption_schema({CONF_KEY: API_KEY}) == {CONF_KEY: API_KEY} + with pytest.raises(cv.Invalid): + _encryption_schema(False) + with pytest.raises(cv.Invalid): + encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True}) + + def test_encryption_key_mismatch_between_merged_configs_rejected() -> None: """Same-port configs with different encryption keys raise.""" full_conf = { @@ -489,6 +506,13 @@ def test_static_encryption_key() -> None: {"USE_OTA_ENCRYPTION", "USE_OTA_PASSWORD"}, {"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"}, ), + # The migration install keeps the password for the old firmware's + # prompt but the build it sends is authenticated by the key alone + ( + "migration_password", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"}, + {"USE_OTA_PASSWORD", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), # The ota encryption block is what makes the device refuse plaintext ( "encryption_required", @@ -549,6 +573,16 @@ def test_password_with_encryption_rejected() -> None: _validate_no_password_with_encryption(config) +def test_password_with_migration_install_accepted() -> None: + """The old firmware may still ask for the password on the plaintext + leg of the migration install.""" + config = { + CONF_PASSWORD: "pw", + CONF_ENCRYPTION: {CONF_KEY: API_KEY, CONF_ALLOW_PLAINTEXT_UPLOAD: True}, + } + assert _validate_no_password_with_encryption(config) is config + + def test_password_alone_accepted() -> None: """A password without encryption still validates.""" config = {CONF_PASSWORD: "pw"} diff --git a/tests/component_tests/ota/test_esphome_ota_migration_password.yaml b/tests/component_tests/ota/test_esphome_ota_migration_password.yaml new file mode 100644 index 0000000000..6a44f75751 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_migration_password.yaml @@ -0,0 +1,14 @@ +esphome: + name: ota-migration-password + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" + encryption: + allow_plaintext_upload: true diff --git a/tests/components/ota/allow_plaintext_upload.yaml b/tests/components/ota/allow_plaintext_upload.yaml new file mode 100644 index 0000000000..be222ec433 --- /dev/null +++ b/tests/components/ota/allow_plaintext_upload.yaml @@ -0,0 +1,13 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" + encryption: + allow_plaintext_upload: true diff --git a/tests/components/ota/validate-allow_plaintext_upload.esp8266-ard.yaml b/tests/components/ota/validate-allow_plaintext_upload.esp8266-ard.yaml new file mode 100644 index 0000000000..834e6197ac --- /dev/null +++ b/tests/components/ota/validate-allow_plaintext_upload.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include allow_plaintext_upload.yaml diff --git a/tests/unit_tests/test_espota2_noise.py b/tests/unit_tests/test_espota2_noise.py index 439220f09c..60f6b28e07 100644 --- a/tests/unit_tests/test_espota2_noise.py +++ b/tests/unit_tests/test_espota2_noise.py @@ -218,6 +218,7 @@ def _upload( firmware: bytes, noise_psk: str | None, plaintext_fallback: bool = False, + allow_plaintext_upload: bool = False, ) -> None: device.start() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -231,13 +232,18 @@ def _upload( Path("firmware.bin"), noise_psk=noise_psk, plaintext_fallback=plaintext_fallback, + allow_plaintext_upload=allow_plaintext_upload, ) finally: sock.close() def _run_ota( - device: FakeEncryptedDevice, firmware: bytes, tmp_path: Path, noise_psk: str + device: FakeEncryptedDevice, + firmware: bytes, + tmp_path: Path, + noise_psk: str, + plaintext_fallback: bool = True, ) -> int: """Drive the retry loop, which is where the plaintext fallback reconnects.""" path = tmp_path / "firmware.bin" @@ -249,7 +255,7 @@ def _run_ota( None, path, noise_psk=noise_psk, - plaintext_fallback=True, + plaintext_fallback=plaintext_fallback, ) return rc @@ -299,11 +305,69 @@ def test_tampered_negotiation_breaks_handshake() -> None: def test_client_fails_closed_when_device_lacks_encryption() -> None: """With a key configured, a device not offering noise aborts the upload.""" device = FakeEncryptedDevice(offer_noise=False, require_noise=False) - with pytest.raises(espota2.OTAError, match="refusing to send the image"): + with pytest.raises( + espota2.OTAError, match="refusing to send the image.*allow_plaintext_upload" + ): _upload(device, b"firmware", PSK) device.join_and_check() +def test_allow_plaintext_upload_when_device_does_not_offer( + caplog: pytest.LogCaptureFixture, +) -> None: + """The explicit opt in sends the image in plaintext to a device that + cannot encrypt, naming the option in the warning.""" + firmware = b"firmware" + device = FakeEncryptedDevice(offer_noise=False, require_noise=False) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + _upload(device, firmware, PSK, allow_plaintext_upload=True) + device.join_and_check() + assert device.received == firmware + assert any("'allow_plaintext_upload' is set" in r.message for r in caplog.records) + assert not any("2027.3.0" in r.message for r in caplog.records) + assert not any( + "Remove it from the configuration" in r.message for r in caplog.records + ) + + +def test_allow_plaintext_upload_warns_once_device_encrypts( + caplog: pytest.LogCaptureFixture, +) -> None: + """The removal warning appears exactly when it is safe to act on: the + device offered encryption and accepted the key with the option still set.""" + pytest.importorskip("aioesphomeapi.noise") + firmware = b"firmware" + device = FakeEncryptedDevice() + with caplog.at_level(logging.WARNING): + _upload(device, firmware, PSK, allow_plaintext_upload=True) + device.join_and_check() + assert device.received == firmware + assert any( + "Remove it from the configuration now" in r.message for r in caplog.records + ) + with caplog.at_level(logging.WARNING): + caplog.clear() + _upload(FakeEncryptedDevice(), firmware, PSK) + assert not caplog.records + + +def test_allow_plaintext_upload_keeps_wrong_key_failing( + caplog: pytest.LogCaptureFixture, +) -> None: + """The opt in only covers a device that does not offer; a rejected key + never turns into a plaintext upload.""" + pytest.importorskip("aioesphomeapi.noise") + device = FakeEncryptedDevice(psk=OTHER_PSK, require_noise=False) + with ( + caplog.at_level(logging.WARNING), + pytest.raises(espota2.OTAError, match="encryption key correct"), + ): + _upload(device, b"firmware", PSK, allow_plaintext_upload=True) + device.join_and_check() + assert device.received != b"firmware" + assert not any("plaintext" in r.message for r in caplog.records) + + # Remove before 2027.3.0 def test_fallback_when_device_does_not_offer(caplog: pytest.LogCaptureFixture) -> None: """The api key is tried opportunistically; an older device that cannot @@ -519,3 +583,18 @@ def test_recv_serves_buffered_plaintext_without_new_frame() -> None: assert wrapper.recv(1) == b"A" # reads and decrypts one frame assert wrapper.recv(1) == b"B" # served from the buffer, no new frame wrapper._decrypt.decrypt.assert_called_once() + + +def test_bare_block_refuses_a_device_that_cannot_encrypt( + caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """What the CLI sends for a bare `encryption:` block: a key with neither + fallback. The retry loop never reconnects in plaintext.""" + device = FakeEncryptedDevice(offer_noise=False, require_noise=False) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + rc = _run_ota(device, b"firmware", tmp_path, PSK, plaintext_fallback=False) + device.join_and_check() + assert rc == 1 + assert device.received != b"firmware" + assert any("refusing to send the image" in r.message for r in caplog.records) + assert not any("Retrying in plaintext" in r.message for r in caplog.records) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 8fb9b7376e..956ba86502 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2115,6 +2115,7 @@ def test_upload_program_ota_success( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -2153,9 +2154,75 @@ def test_upload_program_ota_encryption_key( OTA_TYPE_UPDATE_APP, key, plaintext_fallback=False, + allow_plaintext_upload=False, ) +def test_upload_program_bare_encryption_block_never_falls_back( + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """A bare `ota: encryption:` (the api key inherited by final validate, no + option) fails closed against a device that does not offer encryption.""" + from esphome.components.esphome.ota import ota_esphome_final_validate + import esphome.final_validate as fv + + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + config = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: key}}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232, CONF_ENCRYPTION: {}}], + } + token = fv.full_config.set(config) + try: + ota_esphome_final_validate({}) + config = fv.full_config.get() + finally: + fv.full_config.reset(token) + assert config[CONF_OTA][0][CONF_ENCRYPTION] == {CONF_KEY: key} + + with patch("esphome.espota2.run_ota", return_value=(0, "192.168.1.100")) as run_ota: + upload_program(config, MockArgs(), ["192.168.1.100"]) + assert run_ota.call_args.args[5] == key + assert run_ota.call_args.kwargs == { + "plaintext_fallback": False, + "allow_plaintext_upload": False, + } + + +def test_upload_program_ota_allow_plaintext_upload( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """The uploader side opt in reaches run_ota without the removed fallback.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "pw", + CONF_ENCRYPTION: {CONF_KEY: key, "allow_plaintext_upload": True}, + } + ] + } + exit_code, _ = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + assert mock_run_ota.call_args.args[2] == "pw" + assert mock_run_ota.call_args.args[5] == key + assert mock_run_ota.call_args.kwargs == { + "plaintext_fallback": False, + "allow_plaintext_upload": True, + } + + def test_upload_program_ota_api_key_opportunistic( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -2186,6 +2253,7 @@ def test_upload_program_ota_api_key_opportunistic( OTA_TYPE_UPDATE_APP, key, plaintext_fallback=True, + allow_plaintext_upload=False, ) @@ -2214,7 +2282,10 @@ def test_upload_program_ota_no_usable_api_key_stays_plaintext( assert exit_code == 0 assert mock_run_ota.call_args.args[5] is None - assert mock_run_ota.call_args.kwargs == {"plaintext_fallback": False} + assert mock_run_ota.call_args.kwargs == { + "plaintext_fallback": False, + "allow_plaintext_upload": False, + } def test_upload_program_ota_encryption_without_key_fails_closed( @@ -2274,6 +2345,7 @@ def test_upload_program_ota_with_file_arg( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -2330,6 +2402,7 @@ def test_upload_program_ota_partition_table_with_file_arg( OTA_TYPE_UPDATE_PARTITION_TABLE, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -2393,6 +2466,7 @@ def test_upload_program_ota_partition_table_mqttip( OTA_TYPE_UPDATE_PARTITION_TABLE, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -2582,6 +2656,7 @@ def test_upload_program_ota_bootloader_with_file_arg( OTA_TYPE_UPDATE_BOOTLOADER, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -3077,6 +3152,7 @@ def test_upload_program_ota_with_mqtt_resolution( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -3133,6 +3209,7 @@ def test_upload_program_ota_with_mqtt_empty_broker( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) # Verify warning was logged assert "MQTT IP discovery failed" in caplog.text @@ -5306,6 +5383,7 @@ def test_upload_program_ota_static_ip_with_mqttip( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -5357,6 +5435,7 @@ def test_upload_program_ota_multiple_mqttip_resolves_once( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) @@ -5541,6 +5620,7 @@ def test_upload_program_ota_mqtt_timeout_fallback( OTA_TYPE_UPDATE_APP, None, plaintext_fallback=False, + allow_plaintext_upload=False, ) From d3fd124c59cae21035bed8067bfad9f8345be18b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:50:32 +0100 Subject: [PATCH 24/33] [core] Lint against std::nothrow, use RAMAllocator instead (#19244) --- AGENTS.md | 4 +++- script/ci-custom.py | 42 +++++++++++++++++++++++++++++++--- tests/script/test_ci_custom.py | 41 ++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 113cfe1b2a..30ce6dfe9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -737,7 +737,9 @@ file does, and it is the authority when they disagree. The most useful starting 6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices. - 7. **Detection:** Look for these patterns in compiler output: + 7. **Never use `new (std::nothrow)`:** On ESP-IDF exceptions are disabled, so a failed nothrow allocation aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects `std::nothrow`. + + 8. **Detection:** Look for these patterns in compiler output: - Large code sections with STL symbols (vector, map, set) - `alloc`, `realloc`, `dealloc` in symbol names - `_M_realloc_insert`, `_M_default_append` (vector reallocation) diff --git a/script/ci-custom.py b/script/ci-custom.py index bdf7750ce8..aaf177c941 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -164,7 +164,21 @@ def lint_post_check(func): return func -def lint_re_check(regex, **kwargs): +def _nolint_in_match(content, haystack, match, mask): + """With masking, only a trailing comment counts: the raw span still holds string contents, and + the masked text is blank exactly where comments and strings were, so NOLINT must sit after the + last real code character of the span.""" + raw = content[match.start() : match.end()] + if not mask: + return "NOLINT" in raw + masked = haystack[match.start() : match.end()].rstrip() + return "NOLINT" in raw[len(masked) :] + + +def lint_re_check(regex, mask=False, prefilter=None, **kwargs): + """mask=True blanks comments and string literals first so prose about the pattern is not reported; + the masked text keeps its length, so match offsets still index the original content. + prefilter is a literal every match must contain, checked before the costlier masking.""" flags = kwargs.pop("flags", re.MULTILINE) prog = re.compile(regex, flags) decor = lint_content_check(**kwargs) @@ -173,8 +187,11 @@ def lint_re_check(regex, **kwargs): @functools.wraps(func) def new_func(fname, content): errs = [] - for match in prog.finditer(content): - if "NOLINT" in match.group(0): + if prefilter is not None and prefilter not in content: + return errs + haystack = _mask_cpp_comments_strings(content) if mask else content + for match in prog.finditer(haystack): + if _nolint_in_match(content, haystack, match, mask): continue lineno = content.count("\n", 0, match.start()) + 1 substr = content[: match.start()] @@ -1199,6 +1216,25 @@ def lint_no_std_bind(fname, match): ) +@lint_re_check( + r"[^\w]std\s*::\s*nothrow\b" + CPP_RE_EOL, + mask=True, + prefilter="nothrow", + include=cpp_include, +) +def lint_no_std_nothrow(fname, match): + return ( + f"{highlight('new (std::nothrow)')} aborts on ESP-IDF when the allocation fails, exceptions are disabled " + f"there, so it never returns nullptr.\n" + f"Please use {highlight('RAMAllocator')} from esphome/core/helpers.h, which does.\n" + f" Before: {highlight('auto *buf = new (std::nothrow) uint8_t[n];')}\n" + f" After: {highlight('auto buf = RAMAllocator().make_unique_array_for_overwrite(n);')}\n" + f"For one object use {highlight('RAMAllocator().make_unique(args...)')}; both return empty on failure.\n" + f"Default flags prefer PSRAM; pass RAMAllocator::PREFER_INTERNAL to keep it where new put it.\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + LOG_CALL_START_RE = re.compile(r"ESP_LOG\w+\s*\(") # Comments, raw/plain string literals and single char literals are consumed whole so ; ( ) ? : # inside them are never seen. A char literal is exactly one (escaped) char so a digit separator diff --git a/tests/script/test_ci_custom.py b/tests/script/test_ci_custom.py index be2c98051f..5a943cda08 100644 --- a/tests/script/test_ci_custom.py +++ b/tests/script/test_ci_custom.py @@ -1,4 +1,6 @@ -"""Unit tests for the ESP_LOG-needs-braces lint rule in script/ci-custom.py. +"""Unit tests for the ESP_LOG-needs-braces and std::nothrow lint rules in script/ci-custom.py. + +The nothrow rule is a masked lint_re_check, so its tests also pin the decorator's mask option. The rule flags an if/else/for/while whose only body is an unbraced ESP_LOG*() call (which becomes an empty statement -- and a -Wempty-body warning -- once the log level compiles the macro out). These @@ -151,6 +153,43 @@ def test_nolint_on_control_line_suppresses() -> None: assert not _lint("if (x) // NOLINT\n ESP_LOGD(t);\n") +# --- std::nothrow --- + + +def _lint_nothrow(content: str) -> list: + return ci_custom.lint_no_std_nothrow("test.cpp", content) + + +def test_nothrow_is_reported_at_its_line_and_column_and_points_at_ramallocator() -> ( + None +): + errors = _lint_nothrow( + "int a;\nint b;\n auto *p = new (std::nothrow) uint8_t[n];\n" + ) + assert [(line, col) for line, col, _msg in errors] == [(3, 18)] + assert "RAMAllocator" in errors[0][2] + + +def test_nothrow_spacing_and_the_nothrow_t_type() -> None: + assert len(_lint_nothrow("auto *p = new (std :: nothrow) Foo;\n")) == 1 + assert not _lint_nothrow( + "void *operator new(size_t n, const std::nothrow_t &) noexcept;\n" + ) + + +def test_nothrow_in_comments_and_strings_is_masked() -> None: + assert not _lint_nothrow("// new (std::nothrow) aborts on ESP-IDF\n") + assert not _lint_nothrow('ESP_LOGD(TAG, "std::nothrow");\n') + + +def test_nothrow_nolint_suppresses() -> None: + assert not _lint_nothrow("auto *p = new (std::nothrow) Foo; // NOLINT\n") + + +def test_nothrow_nolint_inside_a_string_does_not_suppress() -> None: + assert len(_lint_nothrow('auto *p = new (std::nothrow) Foo; log("NOLINT");\n')) == 1 + + # --- rule: UNIT_ constants must not be redefined (mirror of the CONF_ check) --- # Real UNIT_ constants that live in each canonical home. From af4a8dac8e574acc554d5bcfb1040d274f8a550a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 15:56:01 +0100 Subject: [PATCH 25/33] [esp32] Keep the NVS cache in PSRAM when PSRAM is guaranteed (#19378) --- esphome/components/esp32/__init__.py | 55 ++++++- .../config/nvs_cache_psram_disabled.yaml | 13 ++ .../config/nvs_cache_psram_encrypted.yaml | 14 ++ .../nvs_cache_psram_encrypted_sdkconfig.yaml | 13 ++ .../config/nvs_cache_psram_explicit.yaml | 13 ++ .../config/nvs_cache_psram_guaranteed.yaml | 11 ++ .../nvs_cache_psram_not_guaranteed.yaml | 10 ++ .../config/nvs_cache_psram_user_off.yaml | 13 ++ tests/component_tests/esp32/test_esp32.py | 135 ++++++++++++++++++ 9 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_disabled.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_encrypted.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_encrypted_sdkconfig.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_explicit.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_guaranteed.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_not_guaranteed.yaml create mode 100644 tests/component_tests/esp32/config/nvs_cache_psram_user_off.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0d409b7857..c3da73f510 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -11,7 +11,10 @@ from typing import Any from esphome import yaml_util import esphome.codegen as cg -from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION +from esphome.components.const import ( + CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, + CONF_IGNORE_NOT_FOUND, +) from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( @@ -111,6 +114,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" +CONF_NVS_CACHE_IN_PSRAM = "nvs_cache_in_psram" CONF_FLASH_CHIP = "flash_chip" CONF_KEY_ID = "key_id" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" @@ -1595,6 +1599,29 @@ def final_validate(config) -> None: path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], ) ) + if advanced.get(CONF_NVS_CACHE_IN_PSRAM): + psram_conf = full_config.get(PSRAM_DOMAIN) + if ( + psram_conf is None + or psram_conf[CONF_DISABLED] + or psram_conf[CONF_IGNORE_NOT_FOUND] + ): + errs.append( + cv.Invalid( + f"'{CONF_NVS_CACHE_IN_PSRAM}' requires PSRAM with 'ignore_not_found: false'", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM], + ) + ) + if ( + advanced.get(CONF_NVS_ENCRYPTION) is not None + or conf_fw[CONF_SDKCONFIG_OPTIONS].get("CONFIG_NVS_ENCRYPTION") == "y" + ): + errs.append( + cv.Invalid( + f"'{CONF_NVS_CACHE_IN_PSRAM}' cannot be used with NVS encryption; the keys must stay in internal RAM", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM], + ) + ) final_validate_pins(full_config) @@ -2035,6 +2062,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_NVS_CACHE_IN_PSRAM): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), @@ -2361,6 +2389,20 @@ async def _set_libc_picolibc_newlib_compat() -> None: ) +@coroutine_with_priority(CoroPriority.FINAL) +async def _apply_nvs_cache_in_psram(explicit: bool) -> None: + """Keep the NVS cache in PSRAM unless NVS encryption is on, however it was enabled.""" + # The encrypted partition object holds the derived keys, which must stay in internal RAM + if is_idf_sdkconfig_option_enabled("CONFIG_NVS_ENCRYPTION"): + if explicit: + _LOGGER.warning( + "%s ignored: NVS encryption keeps the NVS cache in internal RAM", + CONF_NVS_CACHE_IN_PSRAM, + ) + return + set_idf_sdkconfig_default("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM", True) + + @coroutine_with_priority(CoroPriority.FINAL) async def _reconcile_certificate_bundle_sdkconfig() -> None: """Enable the mbedTLS certificate bundle only when something asked for it. @@ -2913,6 +2955,17 @@ async def to_code(config): if advanced[CONF_EXECUTE_FROM_PSRAM]: add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True) + # Imported here as psram imports this module + from esphome.components.psram import is_guaranteed as psram_is_guaranteed + + # Frees internal heap (the cache scales with the NVS partition) but slows NVS, so only + # where PSRAM is known to be fitted. Decided at FINAL so every way of enabling NVS + # encryption has been seen and a user's sdkconfig_options value wins. + # Unset means on; only an explicit true is worth a warning when it has to be dropped. + requested = advanced.get(CONF_NVS_CACHE_IN_PSRAM) + if requested is not False and psram_is_guaranteed(): + CORE.add_job(_apply_nvs_cache_in_psram, requested is True) + # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides # significant performance benefits. Our benchmarks show socket operations are diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_disabled.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_disabled.yaml new file mode 100644 index 0000000000..bcfc1eaf4c --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_disabled.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + nvs_cache_in_psram: false + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_encrypted.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_encrypted.yaml new file mode 100644 index 0000000000..d73b5045da --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_encrypted.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + nvs_encryption: + key_id: 0 + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_encrypted_sdkconfig.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_encrypted_sdkconfig.yaml new file mode 100644 index 0000000000..3e1b32ba41 --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_encrypted_sdkconfig.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + sdkconfig_options: + CONFIG_NVS_ENCRYPTION: "y" + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_explicit.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_explicit.yaml new file mode 100644 index 0000000000..86174146d6 --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_explicit.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + nvs_cache_in_psram: true + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_guaranteed.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_guaranteed.yaml new file mode 100644 index 0000000000..618d265cc0 --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_guaranteed.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_not_guaranteed.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_not_guaranteed.yaml new file mode 100644 index 0000000000..b52255c5ad --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_not_guaranteed.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal diff --git a/tests/component_tests/esp32/config/nvs_cache_psram_user_off.yaml b/tests/component_tests/esp32/config/nvs_cache_psram_user_off.yaml new file mode 100644 index 0000000000..ab4043ccac --- /dev/null +++ b/tests/component_tests/esp32/config/nvs_cache_psram_user_off.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + sdkconfig_options: + CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM: "n" + +psram: + mode: octal + ignore_not_found: false diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 08f5dcc822..b41234db1a 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -4,6 +4,7 @@ Test ESP32 configuration import asyncio from collections.abc import Callable +import logging from pathlib import Path from typing import Any @@ -1575,3 +1576,137 @@ def test_vasprintf_stub_only_on_rom_vsnprintf_variants( assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected defines = {define.name for define in CORE.defines} assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected + + +@pytest.mark.parametrize( + ("fixture", "expected"), + [ + ("nvs_cache_psram_guaranteed.yaml", True), + ("nvs_cache_psram_explicit.yaml", True), + ("nvs_cache_psram_not_guaranteed.yaml", None), + ("nvs_cache_psram_disabled.yaml", None), + # the encryption keys must stay in internal RAM, whichever way encryption is enabled + ("nvs_cache_psram_encrypted.yaml", None), + ("nvs_cache_psram_encrypted_sdkconfig.yaml", None), + ], +) +def test_nvs_cache_in_psram_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + fixture: str, + expected: bool | None, +) -> None: + """The NVS cache moves to PSRAM only with guaranteed PSRAM, the option not off and no NVS encryption.""" + generate_main(component_config_path(fixture)) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is expected + + +def test_nvs_cache_in_psram_user_sdkconfig_wins( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A raw sdkconfig_options value for the NVS cache option is left alone.""" + generate_main(component_config_path("nvs_cache_psram_user_off.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig["CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM"] == RawSdkconfigValue("n") + + +@pytest.mark.parametrize( + ("full_config", "error_match"), + [ + pytest.param( + {CONF_ESPHOME: {}, "psram": {"disabled": False, "ignore_not_found": True}}, + r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'", + id="nvs_cache_in_psram_needs_guaranteed_psram", + ), + pytest.param( + {CONF_ESPHOME: {}}, + r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'", + id="nvs_cache_in_psram_needs_psram", + ), + ], +) +def test_nvs_cache_in_psram_explicit_true_errors( + full_config: dict, error_match: str, set_core_config: SetCoreConfigCallable +) -> None: + """An explicit nvs_cache_in_psram: true that cannot apply is a config error, not a silent no-op.""" + set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config) + from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + + config = { + "variant": "esp32s3", + "framework": {"type": "esp-idf", "advanced": {"nvs_cache_in_psram": True}}, + } + with pytest.raises(cv.Invalid, match=error_match): + FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +def test_nvs_cache_in_psram_explicit_true_rejects_encryption( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + full_config={ + CONF_ESPHOME: {}, + "psram": {"disabled": False, "ignore_not_found": False}, + }, + ) + from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + + config = { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"nvs_cache_in_psram": True, "nvs_encryption": {"key_id": 0}}, + }, + } + with pytest.raises(cv.Invalid, match="cannot be used with NVS encryption"): + FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +def test_nvs_cache_in_psram_default_with_encryption_is_quiet( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """Encryption on a board that never mentioned the option must not warn about it.""" + with caplog.at_level(logging.WARNING): + generate_main(component_config_path("nvs_cache_psram_encrypted.yaml")) + assert "nvs_cache_in_psram" not in caplog.text + + +@pytest.mark.asyncio +async def test_nvs_cache_in_psram_explicit_request_warns_when_encrypted( + set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture +) -> None: + """An explicit request dropped for NVS encryption enabled elsewhere logs a warning.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_SDKCONFIG_OPTIONS: {}} + ) + from esphome.components.esp32 import ( + _apply_nvs_cache_in_psram, + add_idf_sdkconfig_option, + ) + + add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True) + with caplog.at_level(logging.WARNING): + await _apply_nvs_cache_in_psram(True) + assert "nvs_cache_in_psram ignored" in caplog.text + assert ( + "CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM" + not in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + ) + + +def test_nvs_cache_in_psram_explicit_true_on_valid_board_is_quiet( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """An explicit true that applies sets the option and warns about nothing.""" + with caplog.at_level(logging.WARNING): + generate_main(component_config_path("nvs_cache_psram_explicit.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is True + assert "nvs_cache_in_psram" not in caplog.text From 45bcff2c2c8ea389169003ebf14ebc97603953c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 16:03:32 +0100 Subject: [PATCH 26/33] [micro_wake_word] Use the apply helpers for the start, stop and model automations (#19620) --- .../components/micro_wake_word/__init__.py | 50 +++++------------ .../components/micro_wake_word/automation.h | 54 ------------------- 2 files changed, 14 insertions(+), 90 deletions(-) delete mode 100644 esphome/components/micro_wake_word/automation.h diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 092c4977ce..f04f784ebc 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -6,7 +6,6 @@ import re from urllib.parse import urljoin from esphome import automation, external_files, git -from esphome.automation import register_action, register_condition from esphome.bundle import add_bundle_file import esphome.codegen as cg from esphome.components import esp32, microphone, ota, psram @@ -58,17 +57,6 @@ micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word") MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component) -DisableModelAction = micro_wake_word_ns.class_("DisableModelAction", automation.Action) -EnableModelAction = micro_wake_word_ns.class_("EnableModelAction", automation.Action) -StartAction = micro_wake_word_ns.class_("StartAction", automation.Action) -StopAction = micro_wake_word_ns.class_("StopAction", automation.Action) - -ModelIsEnabledCondition = micro_wake_word_ns.class_( - "ModelIsEnabledCondition", automation.Condition -) -IsRunningCondition = micro_wake_word_ns.class_( - "IsRunningCondition", automation.Condition -) WakeWordModel = micro_wake_word_ns.class_("WakeWordModel") @@ -619,23 +607,19 @@ async def to_code(config): MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) - -@register_action( +automation.register_apply_action( "micro_wake_word.start", - StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA, - synchronous=True, + automation.ApplyCall("start()"), ) -@register_action( - "micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA, synchronous=True +automation.register_apply_action( + "micro_wake_word.stop", + MICRO_WAKE_WORD_ACTION_SCHEMA, + automation.ApplyCall("stop()"), ) -@register_condition( - "micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA +automation.register_apply_condition( + "micro_wake_word.is_running", MICRO_WAKE_WORD_ACTION_SCHEMA, "is_running()" ) -async def micro_wake_word_action_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id( @@ -644,24 +628,18 @@ MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id( } ) - -@register_action( +automation.register_apply_action( "micro_wake_word.enable_model", - EnableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, - synchronous=True, + automation.ApplyCall("enable()"), ) -@register_action( +automation.register_apply_action( "micro_wake_word.disable_model", - DisableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, - synchronous=True, + automation.ApplyCall("disable()"), ) -@register_condition( +automation.register_apply_condition( "micro_wake_word.model_is_enabled", - ModelIsEnabledCondition, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, + "is_enabled()", ) -async def model_action(config, action_id, template_arg, args): - parent = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, parent) diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h deleted file mode 100644 index 59dfc624fa..0000000000 --- a/esphome/components/micro_wake_word/automation.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include "micro_wake_word.h" -#include "streaming_model.h" - -#ifdef USE_ESP32 - -namespace esphome::micro_wake_word { - -template class StartAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->start(); } -}; - -template class StopAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->stop(); } -}; - -template class IsRunningCondition final : public Condition, public Parented { - public: - bool check(const Ts &...x) override { return this->parent_->is_running(); } -}; - -template class EnableModelAction final : public Action { - public: - explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(const Ts &...x) override { this->wake_word_model_->enable(); } - - protected: - WakeWordModel *wake_word_model_; -}; - -template class DisableModelAction final : public Action { - public: - explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(const Ts &...x) override { this->wake_word_model_->disable(); } - - protected: - WakeWordModel *wake_word_model_; -}; - -template class ModelIsEnabledCondition final : public Condition { - public: - explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); } - - protected: - WakeWordModel *wake_word_model_; -}; - -} // namespace esphome::micro_wake_word - -#endif From db566b25d0bea7df28523847d3a69a5bd9d7a381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 16:04:14 +0100 Subject: [PATCH 27/33] [microphone] Use the apply helpers for the capture, mute and condition automations (#19619) --- esphome/components/microphone/__init__.py | 75 +++++-------------- esphome/components/microphone/automation.h | 25 ------- .../components/microphone/common-actions.yaml | 12 +++ .../microphone/test-actions.esp32-idf.yaml | 8 ++ 4 files changed, 37 insertions(+), 83 deletions(-) create mode 100644 tests/components/microphone/common-actions.yaml create mode 100644 tests/components/microphone/test-actions.esp32-idf.yaml diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 9a3f5b43e7..c604f64d77 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -14,9 +14,9 @@ from esphome.const import ( CONF_ON_DATA, CONF_TRIGGER_ID, ) -from esphome.core import CORE, ID +from esphome.core import CORE from esphome.coroutine import CoroPriority, coroutine_with_priority -from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.cpp_generator import MockObj from esphome.types import ConfigType AUTO_LOAD = ["audio"] @@ -29,30 +29,11 @@ microphone_ns = cg.esphome_ns.namespace("microphone") Microphone = microphone_ns.class_("Microphone") MicrophoneSource = microphone_ns.class_("MicrophoneSource") -CaptureAction = microphone_ns.class_( - "CaptureAction", automation.Action, cg.Parented.template(Microphone) -) -StopCaptureAction = microphone_ns.class_( - "StopCaptureAction", automation.Action, cg.Parented.template(Microphone) -) -MuteAction = microphone_ns.class_( - "MuteAction", automation.Action, cg.Parented.template(Microphone) -) -UnmuteAction = microphone_ns.class_( - "UnmuteAction", automation.Action, cg.Parented.template(Microphone) -) - - DataTrigger = microphone_ns.class_( "DataTrigger", automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")), ) -IsCapturingCondition = microphone_ns.class_( - "IsCapturingCondition", automation.Condition -) -IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition) - async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None: for conf in config.get(CONF_ON_DATA, []): @@ -189,44 +170,22 @@ async def microphone_source_to_code( return mic_source -async def microphone_action( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +for _name, _call in ( + ("microphone.capture", "start()"), + ("microphone.stop_capture", "stop()"), + ("microphone.mute", "set_mute_state(true)"), + ("microphone.unmute", "set_mute_state(false)"), +): + automation.register_apply_action( + _name, MICROPHONE_ACTION_SCHEMA, automation.ApplyCall(_call) + ) - -automation.register_action( - "microphone.capture", - CaptureAction, - MICROPHONE_ACTION_SCHEMA, - synchronous=True, -)(microphone_action) - -automation.register_action( - "microphone.stop_capture", - StopCaptureAction, - MICROPHONE_ACTION_SCHEMA, - synchronous=True, -)(microphone_action) - -automation.register_action( - "microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True -)(microphone_action) -automation.register_action( - "microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True -)(microphone_action) - -automation.register_condition( - "microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA -)(microphone_action) -automation.register_condition( - "microphone.is_muted", IsMutedCondition, MICROPHONE_ACTION_SCHEMA -)(microphone_action) +automation.register_apply_condition( + "microphone.is_capturing", MICROPHONE_ACTION_SCHEMA, "is_running()" +) +automation.register_apply_condition( + "microphone.is_muted", MICROPHONE_ACTION_SCHEMA, "get_mute_state()" +) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index c28616a290..0fac18d828 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -7,21 +7,6 @@ namespace esphome::microphone { -template class CaptureAction final : public Action, public Parented { - void play(const Ts &...x) override { this->parent_->start(); } -}; - -template class StopCaptureAction final : public Action, public Parented { - void play(const Ts &...x) override { this->parent_->stop(); } -}; - -template class MuteAction final : public Action, public Parented { - void play(const Ts &...x) override { this->parent_->set_mute_state(true); } -}; -template class UnmuteAction final : public Action, public Parented { - void play(const Ts &...x) override { this->parent_->set_mute_state(false); } -}; - class DataTrigger final : public Trigger &> { public: explicit DataTrigger(Microphone *mic) { @@ -29,14 +14,4 @@ class DataTrigger final : public Trigger &> { } }; -template class IsCapturingCondition final : public Condition, public Parented { - public: - bool check(const Ts &...x) override { return this->parent_->is_running(); } -}; - -template class IsMutedCondition final : public Condition, public Parented { - public: - bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } -}; - } // namespace esphome::microphone diff --git a/tests/components/microphone/common-actions.yaml b/tests/components/microphone/common-actions.yaml new file mode 100644 index 0000000000..885282bcdc --- /dev/null +++ b/tests/components/microphone/common-actions.yaml @@ -0,0 +1,12 @@ +# capture, stop_capture and is_capturing on the external microphone. +button: + - platform: template + name: Microphone Capture + on_press: + - if: + condition: + microphone.is_capturing: mic_id_external + then: + - microphone.stop_capture: mic_id_external + else: + - microphone.capture: mic_id_external diff --git a/tests/components/microphone/test-actions.esp32-idf.yaml b/tests/components/microphone/test-actions.esp32-idf.yaml new file mode 100644 index 0000000000..c2f27a4286 --- /dev/null +++ b/tests/components/microphone/test-actions.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + i2s_din_pin1: GPIO33 + i2s_din_pin2: GPIO34 + +packages: + i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml + microphone: !include common-pdm.yaml + actions: !include common-actions.yaml From d346835e79732c008484b90c75e4b73512f103f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 16:04:51 +0100 Subject: [PATCH 28/33] [wireguard] Use the apply helpers for the enable, disable, enabled and peer_online automations (#19616) --- esphome/components/wireguard/__init__.py | 60 ++++-------------------- esphome/components/wireguard/wireguard.h | 26 ---------- 2 files changed, 10 insertions(+), 76 deletions(-) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index ea9e5a3b0c..ac62812fcf 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -41,16 +41,6 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$") wireguard_ns = cg.esphome_ns.namespace("wireguard") Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent) AllowedIP = wireguard_ns.struct("AllowedIP") -WireguardPeerOnlineCondition = wireguard_ns.class_( - "WireguardPeerOnlineCondition", automation.Condition -) -WireguardEnabledCondition = wireguard_ns.class_( - "WireguardEnabledCondition", automation.Condition -) -WireguardEnableAction = wireguard_ns.class_("WireguardEnableAction", automation.Action) -WireguardDisableAction = wireguard_ns.class_( - "WireguardDisableAction", automation.Action -) def _wireguard_key(value): @@ -163,47 +153,17 @@ async def to_code(config): await cg.register_component(var, config) -@automation.register_condition( - "wireguard.peer_online", - WireguardPeerOnlineCondition, - cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +WIREGUARD_AUTOMATION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}) + +automation.register_apply_condition( + "wireguard.peer_online", WIREGUARD_AUTOMATION_SCHEMA, "is_peer_up()" ) -async def wireguard_peer_up_to_code(config, condition_id, template_arg, args): - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_condition( - "wireguard.enabled", - WireguardEnabledCondition, - cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), +automation.register_apply_condition( + "wireguard.enabled", WIREGUARD_AUTOMATION_SCHEMA, "is_enabled()" ) -async def wireguard_enabled_to_code(config, condition_id, template_arg, args): - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_action( - "wireguard.enable", - WireguardEnableAction, - cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), - synchronous=True, +automation.register_apply_action( + "wireguard.enable", WIREGUARD_AUTOMATION_SCHEMA, automation.ApplyCall("enable()") ) -async def wireguard_enable_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_action( - "wireguard.disable", - WireguardDisableAction, - cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), - synchronous=True, +automation.register_apply_action( + "wireguard.disable", WIREGUARD_AUTOMATION_SCHEMA, automation.ApplyCall("disable()") ) -async def wireguard_disable_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index c9c2feb7ae..68885f3a35 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -4,7 +4,6 @@ #include #include -#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/time/real_time_clock.h" @@ -164,30 +163,5 @@ static constexpr size_t MASK_KEY_BUFFER_SIZE = 12; /// Strip most part of the key only for secure printing void mask_key_to(char *buffer, size_t len, const char *key); -/// Condition to check if remote peer is online. -template -class WireguardPeerOnlineCondition final : public Condition, public Parented { - public: - bool check(const Ts &...x) override { return this->parent_->is_peer_up(); } -}; - -/// Condition to check if Wireguard component is enabled. -template class WireguardEnabledCondition final : public Condition, public Parented { - public: - bool check(const Ts &...x) override { return this->parent_->is_enabled(); } -}; - -/// Action to enable Wireguard component. -template class WireguardEnableAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->enable(); } -}; - -/// Action to disable Wireguard component. -template class WireguardDisableAction final : public Action, public Parented { - public: - void play(const Ts &...x) override { this->parent_->disable(); } -}; - } // namespace esphome::wireguard #endif From 044c4cb9bb9ce5622650cdadd89cb8b06dde0f05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 16:46:12 +0100 Subject: [PATCH 29/33] [core] Migrate trivial action and condition builders to the shared helpers (#19322) --- .../alarm_control_panel/__init__.py | 31 ++----- esphome/components/binary_sensor/__init__.py | 5 +- esphome/components/bl0906/sensor.py | 13 +-- esphome/components/ble_client/__init__.py | 15 +--- esphome/components/bm8563/time.py | 22 +---- esphome/components/cc1101/__init__.py | 38 ++++++--- esphome/components/cm1106/sensor.py | 13 +-- esphome/components/cs5460a/sensor.py | 12 +-- esphome/components/dfplayer/__init__.py | 6 +- .../components/dfrobot_sen0395/__init__.py | 12 +-- .../components/display_menu_base/__init__.py | 80 +++++++++---------- esphome/components/ds1307/time.py | 24 +----- esphome/components/duty_time/sensor.py | 57 ++++--------- esphome/components/esp32_ble/__init__.py | 48 +++++------ esphome/components/ethernet/__init__.py | 35 +++----- esphome/components/ezo_pmp/__init__.py | 32 ++------ .../components/fingerprint_grow/__init__.py | 12 +-- .../components/grove_tb6612fng/__init__.py | 14 +--- esphome/components/haier/climate.py | 49 ++++++------ esphome/components/hbridge/fan/__init__.py | 13 +-- esphome/components/hdc302x/sensor.py | 11 +-- esphome/components/integration/sensor.py | 11 +-- esphome/components/key_collector/__init__.py | 23 +----- esphome/components/max17043/sensor.py | 17 ++-- esphome/components/mcp4461/output/__init__.py | 36 ++++----- esphome/components/mhz19/sensor.py | 19 ++--- .../components/mitsubishi_cn105/__init__.py | 11 +-- esphome/components/nau7802/sensor.py | 21 ++--- esphome/components/pcf85063/time.py | 24 +----- esphome/components/pcf8563/time.py | 24 +----- esphome/components/pmwcs3/sensor.py | 14 +--- esphome/components/pn532/__init__.py | 15 +--- esphome/components/pn7150/__init__.py | 65 ++++++++------- esphome/components/pn7160/__init__.py | 65 ++++++++------- esphome/components/pzemac/sensor.py | 12 +-- esphome/components/pzemdc/sensor.py | 12 +-- esphome/components/rx8130/time.py | 24 +----- esphome/components/scd4x/sensor.py | 11 +-- esphome/components/sen5x/sensor.py | 12 +-- esphome/components/senseair/sensor.py | 28 +++---- esphome/components/sim800l/__init__.py | 10 +-- esphome/components/sound_level/sensor.py | 27 +++---- esphome/components/sprinkler/__init__.py | 49 +++++++----- esphome/components/sps30/sensor.py | 21 ++--- esphome/components/sx126x/__init__.py | 23 +++--- esphome/components/sx127x/__init__.py | 27 +++---- esphome/components/tinyusb/__init__.py | 12 +-- esphome/components/tm1651/__init__.py | 27 ++----- esphome/components/ufire_ec/sensor.py | 10 +-- esphome/components/ufire_ise/sensor.py | 10 +-- esphome/components/update/__init__.py | 26 +----- esphome/components/valve/__init__.py | 40 +++++----- .../components/voice_assistant/__init__.py | 33 ++------ esphome/components/wifi/__init__.py | 71 ++++++++-------- esphome/components/zigbee/__init__.py | 13 +-- 55 files changed, 455 insertions(+), 930 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 001ecd8861..b0fea43801 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -208,65 +208,50 @@ for _name, _arm in ( ) -@automation.register_action( +automation.register_simple_action( "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA, synchronous=True, ) -async def alarm_action_pending_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA, synchronous=True, ) -async def alarm_action_trigger_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA, synchronous=True, ) -async def alarm_action_chime_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA, synchronous=True, ) -@automation.register_condition( + + +automation.register_simple_condition( "alarm_control_panel.ready", AlarmControlPanelCondition, ALARM_CONTROL_PANEL_CONDITION_SCHEMA, ) -async def alarm_action_ready_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_condition( +automation.register_simple_condition( "alarm_control_panel.is_armed", AlarmControlPanelCondition, ALARM_CONTROL_PANEL_CONDITION_SCHEMA, ) -async def alarm_control_panel_is_armed_to_code( - config, condition_id, template_arg, args -): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, template_arg, paren) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index da0276c037..696d7345dd 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -655,7 +655,7 @@ async def to_code(config): cg.add_global(binary_sensor_ns.using) -@automation.register_action( +automation.register_simple_action( "binary_sensor.invalidate_state", BinarySensorInvalidateAction, cv.maybe_simple_value( @@ -666,9 +666,6 @@ async def to_code(config): ), synchronous=True, ) -async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) # automation.cpp only implements the click/double_click/multi_click triggers diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 1a0c2287ab..e24cd316f4 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -32,8 +32,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType # Import ICONS not included in esphome's const.py, from the local components const.py @@ -138,7 +136,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) -@automation.register_action( +automation.register_parented_action( "bl0906.reset_energy", ResetEnergyAction, maybe_simple_id( @@ -148,15 +146,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ), synchronous=True, ) -async def reset_energy_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 1ef7967fa8..810f4b66e3 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -207,26 +207,20 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action( +automation.register_simple_action( "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA, synchronous=False, ) -async def ble_disconnect_to_code(config, action_id, template_arg, args): - parent = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, parent) -@automation.register_action( +automation.register_simple_action( "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA, synchronous=False, ) -async def ble_connect_to_code(config, action_id, template_arg, args): - parent = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -328,15 +322,12 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): return var -@automation.register_action( +automation.register_simple_action( "ble_client.remove_bond", BLERemoveBondAction, BLE_REMOVE_BOND_ACTION_SCHEMA, synchronous=True, ) -async def remove_bond_to_code(config, action_id, template_arg, args): - parent = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, parent) async def to_code(config): diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index 5ef162bb7c..3a15d0eb1c 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = ( ) -@automation.register_action( +automation.register_parented_action( "bm8563.write_time", WriteAction, automation.maybe_simple_id( @@ -38,15 +38,6 @@ CONFIG_SCHEMA = ( ), synchronous=True, ) -async def bm8563_write_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var @automation.register_action( @@ -73,7 +64,7 @@ async def bm8563_start_timer_to_code( return var -@automation.register_action( +automation.register_parented_action( "bm8563.read_time", ReadAction, automation.maybe_simple_id( @@ -83,15 +74,6 @@ async def bm8563_start_timer_to_code( ), synchronous=True, ) -async def bm8563_read_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index dbafae4ad0..d851d7386d 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -364,22 +364,36 @@ CC1101_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action( - "cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True +automation.register_parented_action( + "cc1101.begin_tx", + BeginTxAction, + CC1101_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "cc1101.begin_rx", + BeginRxAction, + CC1101_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "cc1101.reset", + ResetAction, + CC1101_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "cc1101.set_idle", + SetIdleAction, + CC1101_ACTION_SCHEMA, + synchronous=True, ) -async def cc1101_action_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var def validate_raw_data(value): diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index a36f0b0059..cbb0509ce5 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -13,8 +13,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType DEPENDENCIES = ["uart"] @@ -72,18 +70,9 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_simple_action( "cm1106.calibrate_zero", CM1106CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def cm1106_calibration_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - """Service code generation entry point.""" - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index 5f14457101..946037fc34 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -17,8 +17,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@balrog-kun"] @@ -131,7 +129,7 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_power_sensor(sens)) -@automation.register_action( +automation.register_simple_action( "cs5460a.restart", CS5460ARestartAction, maybe_simple_id( @@ -141,11 +139,3 @@ async def to_code(config: ConfigType) -> None: ), synchronous=True, ) -async def restart_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index d2120deaa6..0516b27ca2 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -199,7 +199,7 @@ automation.register_apply_action( ) -@automation.register_condition( +automation.register_parented_condition( "dfplayer.is_playing", DFPlayerIsPlayingCondition, cv.Schema( @@ -208,7 +208,3 @@ automation.register_apply_action( } ), ) -async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args): - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 51562f923c..81d57026cc 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -49,7 +49,7 @@ async def to_code(config: ConfigType) -> None: await uart.register_uart_device(var, config) -@automation.register_action( +automation.register_parented_action( "dfrobot_sen0395.reset", DfrobotSen0395ResetAction, maybe_simple_id( @@ -59,16 +59,6 @@ async def to_code(config: ConfigType) -> None: ), synchronous=True, ) -async def dfrobot_sen0395_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - - return var def range_segment_list(input: Any) -> list: diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 2120abe5f7..a5204cfe2f 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -294,74 +294,71 @@ MENU_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.up", + UpAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_up_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.down", + DownAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_down_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.left", + LeftAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_left_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.right", + RightAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_right_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.enter", + EnterAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_enter_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.show", + ShowAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_show_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "display_menu.hide", + HideAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) -async def menu_hide_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA, synchronous=True, ) -async def menu_show_main_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_condition( +automation.register_simple_condition( "display_menu.is_active", IsActiveCondition, automation.maybe_simple_id( @@ -370,9 +367,6 @@ async def menu_show_main_to_code(config, action_id, template_arg, args): } ), ) -async def display_menu_is_active_to_code(config, condition_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, template_arg, paren) async def menu_item_to_code(menu, config, parent): diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py index a3ae3eb5af..278494c81f 100644 --- a/esphome/components/ds1307/time.py +++ b/esphome/components/ds1307/time.py @@ -3,8 +3,6 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@badbadc0ffee"] @@ -22,7 +20,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ).extend(i2c.i2c_device_schema(0x68)) -@automation.register_action( +automation.register_parented_action( "ds1307.write_time", WriteAction, cv.Schema( @@ -32,18 +30,9 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def ds1307_write_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( +automation.register_parented_action( "ds1307.read_time", ReadAction, automation.maybe_simple_id( @@ -53,15 +42,6 @@ async def ds1307_write_time_to_code( ), synchronous=True, ) -async def ds1307_read_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 6d878a80a5..af7e14abb8 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -1,10 +1,5 @@ -from esphome.automation import ( - Action, - Condition, - maybe_simple_id, - register_action, - register_condition, -) +from esphome import automation +from esphome.automation import Action, Condition, maybe_simple_id, register_condition import esphome.codegen as cg from esphome.components import binary_sensor, sensor import esphome.config_validation as cv @@ -93,46 +88,28 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( ) -@register_action( - "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True +automation.register_parented_action( + "sensor.duty_time.start", + StartAction, + DUTY_TIME_ID_SCHEMA, + synchronous=True, ) -async def sensor_runtime_start_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@register_action( - "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True +automation.register_parented_action( + "sensor.duty_time.stop", + StopAction, + DUTY_TIME_ID_SCHEMA, + synchronous=True, ) -async def sensor_runtime_stop_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@register_action( - "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True +automation.register_parented_action( + "sensor.duty_time.reset", + ResetAction, + DUTY_TIME_ID_SCHEMA, + synchronous=True, ) -async def sensor_runtime_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var @register_condition( diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 7e97111686..77ec17f389 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -32,8 +32,7 @@ from esphome.const import ( CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, ) -from esphome.core import CORE, ID, TimePeriod -from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.core import CORE, TimePeriod import esphome.final_validate as fv from esphome.types import ConfigType @@ -606,38 +605,27 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_ESP32_BLE_UUID") -@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) -async def ble_enabled_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - return cg.new_Pvariable(condition_id, template_arg) - - -@automation.register_action( - "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True +automation.register_bare_condition( + "ble.enabled", + BLEEnabledCondition, + cv.Schema({}), ) -async def ble_enable_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - return cg.new_Pvariable(action_id, template_arg) -@automation.register_action( - "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True +automation.register_bare_action( + "ble.enable", + BLEEnableAction, + cv.Schema({}), + synchronous=True, +) + + +automation.register_bare_action( + "ble.disable", + BLEDisableAction, + cv.Schema({}), + synchronous=True, ) -async def ble_disable_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - return cg.new_Pvariable(action_id, template_arg) # ble_advertising.cpp is fully #ifdef'd on USE_ESP32_BLE_ADVERTISING, set diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 3e7d345805..920f2e5e4b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -53,12 +53,10 @@ from esphome.const import ( ) from esphome.core import ( CORE, - ID, CoroPriority, TimePeriodMilliseconds, coroutine_with_priority, ) -from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -924,24 +922,15 @@ def _filter_source_files() -> list[str]: FILTER_SOURCE_FILES = _filter_source_files -async def _new_pvariable_to_code( - config: ConfigType, - id_: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - return cg.new_Pvariable(id_, template_arg) - - -for _name, _cls in ( - ("ethernet.connected", EthernetConnectedCondition), - ("ethernet.enabled", EthernetEnabledCondition), -): - automation.register_condition(_name, _cls, cv.Schema({}))(_new_pvariable_to_code) -for _name, _cls in ( - ("ethernet.enable", EthernetEnableAction), - ("ethernet.disable", EthernetDisableAction), -): - automation.register_action(_name, _cls, cv.Schema({}), synchronous=True)( - _new_pvariable_to_code - ) +automation.register_bare_condition( + "ethernet.connected", EthernetConnectedCondition, cv.Schema({}) +) +automation.register_bare_condition( + "ethernet.enabled", EthernetEnabledCondition, cv.Schema({}) +) +automation.register_bare_action( + "ethernet.enable", EthernetEnableAction, cv.Schema({}), synchronous=True +) +automation.register_bare_action( + "ethernet.disable", EthernetDisableAction, cv.Schema({}), synchronous=True +) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index 0793495e1a..3477de88a4 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -80,72 +80,52 @@ EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( ) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_find_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.dose_continuously", EzoPMPDoseContinuouslyAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.clear_total_volume_dosed", EzoPMPClearTotalVolumeDispensedAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_clear_total_volume_dosed_to_code( - config, action_id, template_arg, args -): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.clear_calibration", EzoPMPClearCalibrationAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) # Actions that require Multiple Args diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 8d935a3c9e..85fe3994c5 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -206,7 +206,7 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args) return var -@automation.register_action( +automation.register_parented_action( "fingerprint_grow.cancel_enroll", CancelEnrollmentAction, cv.Schema( @@ -216,10 +216,6 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args) ), synchronous=True, ) -async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var @automation.register_action( @@ -243,7 +239,7 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args) return var -@automation.register_action( +automation.register_parented_action( "fingerprint_grow.delete_all", DeleteAllAction, cv.Schema( @@ -253,10 +249,6 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args) ), synchronous=True, ) -async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index ae64c049f5..e7937b1f29 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -126,7 +126,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): return var -@automation.register_action( +automation.register_parented_action( "grove_tb6612fng.standby", GROVETB6612FNGMotorStandbyAction, cv.Schema( @@ -136,14 +136,9 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): ), synchronous=True, ) -async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - - return var -@automation.register_action( +automation.register_parented_action( "grove_tb6612fng.no_standby", GROVETB6612FNGMotorNoStandbyAction, cv.Schema( @@ -153,11 +148,6 @@ async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args) ), synchronous=True, ) -async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - - return var @automation.register_action( diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 70ae36f528..5a87ec1557 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -284,56 +284,53 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( +automation.register_simple_action( "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -async def display_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA, synchronous=True, ) -async def beeper_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) # Start self cleaning or steri-cleaning action action -@automation.register_action( +automation.register_simple_action( "climate.haier.start_self_cleaning", StartSelfCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.start_steri_cleaning", StartSteriCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, synchronous=True, ) -async def start_cleaning_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) # Set vertical airflow direction action @@ -384,44 +381,44 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, return var -@automation.register_action( +automation.register_simple_action( "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -async def health_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA, synchronous=True, ) -async def power_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) def _final_validate(config) -> None: diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 2cf1693b47..cd47d100be 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -7,14 +7,11 @@ import esphome.config_validation as cv from esphome.const import ( CONF_DECAY_MODE, CONF_ENABLE_PIN, - CONF_ID, CONF_PIN_A, CONF_PIN_B, CONF_PRESET_MODES, CONF_SPEED_COUNT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType from .. import hbridge_ns @@ -51,20 +48,12 @@ CONFIG_SCHEMA = ( ) -@automation.register_action( +automation.register_simple_action( "fan.hbridge.brake", BrakeAction, maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), synchronous=True, ) -async def fan_hbridge_brake_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py index 6d91c3df7c..31671c5b1e 100644 --- a/esphome/components/hdc302x/sensor.py +++ b/esphome/components/hdc302x/sensor.py @@ -139,18 +139,9 @@ async def hdc302x_heater_on_to_code( return var -@automation.register_action( +automation.register_parented_action( "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA, synchronous=True, ) -async def hdc302x_heater_off_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 82e8ba8df8..672af46e3e 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -106,7 +106,7 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_restore(config[CONF_RESTORE])) -@automation.register_action( +automation.register_parented_action( "sensor.integration.reset", ResetAction, automation.maybe_simple_id( @@ -116,15 +116,6 @@ async def to_code(config: ConfigType) -> None: ), synchronous=True, ) -async def sensor_integration_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var @automation.register_action( diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index bf47b6df88..c95bdc46b0 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_TIMEOUT, CONF_TRIGGER_ID, ) -from esphome.core import ID from esphome.cpp_generator import MockObj, literal from esphome.types import ConfigType, TemplateArgsType @@ -135,7 +134,7 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT])) -@automation.register_action( +automation.register_parented_action( "key_collector.enable", EnableAction, automation.maybe_simple_id( @@ -145,18 +144,9 @@ async def to_code(config: ConfigType) -> None: ), synchronous=True, ) -async def enable_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( +automation.register_parented_action( "key_collector.disable", DisableAction, automation.maybe_simple_id( @@ -166,12 +156,3 @@ async def enable_to_code( ), synchronous=True, ) -async def disable_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py index 67fb8aa5b7..7de1424a96 100644 --- a/esphome/components/max17043/sensor.py +++ b/esphome/components/max17043/sensor.py @@ -14,8 +14,6 @@ from esphome.const import ( UNIT_PERCENT, UNIT_VOLT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -74,14 +72,9 @@ MAX17043_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "max17043.sleep_mode", + SleepAction, + MAX17043_ACTION_SCHEMA, + synchronous=True, ) -async def max17043_sleep_mode_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index db1a1e6a29..d2b4b32174 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -144,36 +144,28 @@ TERMINAL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action( - "mcp4461.wiper.increase", WiperIncreaseAction, WIPER_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "mcp4461.wiper.increase", + WiperIncreaseAction, + WIPER_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "mcp4461.wiper.decrease", WiperDecreaseAction, WIPER_ACTION_SCHEMA, synchronous=True -) -async def mcp4461_wiper_step_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - wiper = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, wiper) -@automation.register_action( +automation.register_simple_action( + "mcp4461.wiper.decrease", + WiperDecreaseAction, + WIPER_ACTION_SCHEMA, + synchronous=True, +) + + +automation.register_simple_action( "mcp4461.wiper.store_nonvolatile", WiperStoreNonvolatileAction, WIPER_ACTION_SCHEMA, synchronous=True, ) -async def mcp4461_wiper_store_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - wiper = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, wiper) @automation.register_action( diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 5852686608..4434eddb08 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -122,33 +122,28 @@ NO_ARGS_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "mhz19.abc_enable", MHZ19ABCEnableAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "mhz19.abc_disable", MHZ19ABCDisableAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def mhz19_no_args_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var RANGE_ACTION_SCHEMA = maybe_simple_id( diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py index 9502773616..8f1c6ef648 100644 --- a/esphome/components/mitsubishi_cn105/__init__.py +++ b/esphome/components/mitsubishi_cn105/__init__.py @@ -162,21 +162,12 @@ async def remote_temperature_action_to_code( return var -@automation.register_action( +automation.register_parented_action( f"{DOMAIN}.clear_remote_temperature", ClearRemoteTemperatureAction, CLEAR_REMOTE_TEMPERATURE_ACTION_SCHEMA, synchronous=True, ) -async def clear_temperature_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var VANE_CONTROL_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/nau7802/sensor.py b/esphome/components/nau7802/sensor.py index 415ae09daf..3112df99d0 100644 --- a/esphome/components/nau7802/sensor.py +++ b/esphome/components/nau7802/sensor.py @@ -4,8 +4,6 @@ import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import CONF_GAIN, CONF_ID, ICON_SCALE, STATE_CLASS_MEASUREMENT -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@cujomalainey"] @@ -116,30 +114,25 @@ NAU7802_CALIBRATE_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "nau7802.calibrate_internal_offset", NAU7802CalbrateInternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "nau7802.calibrate_external_offset", NAU7802CalbrateExternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "nau7802.calibrate_gain", NAU7802CalbrateGainAction, NAU7802_CALIBRATE_SCHEMA, synchronous=True, ) -async def nau7802_calibrate_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py index 771461905e..cc7a373b42 100644 --- a/esphome/components/pcf85063/time.py +++ b/esphome/components/pcf85063/time.py @@ -3,8 +3,6 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@brogon"] @@ -24,7 +22,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ).extend(i2c.i2c_device_schema(0x51)) -@automation.register_action( +automation.register_parented_action( "pcf85063.write_time", WriteAction, cv.Schema( @@ -34,18 +32,9 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def pcf85063_write_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( +automation.register_parented_action( "pcf85063.read_time", ReadAction, automation.maybe_simple_id( @@ -55,15 +44,6 @@ async def pcf85063_write_time_to_code( ), synchronous=True, ) -async def pcf85063_read_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py index 8a0b871be9..141949bf35 100644 --- a/esphome/components/pcf8563/time.py +++ b/esphome/components/pcf8563/time.py @@ -3,8 +3,6 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@KoenBreeman"] @@ -27,7 +25,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ).extend(i2c.i2c_device_schema(0x51)) -@automation.register_action( +automation.register_parented_action( "pcf8563.write_time", WriteAction, automation.maybe_simple_id( @@ -37,18 +35,9 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def pcf8563_write_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( +automation.register_parented_action( "pcf8563.read_time", ReadAction, automation.maybe_simple_id( @@ -58,15 +47,6 @@ async def pcf8563_write_time_to_code( ), synchronous=True, ) -async def pcf8563_read_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index ae22b3e0d6..552cfafa04 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -105,26 +105,20 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( ) -@automation.register_action( +automation.register_simple_action( "pmwcs3.air_calibration", PMWCS3AirCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "pmwcs3.water_calibration", PMWCS3WaterCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, synchronous=True, ) -async def pmwcs3_calibration_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - parent = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, parent) PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 6258932312..d396050511 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -3,14 +3,12 @@ import esphome.codegen as cg from esphome.components import nfc import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_ON_FINISHED_WRITE, CONF_ON_TAG, CONF_ON_TAG_REMOVED, CONF_TRIGGER_ID, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@OttoWinter", "@jesserockz"] @@ -79,7 +77,7 @@ async def setup_pn532(var: MockObj, config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) -@automation.register_condition( +automation.register_parented_condition( "pn532.is_writing", PN532IsWritingCondition, cv.Schema( @@ -88,12 +86,3 @@ async def setup_pn532(var: MockObj, config: ConfigType) -> None: } ), ) -async def pn532_is_writing_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 4638992abf..da5f482464 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -127,54 +127,68 @@ async def pn7150_set_message_to_code( return var -@automation.register_action( +automation.register_parented_action( "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( - "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.emulation_on", + EmulationOnAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.polling_off", + PollingOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.polling_on", + PollingOnAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -async def pn7150_simple_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var _CALLBACK_AUTOMATIONS = ( @@ -220,7 +234,7 @@ async def setup_pn7150(var: MockObj, config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) -@automation.register_condition( +automation.register_parented_condition( "pn7150.is_writing", PN7150IsWritingCondition, cv.Schema( @@ -229,12 +243,3 @@ async def setup_pn7150(var: MockObj, config: ConfigType) -> None: } ), ) -async def pn7150_is_writing_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 7f9f9172a1..5ce0dd1286 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -131,54 +131,68 @@ async def pn7160_set_message_to_code( return var -@automation.register_action( +automation.register_parented_action( "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( - "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.emulation_on", + EmulationOnAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.polling_off", + PollingOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "tag.polling_on", + PollingOnAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA, synchronous=True, ) -async def pn7160_simple_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var _CALLBACK_AUTOMATIONS = ( @@ -232,7 +246,7 @@ async def setup_pn7160(var: MockObj, config: ConfigType) -> None: await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) -@automation.register_condition( +automation.register_parented_condition( "pn7160.is_writing", PN7160IsWritingCondition, cv.Schema( @@ -241,12 +255,3 @@ async def setup_pn7160(var: MockObj, config: ConfigType) -> None: } ), ) -async def pn7160_is_writing_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index f093262e18..1670204468 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -26,8 +26,6 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["modbus"] @@ -85,7 +83,7 @@ CONFIG_SCHEMA = ( ) -@automation.register_action( +automation.register_simple_action( "pzemac.reset_energy", ResetEnergyAction, maybe_simple_id( @@ -95,14 +93,6 @@ CONFIG_SCHEMA = ( ), synchronous=True, ) -async def reset_energy_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) def _final_validate(config: ConfigType) -> None: diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index b9f7246b72..97e9d09d05 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -20,8 +20,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_WATT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["modbus"] @@ -67,7 +65,7 @@ CONFIG_SCHEMA = ( ) -@automation.register_action( +automation.register_simple_action( "pzemdc.reset_energy", ResetEnergyAction, maybe_simple_id( @@ -77,14 +75,6 @@ CONFIG_SCHEMA = ( ), synchronous=True, ) -async def reset_energy_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) def _final_validate(config: ConfigType) -> None: diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py index 40d10e9f6b..0e67593fb3 100644 --- a/esphome/components/rx8130/time.py +++ b/esphome/components/rx8130/time.py @@ -3,8 +3,6 @@ import esphome.codegen as cg from esphome.components import i2c, time import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@beormund"] @@ -22,7 +20,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ).extend(i2c.i2c_device_schema(0x32)) -@automation.register_action( +automation.register_parented_action( "rx8130.write_time", WriteAction, cv.Schema( @@ -32,18 +30,9 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( ), synchronous=True, ) -async def rx8130_write_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( +automation.register_parented_action( "rx8130.read_time", ReadAction, automation.maybe_simple_id( @@ -53,15 +42,6 @@ async def rx8130_write_time_to_code( ), synchronous=True, ) -async def rx8130_read_time_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index af3ff3a7af..fcae9e670e 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -166,18 +166,9 @@ SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA, synchronous=True, ) -async def scd4x_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index e86c8bf899..e7f61d8b2d 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -43,8 +43,6 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_PERCENT, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@martgras"] @@ -282,17 +280,9 @@ SEN5X_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_simple_action( "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA, synchronous=True, ) -async def sen54_fan_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 82368a60d0..ac92efa3a4 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -11,8 +11,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType DEPENDENCIES = ["uart"] @@ -82,41 +80,41 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_simple_action( "senseair.background_calibration", SenseAirBackgroundCalibrationAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "senseair.background_calibration_result", SenseAirBackgroundCalibrationResultAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "senseair.abc_enable", SenseAirABCEnableAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "senseair.abc_disable", SenseAirABCDisableAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "senseair.abc_get_period", SenseAirABCGetPeriodAction, CALIBRATION_ACTION_SCHEMA, synchronous=True, ) -async def senseair_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index ae7ee6fa59..7f42edb863 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -125,15 +125,12 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): return var -@automation.register_action( +automation.register_simple_action( "sim800l.connect", Sim800LConnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), synchronous=True, ) -async def sim800l_connect_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) SIM800L_SEND_USSD_SCHEMA = cv.Schema( @@ -158,12 +155,9 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): return var -@automation.register_action( +automation.register_simple_action( "sim800l.disconnect", Sim800LDisconnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), synchronous=True, ) -async def sim800l_disconnect_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index d217534041..fe00ae41ba 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -11,8 +11,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["audio"] @@ -92,18 +90,17 @@ SOUND_LEVEL_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( - "sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True +automation.register_parented_action( + "sound_level.start", + StartAction, + SOUND_LEVEL_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True + + +automation.register_parented_action( + "sound_level.stop", + StopAction, + SOUND_LEVEL_ACTION_SCHEMA, + synchronous=True, ) -async def sound_level_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 21863e2621..f14ea0cdeb 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -446,26 +446,20 @@ automation.register_apply_action( ) -@automation.register_action( +automation.register_simple_action( "sprinkler.start_from_queue", StartFromQueueAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( +automation.register_simple_action( "sprinkler.start_full_cycle", StartFullCycleAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -485,45 +479,60 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, return var -@automation.register_action( +automation.register_simple_action( "sprinkler.clear_queued_valves", ClearQueuedValvesAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "sprinkler.next_valve", NextValveAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "sprinkler.previous_valve", PreviousValveAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( - "sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA, synchronous=True + + +automation.register_simple_action( + "sprinkler.pause", + PauseAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( - "sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA, synchronous=True + + +automation.register_simple_action( + "sprinkler.resume", + ResumeAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_simple_action( "sprinkler.shutdown", ShutdownAction, SPRINKLER_ACTION_SCHEMA, synchronous=True, ) -async def sprinkler_simple_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) async def to_code(config): diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 681166cd3c..a310993d82 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -26,8 +26,6 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_MICROMETER, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@martgras"] @@ -182,30 +180,25 @@ SPS30_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA, synchronous=True, ) -async def sps30_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 29e3ad5359..f8f51296a1 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -302,39 +302,36 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def no_args_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var SET_MODE_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id( diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 34f2d4122f..5701f2acb7 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -286,45 +286,44 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( +automation.register_parented_action( "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( + + +automation.register_parented_action( "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def no_args_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 7ad88d3018..422a994238 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -12,8 +12,6 @@ from esphome.components.esp32 import ( ) import esphome.config_validation as cv from esphome.const import CONF_HARDWARE_UART, CONF_ID -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] @@ -136,16 +134,8 @@ async def to_code(config: ConfigType) -> None: add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100) -@automation.register_condition( +automation.register_simple_condition( "tinyusb.is_mounted", IsMountedCondition, cv.Schema({cv.GenerateID(): cv.use_id(TinyUSB)}), ) -async def tinyusb_is_mounted_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index c0cc6f1d2c..b07576e443 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -141,32 +141,17 @@ async def tm1651_set_level_percent_to_code( return var -@automation.register_action( +automation.register_parented_action( "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True, ) -async def output_turn_off_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_action( - "tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True +automation.register_parented_action( + "tm1651.turn_on", + TurnOnAction, + BINARY_OUTPUT_ACTION_SCHEMA, + synchronous=True, ) -async def output_turn_on_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 9d989ad4e6..406de3eff3 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -124,17 +124,9 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( ) -@automation.register_action( +automation.register_simple_action( "ufire_ec.reset", UFireECResetAction, UFIRE_EC_RESET_SCHEMA, synchronous=True, ) -async def ufire_ec_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index c7e3b6f28d..aab30ff50e 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -131,17 +131,9 @@ async def ufire_ise_calibrate_probe_high_to_code( UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent)}) -@automation.register_action( +automation.register_simple_action( "ufire_ise.reset", UFireISEResetAction, UFIRE_ISE_RESET_SCHEMA, synchronous=True, ) -async def ufire_ise_reset_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 818358c48a..8770715989 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -14,14 +14,14 @@ from esphome.const import ( DEVICE_CLASS_FIRMWARE, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType +from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -144,7 +144,7 @@ automation.register_apply_action( ) -@automation.register_action( +automation.register_parented_action( "update.check", CheckAction, automation.maybe_simple_id( @@ -154,18 +154,9 @@ automation.register_apply_action( ), synchronous=True, ) -async def update_check_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@automation.register_condition( +automation.register_parented_condition( "update.is_available", IsAvailableCondition, automation.maybe_simple_id( @@ -174,12 +165,3 @@ async def update_check_action_to_code( } ), ) -async def update_is_available_condition_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 692759f3e4..9ec21ef6ab 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -182,36 +182,36 @@ VALVE_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action( - "valve.open", OpenAction, VALVE_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "valve.open", + OpenAction, + VALVE_ACTION_SCHEMA, + synchronous=True, ) -async def valve_open_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "valve.close", CloseAction, VALVE_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "valve.close", + CloseAction, + VALVE_ACTION_SCHEMA, + synchronous=True, ) -async def valve_close_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "valve.stop", StopAction, VALVE_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "valve.stop", + StopAction, + VALVE_ACTION_SCHEMA, + synchronous=True, ) -async def valve_stop_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action( - "valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA, synchronous=True +automation.register_simple_action( + "valve.toggle", + ToggleAction, + VALVE_ACTION_SCHEMA, + synchronous=True, ) -async def valve_toggle_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d66ef43fdc..2f193ebb4e 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,5 +1,4 @@ from esphome import automation -from esphome.automation import register_condition import esphome.codegen as cg from esphome.components import media_player, micro_wake_word, microphone, speaker import esphome.config_validation as cv @@ -14,8 +13,6 @@ from esphome.const import ( CONF_ON_START, CONF_SPEAKER, ) -from esphome.core import ID -from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["audio", "ring_buffer", "socket"] @@ -424,29 +421,15 @@ automation.register_apply_action( ) -@register_condition( - "voice_assistant.is_running", IsRunningCondition, VOICE_ASSISTANT_ACTION_SCHEMA +automation.register_parented_condition( + "voice_assistant.is_running", + IsRunningCondition, + VOICE_ASSISTANT_ACTION_SCHEMA, ) -async def voice_assistant_is_running_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var -@register_condition( - "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA +automation.register_parented_condition( + "voice_assistant.connected", + ConnectedCondition, + VOICE_ASSISTANT_ACTION_SCHEMA, ) -async def voice_assistant_connected_to_code( - config: ConfigType, - condition_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - var = cg.new_Pvariable(condition_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c22d49e665..c9616a6c29 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -67,14 +67,13 @@ from esphome.const import ( ) from esphome.core import ( CORE, - ID, CoroPriority, EsphomeError, HexInt, coroutine_with_priority, ) import esphome.final_validate as fv -from esphome.types import ConfigType, TemplateArgsType +from esphome.types import ConfigType from . import wpa2_eap @@ -830,45 +829,49 @@ async def to_code(config): CORE.add_job(final_step) -@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) -async def wifi_connected_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) - - -@automation.register_condition("wifi.enabled", WiFiEnabledCondition, cv.Schema({})) -async def wifi_enabled_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) - - -@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({})) -async def wifi_ap_active_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) - - -@automation.register_action( - "wifi.enable", WiFiEnableAction, cv.Schema({}), synchronous=True +automation.register_bare_condition( + "wifi.connected", + WiFiConnectedCondition, + cv.Schema({}), ) -async def wifi_enable_to_code(config, action_id, template_arg, args): - return cg.new_Pvariable(action_id, template_arg) -@automation.register_action( - "wifi.disable", WiFiDisableAction, cv.Schema({}), synchronous=True +automation.register_bare_condition( + "wifi.enabled", + WiFiEnabledCondition, + cv.Schema({}), ) -async def wifi_disable_to_code(config, action_id, template_arg, args): - return cg.new_Pvariable(action_id, template_arg) -@automation.register_action( - "wifi.roam", WiFiRoamAction, cv.Schema({}), synchronous=True +automation.register_bare_condition( + "wifi.ap_active", + WiFiAPActiveCondition, + cv.Schema({}), +) + + +automation.register_bare_action( + "wifi.enable", + WiFiEnableAction, + cv.Schema({}), + synchronous=True, +) + + +automation.register_bare_action( + "wifi.disable", + WiFiDisableAction, + cv.Schema({}), + synchronous=True, +) + + +automation.register_bare_action( + "wifi.roam", + WiFiRoamAction, + cv.Schema({}), + synchronous=True, ) -async def wifi_roam_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> cg.MockObj: - return cg.new_Pvariable(action_id, template_arg) KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 47913b34d7..8e9824d42e 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -1,7 +1,7 @@ import logging from typing import Any -from esphome import automation, core +from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import only_on_variant from esphome.components.esp32.const import ( @@ -298,18 +298,9 @@ FactoryResetAction = zigbee_ns.class_( ) -@automation.register_action( +automation.register_parented_action( "zigbee.factory_reset", FactoryResetAction, ZIGBEE_ACTION_SCHEMA, synchronous=True, ) -async def reset_zigbee_to_code( - config: ConfigType, - action_id: core.ID, - template_arg: cg.TemplateArguments, - args: list[tuple], -) -> cg.Pvariable: - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var From bb84409261282e81fd65e8e41757ffa7e36fc4f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 16:58:56 +0100 Subject: [PATCH 30/33] [sprinkler] Use register_apply_action for the seven keyless controller actions (#19617) --- esphome/components/sprinkler/__init__.py | 149 ++++++++-------------- esphome/components/sprinkler/automation.h | 70 ---------- 2 files changed, 55 insertions(+), 164 deletions(-) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index f14ea0cdeb..00144adacd 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -66,19 +66,10 @@ SprinklerControllerSwitch = sprinkler_ns.class_( "SprinklerControllerSwitch", switch.Switch, cg.Component ) -ClearQueuedValvesAction = sprinkler_ns.class_( - "ClearQueuedValvesAction", automation.Action -) -StartFromQueueAction = sprinkler_ns.class_("StartFromQueueAction", automation.Action) -StartFullCycleAction = sprinkler_ns.class_("StartFullCycleAction", automation.Action) StartSingleValveAction = sprinkler_ns.class_( "StartSingleValveAction", automation.Action ) ShutdownAction = sprinkler_ns.class_("ShutdownAction", automation.Action) -NextValveAction = sprinkler_ns.class_("NextValveAction", automation.Action) -PreviousValveAction = sprinkler_ns.class_("PreviousValveAction", automation.Action) -PauseAction = sprinkler_ns.class_("PauseAction", automation.Action) -ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) @@ -415,51 +406,61 @@ CONFIG_SCHEMA = cv.All( _VALVE_AND_DURATION = ((CONF_VALVE_NUMBER, cg.size_t), (CONF_RUN_DURATION, cg.uint32)) -automation.register_apply_action( - "sprinkler.set_divider", - SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, - automation.ApplyField(CONF_DIVIDER, "set_divider", cg.uint32), -) - -automation.register_apply_action( - "sprinkler.set_multiplier", - SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, - automation.ApplyField(CONF_MULTIPLIER, "set_multiplier", cg.float_), -) - -automation.register_apply_action( - "sprinkler.queue_valve", - SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, - automation.ApplyCall("queue_valve({}, {})", _VALVE_AND_DURATION), -) - -automation.register_apply_action( - "sprinkler.set_repeat", - SPRINKLER_ACTION_REPEAT_SCHEMA, - automation.ApplyField(CONF_REPEAT, "set_repeat", cg.uint32), -) - -automation.register_apply_action( - "sprinkler.set_valve_run_duration", - SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, - automation.ApplyCall("set_valve_run_duration({}, {})", _VALVE_AND_DURATION), -) - - -automation.register_simple_action( - "sprinkler.start_from_queue", - StartFromQueueAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "sprinkler.start_full_cycle", - StartFullCycleAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) +for _name, _schema, _apply in ( + ( + "sprinkler.set_divider", + SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, + automation.ApplyField(CONF_DIVIDER, "set_divider", cg.uint32), + ), + ( + "sprinkler.set_multiplier", + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, + automation.ApplyField(CONF_MULTIPLIER, "set_multiplier", cg.float_), + ), + ( + "sprinkler.queue_valve", + SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, + automation.ApplyCall("queue_valve({}, {})", _VALVE_AND_DURATION), + ), + ( + "sprinkler.set_repeat", + SPRINKLER_ACTION_REPEAT_SCHEMA, + automation.ApplyField(CONF_REPEAT, "set_repeat", cg.uint32), + ), + ( + "sprinkler.set_valve_run_duration", + SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, + automation.ApplyCall("set_valve_run_duration({}, {})", _VALVE_AND_DURATION), + ), + ( + "sprinkler.start_from_queue", + SPRINKLER_ACTION_SCHEMA, + automation.ApplyCall("start_from_queue()"), + ), + ( + "sprinkler.start_full_cycle", + SPRINKLER_ACTION_SCHEMA, + automation.ApplyCall("start_full_cycle()"), + ), + ( + "sprinkler.clear_queued_valves", + SPRINKLER_ACTION_SCHEMA, + automation.ApplyCall("clear_queued_valves()"), + ), + ( + "sprinkler.next_valve", + SPRINKLER_ACTION_SCHEMA, + automation.ApplyCall("next_valve()"), + ), + ( + "sprinkler.previous_valve", + SPRINKLER_ACTION_SCHEMA, + automation.ApplyCall("previous_valve()"), + ), + ("sprinkler.pause", SPRINKLER_ACTION_SCHEMA, automation.ApplyCall("pause()")), + ("sprinkler.resume", SPRINKLER_ACTION_SCHEMA, automation.ApplyCall("resume()")), +): + automation.register_apply_action(_name, _schema, _apply) @automation.register_action( @@ -479,46 +480,6 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, return var -automation.register_simple_action( - "sprinkler.clear_queued_valves", - ClearQueuedValvesAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "sprinkler.next_valve", - NextValveAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "sprinkler.previous_valve", - PreviousValveAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "sprinkler.pause", - PauseAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "sprinkler.resume", - ResumeAction, - SPRINKLER_ACTION_SCHEMA, - synchronous=True, -) - - automation.register_simple_action( "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index 14d9bd20ec..1de0e1ee11 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -6,36 +6,6 @@ namespace esphome::sprinkler { -template class ClearQueuedValvesAction final : public Action { - public: - explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->clear_queued_valves(); } - - protected: - Sprinkler *sprinkler_; -}; - -template class StartFromQueueAction final : public Action { - public: - explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->start_from_queue(); } - - protected: - Sprinkler *sprinkler_; -}; - -template class StartFullCycleAction final : public Action { - public: - explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->start_full_cycle(); } - - protected: - Sprinkler *sprinkler_; -}; - template class StartSingleValveAction final : public Action { public: explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -64,46 +34,6 @@ template class ShutdownAction final : public Action { Sprinkler *sprinkler_; }; -template class NextValveAction final : public Action { - public: - explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->next_valve(); } - - protected: - Sprinkler *sprinkler_; -}; - -template class PreviousValveAction final : public Action { - public: - explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->previous_valve(); } - - protected: - Sprinkler *sprinkler_; -}; - -template class PauseAction final : public Action { - public: - explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->pause(); } - - protected: - Sprinkler *sprinkler_; -}; - -template class ResumeAction final : public Action { - public: - explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - - void play(const Ts &...x) override { this->sprinkler_->resume(); } - - protected: - Sprinkler *sprinkler_; -}; - template class ResumeOrStartAction final : public Action { public: explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} From 1dcf0a0f9d119025c57d3831e9c128f477637356 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 17:00:33 +0100 Subject: [PATCH 31/33] [valve] Use register_apply_action for valve.open, valve.close, valve.stop and valve.toggle (#19618) --- esphome/components/valve/__init__.py | 47 ++++------------ esphome/components/valve/automation.h | 58 -------------------- tests/components/valve/common.yaml | 18 ++++++ tests/components/valve/test.esp32-idf.yaml | 2 + tests/components/valve/test.esp8266-ard.yaml | 2 + 5 files changed, 32 insertions(+), 95 deletions(-) create mode 100644 tests/components/valve/common.yaml create mode 100644 tests/components/valve/test.esp32-idf.yaml create mode 100644 tests/components/valve/test.esp8266-ard.yaml diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 9ec21ef6ab..7df58c7a2a 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -1,5 +1,5 @@ from esphome import automation -from esphome.automation import Condition, maybe_simple_id +from esphome.automation import maybe_simple_id import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv @@ -62,13 +62,7 @@ VALVE_OPERATIONS = { validate_valve_operation = cv.enum(VALVE_OPERATIONS, upper=True) # Actions -OpenAction = valve_ns.class_("OpenAction", automation.Action) -CloseAction = valve_ns.class_("CloseAction", automation.Action) -StopAction = valve_ns.class_("StopAction", automation.Action) -ToggleAction = valve_ns.class_("ToggleAction", automation.Action) ValvePublishAction = valve_ns.class_("ValvePublishAction", automation.Action) -ValveIsOpenCondition = valve_ns.class_("ValveIsOpenCondition", Condition) -ValveIsClosedCondition = valve_ns.class_("ValveIsClosedCondition", Condition) # Triggers ValveOpenTrigger = valve_ns.class_("ValveOpenTrigger", automation.Trigger.template()) @@ -182,36 +176,15 @@ VALVE_ACTION_SCHEMA = maybe_simple_id( ) -automation.register_simple_action( - "valve.open", - OpenAction, - VALVE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "valve.close", - CloseAction, - VALVE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "valve.stop", - StopAction, - VALVE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "valve.toggle", - ToggleAction, - VALVE_ACTION_SCHEMA, - synchronous=True, -) +for _name, _command in ( + ("valve.open", "set_command_open()"), + ("valve.close", "set_command_close()"), + ("valve.stop", "set_command_stop()"), + ("valve.toggle", "set_command_toggle()"), +): + automation.register_apply_action( + _name, VALVE_ACTION_SCHEMA, automation.ApplyCall(_command), call="make_call" + ) VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 9fdb7665b3..598bbc1678 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -6,64 +6,6 @@ namespace esphome::valve { -template class OpenAction final : public Action { - public: - explicit OpenAction(Valve *valve) : valve_(valve) {} - - void play(const Ts &...x) override { this->valve_->make_call().set_command_open().perform(); } - - protected: - Valve *valve_; -}; - -template class CloseAction final : public Action { - public: - explicit CloseAction(Valve *valve) : valve_(valve) {} - - void play(const Ts &...x) override { this->valve_->make_call().set_command_close().perform(); } - - protected: - Valve *valve_; -}; - -template class StopAction final : public Action { - public: - explicit StopAction(Valve *valve) : valve_(valve) {} - - void play(const Ts &...x) override { this->valve_->make_call().set_command_stop().perform(); } - - protected: - Valve *valve_; -}; - -template class ToggleAction final : public Action { - public: - explicit ToggleAction(Valve *valve) : valve_(valve) {} - - void play(const Ts &...x) override { this->valve_->make_call().set_command_toggle().perform(); } - - protected: - Valve *valve_; -}; - -template class ValveIsOpenCondition final : public Condition { - public: - ValveIsOpenCondition(Valve *valve) : valve_(valve) {} - bool check(const Ts &...x) override { return this->valve_->is_fully_open(); } - - protected: - Valve *valve_; -}; - -template class ValveIsClosedCondition final : public Condition { - public: - ValveIsClosedCondition(Valve *valve) : valve_(valve) {} - bool check(const Ts &...x) override { return this->valve_->is_fully_closed(); } - - protected: - Valve *valve_; -}; - class ValveOpenTrigger final : public Trigger<> { public: ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { diff --git a/tests/components/valve/common.yaml b/tests/components/valve/common.yaml new file mode 100644 index 0000000000..bf01f77bc9 --- /dev/null +++ b/tests/components/valve/common.yaml @@ -0,0 +1,18 @@ +valve: + - platform: template + name: Test Valve + id: test_valve + optimistic: true + has_position: true + +button: + - platform: template + name: Valve Commands + on_press: + - valve.open: test_valve + - valve.close: test_valve + - valve.stop: test_valve + - valve.toggle: test_valve + - valve.control: + id: test_valve + position: 50% diff --git a/tests/components/valve/test.esp32-idf.yaml b/tests/components/valve/test.esp32-idf.yaml new file mode 100644 index 0000000000..3c2acee7ef --- /dev/null +++ b/tests/components/valve/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + valve: !include common.yaml diff --git a/tests/components/valve/test.esp8266-ard.yaml b/tests/components/valve/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3c2acee7ef --- /dev/null +++ b/tests/components/valve/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + valve: !include common.yaml From 54d05180b7e07c6b5ab283b4bc87d13502869ef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 17:30:10 +0100 Subject: [PATCH 32/33] [duty_time] Use one import style for the automation module (#19622) --- esphome/components/duty_time/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index af7e14abb8..4afcf0a0ce 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -1,5 +1,4 @@ from esphome import automation -from esphome.automation import Action, Condition, maybe_simple_id, register_condition import esphome.codegen as cg from esphome.components import binary_sensor, sensor import esphome.config_validation as cv @@ -24,13 +23,13 @@ duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor") DutyTimeSensor = duty_time_sensor_ns.class_( "DutyTimeSensor", sensor.Sensor, cg.PollingComponent ) -BaseAction = duty_time_sensor_ns.class_("BaseAction", Action, cg.Parented) +BaseAction = duty_time_sensor_ns.class_("BaseAction", automation.Action, cg.Parented) StartAction = duty_time_sensor_ns.class_("StartAction", BaseAction) StopAction = duty_time_sensor_ns.class_("StopAction", BaseAction) ResetAction = duty_time_sensor_ns.class_("ResetAction", BaseAction) SetAction = duty_time_sensor_ns.class_("SetAction", BaseAction) RunningCondition = duty_time_sensor_ns.class_( - "RunningCondition", Condition, cg.Parented + "RunningCondition", automation.Condition, cg.Parented ) @@ -81,7 +80,7 @@ async def to_code(config: ConfigType) -> None: # AUTOMATIONS -DUTY_TIME_ID_SCHEMA = maybe_simple_id( +DUTY_TIME_ID_SCHEMA = automation.maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(DutyTimeSensor), } @@ -112,7 +111,7 @@ automation.register_parented_action( ) -@register_condition( +@automation.register_condition( "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) async def duty_time_is_running_to_code( @@ -125,7 +124,7 @@ async def duty_time_is_running_to_code( return cg.new_Pvariable(condition_id, template_arg, paren, True) -@register_condition( +@automation.register_condition( "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA ) async def duty_time_is_not_running_to_code( From 9a9169305e532d91aaca166655dad3515bd0974c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2026 17:31:34 +0100 Subject: [PATCH 33/33] [haier] Use register_apply_action for the thirteen climate.haier actions (#19624) --- esphome/components/haier/automation.h | 128 -------------- esphome/components/haier/climate.py | 165 ++++-------------- tests/components/haier/common-actions.yaml | 21 +++ .../haier/test-actions.esp8266-ard.yaml | 8 + 4 files changed, 63 insertions(+), 259 deletions(-) delete mode 100644 esphome/components/haier/automation.h create mode 100644 tests/components/haier/common-actions.yaml create mode 100644 tests/components/haier/test-actions.esp8266-ard.yaml diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h deleted file mode 100644 index a81fd4bdb7..0000000000 --- a/esphome/components/haier/automation.h +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "haier_base.h" -#include "hon_climate.h" - -namespace esphome::haier { - -template class DisplayOnAction final : public Action { - public: - DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_display_state(true); } - - protected: - HaierClimateBase *parent_; -}; - -template class DisplayOffAction final : public Action { - public: - DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_display_state(false); } - - protected: - HaierClimateBase *parent_; -}; - -template class BeeperOnAction final : public Action { - public: - BeeperOnAction(HonClimate *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_beeper_state(true); } - - protected: - HonClimate *parent_; -}; - -template class BeeperOffAction final : public Action { - public: - BeeperOffAction(HonClimate *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_beeper_state(false); } - - protected: - HonClimate *parent_; -}; - -template class VerticalAirflowAction final : public Action { - public: - VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction) - void play(const Ts &...x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } - - protected: - HonClimate *parent_; -}; - -template class HorizontalAirflowAction final : public Action { - public: - HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction) - void play(const Ts &...x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } - - protected: - HonClimate *parent_; -}; - -template class HealthOnAction final : public Action { - public: - HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_health_mode(true); } - - protected: - HaierClimateBase *parent_; -}; - -template class HealthOffAction final : public Action { - public: - HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->set_health_mode(false); } - - protected: - HaierClimateBase *parent_; -}; - -template class StartSelfCleaningAction final : public Action { - public: - StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->start_self_cleaning(); } - - protected: - HonClimate *parent_; -}; - -template class StartSteriCleaningAction final : public Action { - public: - StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->start_steri_cleaning(); } - - protected: - HonClimate *parent_; -}; - -template class PowerOnAction final : public Action { - public: - PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->send_power_on_command(); } - - protected: - HaierClimateBase *parent_; -}; - -template class PowerOffAction final : public Action { - public: - PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->send_power_off_command(); } - - protected: - HaierClimateBase *parent_; -}; - -template class PowerToggleAction final : public Action { - public: - PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} - void play(const Ts &...x) { this->parent_->toggle_power(); } - - protected: - HaierClimateBase *parent_; -}; - -} // namespace esphome::haier diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 5a87ec1557..5c4d498912 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_BEEPER, CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, - CONF_ID, CONF_LEVEL, CONF_LOGGER, CONF_LOGS, @@ -254,23 +253,6 @@ CONFIG_SCHEMA = cv.All( ) -# Actions -DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action) -DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action) -BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action) -BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action) -StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action) -StartSteriCleaningAction = haier_ns.class_( - "StartSteriCleaningAction", automation.Action -) -VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action) -HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action) -HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action) -HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action) -PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action) -PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action) -PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action) - HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id( { cv.GenerateID(): cv.use_id(HaierClimateBase), @@ -284,59 +266,35 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( ) -automation.register_simple_action( - "climate.haier.display_on", - DisplayOnAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) +for _name, _schema, _call in ( + ("climate.haier.display_on", HAIER_BASE_ACTION_SCHEMA, "set_display_state(true)"), + ("climate.haier.display_off", HAIER_BASE_ACTION_SCHEMA, "set_display_state(false)"), + ("climate.haier.beeper_on", HAIER_HON_BASE_ACTION_SCHEMA, "set_beeper_state(true)"), + ( + "climate.haier.beeper_off", + HAIER_HON_BASE_ACTION_SCHEMA, + "set_beeper_state(false)", + ), + ( + "climate.haier.start_self_cleaning", + HAIER_HON_BASE_ACTION_SCHEMA, + "start_self_cleaning()", + ), + ( + "climate.haier.start_steri_cleaning", + HAIER_HON_BASE_ACTION_SCHEMA, + "start_steri_cleaning()", + ), + ("climate.haier.health_on", HAIER_BASE_ACTION_SCHEMA, "set_health_mode(true)"), + ("climate.haier.health_off", HAIER_BASE_ACTION_SCHEMA, "set_health_mode(false)"), + ("climate.haier.power_on", HAIER_BASE_ACTION_SCHEMA, "send_power_on_command()"), + ("climate.haier.power_off", HAIER_BASE_ACTION_SCHEMA, "send_power_off_command()"), + ("climate.haier.power_toggle", HAIER_BASE_ACTION_SCHEMA, "toggle_power()"), +): + automation.register_apply_action(_name, _schema, automation.ApplyCall(_call)) - -automation.register_simple_action( - "climate.haier.display_off", - DisplayOffAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.beeper_on", - BeeperOnAction, - HAIER_HON_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.beeper_off", - BeeperOffAction, - HAIER_HON_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -# Start self cleaning or steri-cleaning action action -automation.register_simple_action( - "climate.haier.start_self_cleaning", - StartSelfCleaningAction, - HAIER_HON_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.start_steri_cleaning", - StartSteriCleaningAction, - HAIER_HON_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -# Set vertical airflow direction action -@automation.register_action( +automation.register_apply_action( "climate.haier.set_vertical_airflow", - VerticalAirflowAction, cv.Schema( { cv.GenerateID(): cv.use_id(HonClimate), @@ -345,22 +303,13 @@ automation.register_simple_action( ), } ), - synchronous=True, + automation.ApplyField( + CONF_VERTICAL_AIRFLOW, "set_vertical_airflow", AirflowVerticalDirection + ), ) -async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable( - config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection - ) - cg.add(var.set_direction(template_)) - return var - -# Set horizontal airflow direction action -@automation.register_action( +automation.register_apply_action( "climate.haier.set_horizontal_airflow", - HorizontalAirflowAction, cv.Schema( { cv.GenerateID(): cv.use_id(HonClimate), @@ -369,55 +318,9 @@ async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, ar ), } ), - synchronous=True, -) -async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable( - config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection - ) - cg.add(var.set_direction(template_)) - return var - - -automation.register_simple_action( - "climate.haier.health_on", - HealthOnAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.health_off", - HealthOffAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.power_on", - PowerOnAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.power_off", - PowerOffAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, -) - - -automation.register_simple_action( - "climate.haier.power_toggle", - PowerToggleAction, - HAIER_BASE_ACTION_SCHEMA, - synchronous=True, + automation.ApplyField( + CONF_HORIZONTAL_AIRFLOW, "set_horizontal_airflow", AirflowHorizontalDirection + ), ) diff --git a/tests/components/haier/common-actions.yaml b/tests/components/haier/common-actions.yaml new file mode 100644 index 0000000000..11b0902d79 --- /dev/null +++ b/tests/components/haier/common-actions.yaml @@ -0,0 +1,21 @@ +button: + - platform: template + name: Haier Actions + on_press: + - climate.haier.display_on: haier_ac + - climate.haier.display_off: haier_ac + - climate.haier.beeper_on: haier_ac + - climate.haier.beeper_off: haier_ac + - climate.haier.start_self_cleaning: haier_ac + - climate.haier.start_steri_cleaning: haier_ac + - climate.haier.health_on: haier_ac + - climate.haier.health_off: haier_ac + - climate.haier.power_on: haier_ac + - climate.haier.power_off: haier_ac + - climate.haier.power_toggle: haier_ac + - climate.haier.set_vertical_airflow: + id: haier_ac + vertical_airflow: UP + - climate.haier.set_horizontal_airflow: + id: haier_ac + horizontal_airflow: !lambda return esphome::haier::hon_protocol::HorizontalSwingMode::LEFT; diff --git a/tests/components/haier/test-actions.esp8266-ard.yaml b/tests/components/haier/test-actions.esp8266-ard.yaml new file mode 100644 index 0000000000..079cc9f639 --- /dev/null +++ b/tests/components/haier/test-actions.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + haier: !include common.yaml + actions: !include common-actions.yaml