From d6ca01775e5c19a48c58f7147c3dfc8e6cc00489 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 28 Nov 2025 19:24:09 +0100 Subject: [PATCH 01/10] [packages] Restore remote shorthand vars and !remove in early package contents validation (#12158) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 12 +++++++++--- .../substitutions/06-remote_packages.approved.yaml | 5 +++++ .../substitutions/06-remote_packages.input.yaml | 6 ++++++ .../substitutions/remote_package_shorthand.yaml | 4 ++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 41cde0391b..67fd2770e9 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -2,7 +2,8 @@ import logging from pathlib import Path from esphome import git, yaml_util -from esphome.config_helpers import merge_config +from esphome.components.substitutions.jinja import has_jinja +from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -39,10 +40,15 @@ def valid_package_contents(package_config: dict): for k, v in package_config.items(): if not isinstance(k, str): raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list)): - continue # e.g. script: [] or logger: {level: debug} + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} if v is None: continue # e.g. web_server: + if isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch } + continue + raise cv.Invalid("Invalid component content in package definition") return package_config diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml index 4b5315013c..0fffbfb7cb 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -23,3 +23,8 @@ values_from_repo1_main: y: 10 z: 11 volume: 990 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml index a8128a7a07..772860bf19 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -35,3 +35,9 @@ packages: b: 10 c: 11 ref: main + package5: !include + file: remote_package_shorthand.yaml + vars: + repo: repo1 + file: file1.yaml + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml new file mode 100644 index 0000000000..f49e85e038 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml @@ -0,0 +1,4 @@ +# acts as a proxy to be able to include a remote package +# in which the shorthand comes from a substitution +packages: + - github://esphome/${repo}/${file}@${ref} From 8fe981b9f1c9265daa09d0565bbc9daa7b13269f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:46:00 -0600 Subject: [PATCH 02/10] [ota] Replace std::function callbacks with listener interface --- .../components/esp32_ble_tracker/__init__.py | 4 +- .../components/esphome/ota/ota_esphome.cpp | 18 +-- .../http_request/ota/ota_http_request.cpp | 18 +-- .../http_request/update/__init__.py | 4 +- .../update/http_request_update.cpp | 30 +++-- .../http_request/update/http_request_update.h | 11 +- .../components/micro_wake_word/__init__.py | 4 +- esphome/components/ota/__init__.py | 22 +++- esphome/components/ota/automation.h | 88 +++++++------- esphome/components/ota/ota_backend.cpp | 6 +- esphome/components/ota/ota_backend.h | 108 ++++++++++++------ .../speaker/media_player/__init__.py | 4 +- .../web_server/ota/ota_web_server.cpp | 38 +++--- esphome/core/defines.h | 2 +- 14 files changed, 213 insertions(+), 144 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 4e25434aad..37e74672ed 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -5,7 +5,7 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components import esp32_ble +from esphome.components import esp32_ble, ota from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( IDF_MAX_CONNECTIONS, @@ -328,7 +328,7 @@ async def to_code(config): # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now # configured in esp32_ble component based on max_connections setting - cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + ota.request_ota_state_listeners() # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") CORE.add_job(_add_ble_features) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index eb6c61a69b..469c57211c 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -42,7 +42,7 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 #endif // USE_OTA_PASSWORD void ESPHomeOTAComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER ota::register_ota_platform(this); #endif @@ -298,8 +298,8 @@ void ESPHomeOTAComponent::handle_data_() { // accidentally trigger the update process. this->log_start_(LOG_STR("update")); this->status_set_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif // This will block for a few seconds as it locks flash @@ -358,8 +358,8 @@ void ESPHomeOTAComponent::handle_data_() { last_progress = now; float percentage = (total * 100.0f) / ota_size; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif // feed watchdog and give other tasks a chance to run this->yield_and_feed_watchdog_(); @@ -388,8 +388,8 @@ void ESPHomeOTAComponent::handle_data_() { delay(10); ESP_LOGI(TAG, "Update complete"); this->status_clear_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); #endif delay(100); // NOLINT App.safe_reboot(); @@ -403,8 +403,8 @@ error: } this->status_momentary_error("onerror", 5000); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4d9e868c74..2a52a0e264 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -18,7 +18,7 @@ namespace http_request { static const char *const TAG = "http_request.ota"; void OtaHttpRequestComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER ota::register_ota_platform(this); #endif } @@ -49,24 +49,24 @@ void OtaHttpRequestComponent::flash() { } ESP_LOGI(TAG, "Starting update"); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif auto ota_status = this->do_ota_(); switch (ota_status) { case ota::OTA_RESPONSE_OK: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status); #endif delay(10); App.safe_reboot(); break; default: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status); #endif this->md5_computed_.clear(); // will be reset at next attempt this->md5_expected_.clear(); // will be reset at next attempt @@ -159,8 +159,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { last_progress = now; float percentage = container->get_bytes_read() * 100.0f / container->content_length; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif } } // while diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index abb4b2a430..d84d80109a 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import update +from esphome.components import ota, update import esphome.config_validation as cv from esphome.const import CONF_SOURCE @@ -38,6 +38,6 @@ async def to_code(config): cg.add(var.set_source_url(config[CONF_SOURCE])) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() await cg.register_component(var, config) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c91b0eba73..ca4afa67d4 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -21,20 +21,26 @@ static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; void HttpRequestUpdate::setup() { - this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) { - if (state == ota::OTAState::OTA_IN_PROGRESS) { - this->state_ = update::UPDATE_STATE_INSTALLING; - this->update_info_.has_progress = true; - this->update_info_.progress = progress; - this->publish_state(); - } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { - this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error(LOG_STR("Failed to install firmware")); - this->publish_state(); - } - }); +#ifdef USE_OTA_STATE_LISTENER + this->ota_parent_->add_state_listener(this); +#endif } +#ifdef USE_OTA_STATE_LISTENER +void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { + if (state == ota::OTAState::OTA_IN_PROGRESS) { + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = true; + this->update_info_.progress = progress; + this->publish_state(); + } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + this->status_set_error(LOG_STR("Failed to install firmware")); + this->publish_state(); + } +} +#endif + void HttpRequestUpdate::update() { #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index e05fdb0cc2..937f6dbeb9 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/components/http_request/http_request.h" @@ -14,7 +15,11 @@ namespace esphome { namespace http_request { -class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { +#ifdef USE_OTA_STATE_LISTENER +class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { +#else +class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent { +#endif public: void setup() override; void update() override; @@ -29,6 +34,10 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } +#ifdef USE_OTA_STATE_LISTENER + void on_ota_state(ota::OTAState state, float progress, uint8_t error) override; +#endif + protected: HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 575fb97799..0d478f749b 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, socket +from esphome.components import esp32, microphone, ota, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -452,7 +452,7 @@ async def to_code(config): cg.add(var.set_microphone_source(mic_source)) cg.add_define("USE_MICRO_WAKE_WORD") - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index eec39668db..387a307ab7 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -13,6 +13,8 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +OTA_STATE_LISTENER_KEY = "ota_state_listener" + CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -86,6 +88,7 @@ BASE_OTA_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config): cg.add_define("USE_OTA") + CORE.add_job(final_step) if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) @@ -122,7 +125,24 @@ async def ota_to_code(var, config): await automation.build_automation(trigger, [(cg.uint8, "x")], conf) use_state_callback = True if use_state_callback: - cg.add_define("USE_OTA_STATE_CALLBACK") + request_ota_state_listeners() + + +def request_ota_state_listeners() -> None: + """Request that OTA state listeners be compiled in. + + Components that need to be notified about OTA state changes (start, progress, + complete, error) should call this function during their code generation. + This enables the add_state_listener() API on OTAComponent. + """ + CORE.data[OTA_STATE_LISTENER_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional OTA features.""" + if CORE.data.get(OTA_STATE_LISTENER_KEY, False): + cg.add_define("USE_OTA_STATE_LISTENER") FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 7e1a60f3ce..520cb293f0 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER #include "ota_backend.h" #include "esphome/core/automation.h" @@ -7,69 +7,65 @@ namespace esphome { namespace ota { -class OTAStateChangeTrigger : public Trigger { +class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStateChangeTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (!parent->is_failed()) { - trigger(state); - } - }); + explicit OTAStateChangeTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } +}; + +class OTAStartTrigger final : public Trigger<>, public OTAStateListener { + public: + explicit OTAStartTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_STARTED) { + this->trigger(); + } } }; -class OTAStartTrigger : public Trigger<> { +class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStartTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_STARTED && !parent->is_failed()) { - trigger(); - } - }); + explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_IN_PROGRESS) { + this->trigger(progress); + } } }; -class OTAProgressTrigger : public Trigger { +class OTAEndTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAProgressTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_IN_PROGRESS && !parent->is_failed()) { - trigger(progress); - } - }); + explicit OTAEndTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_COMPLETED) { + this->trigger(); + } } }; -class OTAEndTrigger : public Trigger<> { +class OTAAbortTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAEndTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_COMPLETED && !parent->is_failed()) { - trigger(); - } - }); + explicit OTAAbortTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_ABORT) { + this->trigger(); + } } }; -class OTAAbortTrigger : public Trigger<> { +class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAAbortTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ABORT && !parent->is_failed()) { - trigger(); - } - }); - } -}; + explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } -class OTAErrorTrigger : public Trigger { - public: - explicit OTAErrorTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ERROR && !parent->is_failed()) { - trigger(error); - } - }); + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_ERROR) { + this->trigger(error); + } } }; diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 30de4ec4b3..5f510b4f8b 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -3,7 +3,7 @@ namespace esphome { namespace ota { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) OTAGlobalCallback *get_global_ota_callback() { @@ -14,6 +14,10 @@ OTAGlobalCallback *get_global_ota_callback() { } void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } + +void OTAComponentBridge::on_ota_state(OTAState state, float progress, uint8_t error) { + this->global_callback_->notify_global_listeners(state, progress, error, this->component_); +} #endif } // namespace ota diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 64ee0b9f7c..e474316bb5 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,9 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#ifdef USE_OTA_STATE_CALLBACK -#include "esphome/core/automation.h" -#endif +#include namespace esphome { namespace ota { @@ -60,62 +58,98 @@ class OTABackend { virtual bool supports_compression() = 0; }; -class OTAComponent : public Component { -#ifdef USE_OTA_STATE_CALLBACK +/** Listener interface for OTA state changes. + * + * Components can implement this interface to receive OTA state updates + * without the overhead of std::function callbacks. + */ +class OTAStateListener { public: - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); - } + virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0; +}; + +class OTAComponent : public Component { +#ifdef USE_OTA_STATE_LISTENER + public: + void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); } protected: - /** Extended callback manager with deferred call support. - * - * This adds a call_deferred() method for thread-safe execution from other tasks. - */ - class StateCallbackManager : public CallbackManager { - public: - StateCallbackManager(OTAComponent *component) : component_(component) {} - - /** Call callbacks with deferral to main loop (for thread safety). - * - * This should be used by OTA implementations that run in separate tasks - * (like web_server OTA) to ensure callbacks execute in the main loop. - */ - void call_deferred(ota::OTAState state, float progress, uint8_t error) { - component_->defer([this, state, progress, error]() { this->call(state, progress, error); }); + void notify_state_(OTAState state, float progress, uint8_t error) { + for (auto *listener : this->state_listeners_) { + listener->on_ota_state(state, progress, error); } + } - private: - OTAComponent *component_; - }; + /** Notify state with deferral to main loop (for thread safety). + * + * This should be used by OTA implementations that run in separate tasks + * (like web_server OTA) to ensure listeners execute in the main loop. + */ + void notify_state_deferred_(OTAState state, float progress, uint8_t error) { + this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); }); + } - StateCallbackManager state_callback_{this}; + std::vector state_listeners_; #endif }; -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER +class OTAGlobalCallback; + +/** Listener interface for global OTA state changes (includes OTA component pointer). + * + * Used by OTAGlobalCallback to aggregate state from multiple OTA components. + */ +class OTAGlobalStateListener { + public: + virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; +}; + +/** Helper class to bridge per-component OTA state to global listeners. + * + * Each OTA component gets one of these registered as a listener. When that + * component fires state events, this bridge forwards them to all global listeners + * along with the component pointer. + */ +class OTAComponentBridge : public OTAStateListener { + public: + OTAComponentBridge(OTAGlobalCallback *global_callback, OTAComponent *component) + : global_callback_(global_callback), component_(component) {} + + void on_ota_state(OTAState state, float progress, uint8_t error) override; + + private: + OTAGlobalCallback *global_callback_; + OTAComponent *component_; +}; + class OTAGlobalCallback { public: void register_ota(OTAComponent *ota_caller) { - ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) { - this->state_callback_.call(state, progress, error, ota_caller); - }); + // Create a bridge that forwards this component's events to global listeners + auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) + ota_caller->add_state_listener(bridge); } - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + + void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); } + + void notify_global_listeners(OTAState state, float progress, uint8_t error, OTAComponent *component) { + for (auto *listener : this->global_listeners_) { + listener->on_ota_global_state(state, progress, error, component); + } } protected: - CallbackManager state_callback_{}; + std::vector global_listeners_; }; OTAGlobalCallback *get_global_ota_callback(); void register_ota_platform(OTAComponent *ota_caller); // OTA implementations should use: -// - state_callback_.call() when already in main loop (e.g., esphome OTA) -// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA) -// This ensures proper callback execution in all contexts. +// - notify_state_() when already in main loop (e.g., esphome OTA) +// - notify_state_deferred_() when in separate task (e.g., web_server OTA) +// This ensures proper listener execution in all contexts. #endif std::unique_ptr make_ota_backend(); diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 062bff92f8..4ca57f2c4a 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, network, psram, speaker +from esphome.components import audio, esp32, media_player, network, ota, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -342,7 +342,7 @@ async def to_code(config): var = await media_player.new_media_player(config) await cg.register_component(var, config) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7929f3647f..30c4a59b8b 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -84,9 +84,9 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { } else { ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } -#ifdef USE_OTA_STATE_CALLBACK - // Report progress - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report progress - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0); #endif this->last_ota_progress_ = now; } @@ -114,9 +114,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Initialize OTA on first call this->ota_init_(filename.c_str()); -#ifdef USE_OTA_STATE_CALLBACK - // Notify OTA started - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Notify OTA started - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0); #endif // Platform-specific pre-initialization @@ -134,9 +134,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf this->ota_backend_ = ota::make_ota_backend(); if (!this->ota_backend_) { ESP_LOGE(TAG, "Failed to create OTA backend"); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, - static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, + static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); #endif return; } @@ -148,8 +148,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGE(TAG, "OTA begin failed: %d", error_code); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -166,8 +166,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf ESP_LOGE(TAG, "OTA write failed: %d", error_code); this->ota_backend_->abort(); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -186,15 +186,15 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf error_code = this->ota_backend_->end(); if (error_code == ota::OTA_RESPONSE_OK) { this->ota_success_ = true; -#ifdef USE_OTA_STATE_CALLBACK - // Report completion before reboot - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report completion before reboot - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0); #endif this->schedule_ota_reboot_(); } else { ESP_LOGE(TAG, "OTA end failed: %d", error_code); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } this->ota_backend_.reset(); @@ -232,7 +232,7 @@ void WebServerOTAComponent::setup() { // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed base->add_handler(new OTARequestHandler(this)); // NOLINT -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER // Register with global OTA callback system ota::register_ota_platform(this); #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad96..82963c767d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -139,7 +139,7 @@ #define USE_OTA_PASSWORD #define USE_OTA_SHA256 #define ALLOW_OTA_DOWNGRADE_MD5 -#define USE_OTA_STATE_CALLBACK +#define USE_OTA_STATE_LISTENER #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE #define USE_WIFI From 515cdf9b9fadaa8bf8e13f9af633fe386323572c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:48:19 -0600 Subject: [PATCH 03/10] its always on --- .../http_request/update/http_request_update.cpp | 8 +------- .../components/http_request/update/http_request_update.h | 6 ------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index ca4afa67d4..cf0fca06e7 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -20,13 +20,8 @@ static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; -void HttpRequestUpdate::setup() { -#ifdef USE_OTA_STATE_LISTENER - this->ota_parent_->add_state_listener(this); -#endif -} +void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } -#ifdef USE_OTA_STATE_LISTENER void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { this->state_ = update::UPDATE_STATE_INSTALLING; @@ -39,7 +34,6 @@ void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_ this->publish_state(); } } -#endif void HttpRequestUpdate::update() { #ifdef USE_ESP32 diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 937f6dbeb9..197a1b5e1c 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -15,11 +15,7 @@ namespace esphome { namespace http_request { -#ifdef USE_OTA_STATE_LISTENER class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { -#else -class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent { -#endif public: void setup() override; void update() override; @@ -34,9 +30,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } -#ifdef USE_OTA_STATE_LISTENER void on_ota_state(ota::OTAState state, float progress, uint8_t error) override; -#endif protected: HttpRequestComponent *request_parent_; From a224d0acbdcf92733e16130905bbac5a8a0a3e0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:50:20 -0600 Subject: [PATCH 04/10] dry --- .../http_request/update/http_request_update.h | 1 - esphome/components/ota/automation.h | 32 ++++--------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 197a1b5e1c..cf34ace18e 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -1,7 +1,6 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/components/http_request/http_request.h" diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 520cb293f0..ded8378ff2 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -14,17 +14,21 @@ class OTAStateChangeTrigger final : public Trigger, public OTAStateLis void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } }; -class OTAStartTrigger final : public Trigger<>, public OTAStateListener { +template class OTAStateTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAStartTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateTrigger(OTAComponent *parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_STARTED) { + if (state == State) { this->trigger(); } } }; +using OTAStartTrigger = OTAStateTrigger; +using OTAEndTrigger = OTAStateTrigger; +using OTAAbortTrigger = OTAStateTrigger; + class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } @@ -36,28 +40,6 @@ class OTAProgressTrigger final : public Trigger, public OTAStateListener } }; -class OTAEndTrigger final : public Trigger<>, public OTAStateListener { - public: - explicit OTAEndTrigger(OTAComponent *parent) { parent->add_state_listener(this); } - - void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_COMPLETED) { - this->trigger(); - } - } -}; - -class OTAAbortTrigger final : public Trigger<>, public OTAStateListener { - public: - explicit OTAAbortTrigger(OTAComponent *parent) { parent->add_state_listener(this); } - - void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_ABORT) { - this->trigger(); - } - } -}; - class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } From d9701af9c11e3992ce3ebade327cc723ef039701 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:51:21 -0600 Subject: [PATCH 05/10] dry --- esphome/components/ota/ota_backend.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index e474316bb5..5316411ba0 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,7 +4,9 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#ifdef USE_OTA_STATE_LISTENER #include +#endif namespace esphome { namespace ota { From ab6b4c77d2409be180140cb4639f854adc086081 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:52:58 -0600 Subject: [PATCH 06/10] dry --- esphome/components/ota/automation.h | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index ded8378ff2..92c0050ba0 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -9,20 +9,30 @@ namespace ota { class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStateChangeTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } - void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (!this->parent_->is_failed()) { + this->trigger(state); + } + } + + protected: + OTAComponent *parent_; }; template class OTAStateTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAStateTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == State) { + if (state == State && !this->parent_->is_failed()) { this->trigger(); } } + + protected: + OTAComponent *parent_; }; using OTAStartTrigger = OTAStateTrigger; @@ -31,24 +41,30 @@ using OTAAbortTrigger = OTAStateTrigger; class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_IN_PROGRESS) { + if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) { this->trigger(progress); } } + + protected: + OTAComponent *parent_; }; class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_ERROR) { + if (state == OTA_ERROR && !this->parent_->is_failed()) { this->trigger(error); } } + + protected: + OTAComponent *parent_; }; } // namespace ota From ee91bb2405663606eb2fcd1d810c3dca6a41b95d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:54:46 -0600 Subject: [PATCH 07/10] dry --- esphome/components/ota/ota_backend.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 5316411ba0..64fbbcfda7 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -128,7 +128,8 @@ class OTAComponentBridge : public OTAStateListener { class OTAGlobalCallback { public: void register_ota(OTAComponent *ota_caller) { - // Create a bridge that forwards this component's events to global listeners + // Create a bridge that forwards this component's events to global listeners. + // Intentionally never deleted - these objects live for the lifetime of the device. auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) ota_caller->add_state_listener(bridge); } From a45a2e8f5fc86da63035d6e7aa0b7ebd0a8dc852 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:01:37 -0600 Subject: [PATCH 08/10] guards --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 27 ++++++------ .../esp32_ble_tracker/esp32_ble_tracker.h | 11 +++++ .../micro_wake_word/micro_wake_word.cpp | 21 ++++++---- .../micro_wake_word/micro_wake_word.h | 16 ++++++- .../media_player/speaker_media_player.cpp | 42 ++++++++++--------- .../media_player/speaker_media_player.h | 18 +++++++- 6 files changed, 92 insertions(+), 43 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d3c5edfb94..542b076f40 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -71,21 +71,24 @@ void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->stop_scan(); -#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT - for (auto *client : this->clients_) { - client->disconnect(); - } -#endif - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->stop_scan(); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->disconnect(); + } +#endif + } +} +#endif + void ESP32BLETracker::loop() { if (!this->parent_->is_active()) { this->ble_was_disabled_ = true; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 92d13a62ad..b64e36279c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -22,6 +22,10 @@ #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_scan_result.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + namespace esphome::esp32_ble_tracker { using namespace esp32_ble; @@ -241,6 +245,9 @@ class ESP32BLETracker : public Component, public GAPScanEventHandler, public GATTcEventHandler, public BLEStatusEventHandler, +#ifdef USE_OTA_STATE_LISTENER + public ota::OTAGlobalStateListener, +#endif public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -274,6 +281,10 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + /// Add a listener for scanner state changes void add_scanner_state_listener(BLEScannerStateListener *listener) { this->scanner_state_listeners_.push_back(listener); diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index a0547b158e..0f72f188bc 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -119,18 +119,21 @@ void MicroWakeWord::setup() { } }); -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->suspend_task_(); - } else if (state == ota::OTA_ERROR) { - this->resume_task_(); - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->suspend_task_(); + } else if (state == ota::OTA_ERROR) { + this->resume_task_(); + } +} +#endif + void MicroWakeWord::inference_task(void *params) { MicroWakeWord *this_mww = (MicroWakeWord *) params; diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index d46c40e48b..84261eaa5b 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -9,8 +9,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/ring_buffer.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include @@ -26,13 +31,22 @@ enum State { STOPPED, }; -class MicroWakeWord : public Component { +class MicroWakeWord : public Component +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: void setup() override; void loop() override; float get_setup_priority() const override; void dump_config() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + void start(); void stop(); diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index b45a78010a..5722aab195 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -66,25 +66,8 @@ void SpeakerMediaPlayer::setup() { this->set_mute_state_(false); } -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->suspend_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->suspend_tasks(); - } - } else if (state == ota::OTA_ERROR) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->resume_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->resume_tasks(); - } - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif this->announcement_pipeline_ = @@ -300,6 +283,27 @@ void SpeakerMediaPlayer::watch_media_commands_() { } } +#ifdef USE_OTA_STATE_LISTENER +void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, + ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->suspend_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->suspend_tasks(); + } + } else if (state == ota::OTA_ERROR) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->resume_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->resume_tasks(); + } + } +} +#endif + void SpeakerMediaPlayer::loop() { this->watch_media_commands_(); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 967772d1a5..f1c564b63d 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -5,14 +5,18 @@ #include "audio_pipeline.h" #include "esphome/components/audio/audio.h" - #include "esphome/components/media_player/media_player.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/preferences.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include #include @@ -39,12 +43,22 @@ struct VolumeRestoreState { bool is_muted; }; -class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { +class SpeakerMediaPlayer : public Component, + public media_player::MediaPlayer +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; void loop() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + // MediaPlayer implementations media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->is_muted_; } From b1a318c0d75682c2a5f7ad910868bdfae1f23e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:07:55 -0600 Subject: [PATCH 09/10] simplify, no more register needed --- .../components/esphome/ota/ota_esphome.cpp | 4 --- .../http_request/ota/ota_http_request.cpp | 6 +--- esphome/components/ota/ota_backend.cpp | 9 ++--- esphome/components/ota/ota_backend.h | 36 +++---------------- .../web_server/ota/ota_web_server.cpp | 4 --- 5 files changed, 11 insertions(+), 48 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 469c57211c..521b3de15a 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -42,10 +42,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 #endif // USE_OTA_PASSWORD void ESPHomeOTAComponent::setup() { -#ifdef USE_OTA_STATE_LISTENER - ota::register_ota_platform(this); -#endif - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { this->log_socket_error_(LOG_STR("creation")); diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 2a52a0e264..59bdeb9ceb 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -17,11 +17,7 @@ namespace http_request { static const char *const TAG = "http_request.ota"; -void OtaHttpRequestComponent::setup() { -#ifdef USE_OTA_STATE_LISTENER - ota::register_ota_platform(this); -#endif -} +void OtaHttpRequestComponent::setup() {} void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); }; diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 5f510b4f8b..8fb9f67214 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -13,10 +13,11 @@ OTAGlobalCallback *get_global_ota_callback() { return global_ota_callback; } -void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } - -void OTAComponentBridge::on_ota_state(OTAState state, float progress, uint8_t error) { - this->global_callback_->notify_global_listeners(state, progress, error, this->component_); +void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) { + for (auto *listener : this->state_listeners_) { + listener->on_ota_state(state, progress, error); + } + get_global_ota_callback()->notify_ota_state(state, progress, error, this); } #endif diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 64fbbcfda7..c00ecba9e6 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -76,11 +76,7 @@ class OTAComponent : public Component { void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); } protected: - void notify_state_(OTAState state, float progress, uint8_t error) { - for (auto *listener : this->state_listeners_) { - listener->on_ota_state(state, progress, error); - } - } + void notify_state_(OTAState state, float progress, uint8_t error); /** Notify state with deferral to main loop (for thread safety). * @@ -96,7 +92,6 @@ class OTAComponent : public Component { }; #ifdef USE_OTA_STATE_LISTENER -class OTAGlobalCallback; /** Listener interface for global OTA state changes (includes OTA component pointer). * @@ -107,36 +102,16 @@ class OTAGlobalStateListener { virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; }; -/** Helper class to bridge per-component OTA state to global listeners. +/** Global callback that aggregates OTA state from all OTA components. * - * Each OTA component gets one of these registered as a listener. When that - * component fires state events, this bridge forwards them to all global listeners - * along with the component pointer. + * OTA components call notify_ota_state() directly with their pointer, + * which forwards the event to all registered global listeners. */ -class OTAComponentBridge : public OTAStateListener { - public: - OTAComponentBridge(OTAGlobalCallback *global_callback, OTAComponent *component) - : global_callback_(global_callback), component_(component) {} - - void on_ota_state(OTAState state, float progress, uint8_t error) override; - - private: - OTAGlobalCallback *global_callback_; - OTAComponent *component_; -}; - class OTAGlobalCallback { public: - void register_ota(OTAComponent *ota_caller) { - // Create a bridge that forwards this component's events to global listeners. - // Intentionally never deleted - these objects live for the lifetime of the device. - auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) - ota_caller->add_state_listener(bridge); - } - void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); } - void notify_global_listeners(OTAState state, float progress, uint8_t error, OTAComponent *component) { + void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) { for (auto *listener : this->global_listeners_) { listener->on_ota_global_state(state, progress, error, component); } @@ -147,7 +122,6 @@ class OTAGlobalCallback { }; OTAGlobalCallback *get_global_ota_callback(); -void register_ota_platform(OTAComponent *ota_caller); // OTA implementations should use: // - notify_state_() when already in main loop (e.g., esphome OTA) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 30c4a59b8b..f612aa056c 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -232,10 +232,6 @@ void WebServerOTAComponent::setup() { // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed base->add_handler(new OTARequestHandler(this)); // NOLINT -#ifdef USE_OTA_STATE_LISTENER - // Register with global OTA callback system - ota::register_ota_platform(this); -#endif } void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } From e3dc9a715fc39a27c10989728a83223077f0f2fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:14:03 -0600 Subject: [PATCH 10/10] tweak --- esphome/components/ota/ota_backend.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index c00ecba9e6..e03afd4fc6 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -67,6 +67,7 @@ class OTABackend { */ class OTAStateListener { public: + virtual ~OTAStateListener() = default; virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0; }; @@ -99,6 +100,7 @@ class OTAComponent : public Component { */ class OTAGlobalStateListener { public: + virtual ~OTAGlobalStateListener() = default; virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; };