From 44037c4f9ba5d81b68fc0f5e367bc08821f52337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 14:06:41 -1000 Subject: [PATCH 1/4] [http_request] Prevent double update task launch (#14910) --- .../components/http_request/update/http_request_update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index a15dc61675..1c52a28105 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -74,6 +74,10 @@ void HttpRequestUpdate::update() { } this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 + if (this->update_task_handle_ != nullptr) { + ESP_LOGW(TAG, "Update check already in progress"); + return; + } xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else this->update_task(this); @@ -204,6 +208,9 @@ defer: // both success and error paths to avoid multiple std::function instantiations. // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. this_update->defer([this_update, result]() { +#ifdef USE_ESP32 + this_update->update_task_handle_ = nullptr; +#endif if (result->error_str != nullptr) { this_update->status_set_error(result->error_str); delete result; From 4d86049c217b5e170ffba1cfe75847526c25df70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 14:06:55 -1000 Subject: [PATCH 2/4] [ota] Pack deferred state args into uint32 to avoid heap allocation (#14922) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/ota/ota_backend.cpp | 13 +++++++++++++ esphome/components/ota/ota_backend.h | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 8fb9f67214..01a18a58ef 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -13,6 +13,19 @@ OTAGlobalCallback *get_global_ota_callback() { return global_ota_callback; } +void OTAComponent::notify_state_deferred_(OTAState state, float progress, uint8_t error) { + // Pack state, error, and progress into a single uint32_t so the lambda + // captures only [this, packed] (8 bytes) — fits in std::function SBO. + // Layout: [state:8][error:8][progress_fixed:16] where progress is 0–10000 (0.01% resolution) + static_assert(OTA_ERROR <= 0xFF, "OTAState must fit in 8 bits for packing"); + uint32_t packed = (static_cast(state) << 24) | (static_cast(error) << 16) | + static_cast(progress * 100.0f); + this->defer([this, packed]() { + this->notify_state_(static_cast(packed >> 24), static_cast(packed & 0xFFFF) / 100.0f, + static_cast(packed >> 16)); + }); +} + void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) { for (auto *listener : this->state_listeners_) { listener->on_ota_state(state, progress, error); diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index bc603a6e9e..ab0ec58e8a 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -73,9 +73,7 @@ class OTAComponent : public Component { * 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); }); - } + void notify_state_deferred_(OTAState state, float progress, uint8_t error); std::vector state_listeners_; #endif From a50d70c8d3ea39b27681bfe6e8f5a62519dfe760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 14:08:03 -1000 Subject: [PATCH 3/4] [core] Remove call_loop_ wrapper and call loop() directly (#14931) --- esphome/core/component.cpp | 5 ++--- esphome/core/component.h | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9e93f9131..00dda0cc26 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -209,7 +209,6 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config_() { this->dump_config(); @@ -259,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop_(); + this->loop(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop_(); + this->loop(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing diff --git a/esphome/core/component.h b/esphome/core/component.h index 02598b53ff..119681f64c 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -301,7 +301,6 @@ class Component { protected: friend class Application; - void call_loop_(); virtual void call_setup(); void call_dump_config_(); From f8be27ce6d07d42e13dbbce08d5f6252ea4a2acf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:42 -0400 Subject: [PATCH 4/4] [ble_client] Fix RSSI sensor reporting same value for all clients (#14939) Co-authored-by: Claude Opus 4.6 (1M context) --- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index dc032a7a98..715298e592 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl switch (event) { // server response on RSSI request: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr)) + return; if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);