From b9f208b63ada31df0da0cfbc70ebeaf075aedaf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 10:16:18 -0600 Subject: [PATCH 1/9] [api][event] Send events immediately to prevent loss during rapid triggers --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47b87d5ea3..a4dbcef57e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1295,8 +1295,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a77c93a2d5..78161442c1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, std::move(creator), message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); From 65fd784fa798c0e8b948a38fe4a3c76619783419 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 10:39:09 -0600 Subject: [PATCH 2/9] tidy --- esphome/components/api/api_connection.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 78161442c1..6cfd108927 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -700,7 +700,7 @@ class APIConnection final : public APIServerConnection { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { #ifdef HAS_PROTO_MESSAGE_DUMP // Log the message in verbose mode - this->log_proto_message_(entity, std::move(creator), message_type); + this->log_proto_message_(entity, creator, message_type); #endif return true; } From 5d613ada8319e95d0d233beccdd322745d42e176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:00:50 -0600 Subject: [PATCH 3/9] Bump pytest from 9.0.0 to 9.0.1 (#11874) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 35010ad52f..5c7cccaf25 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==9.0.0 +pytest==9.0.1 pytest-cov==7.0.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 From 3872a2fd919fb8b33fe0829001d0dce50a54001d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:07 -0600 Subject: [PATCH 4/9] [captive_portal] Warn when enabled without WiFi AP configured (#11856) --- esphome/components/captive_portal/__init__.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf..9bd3ef8a05 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) From 4b58cb4ce61cb07a3b8e1d97c7274e82e98b8db5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:19 -0600 Subject: [PATCH 5/9] [wifi] Pass ManualIP by const reference to reduce stack usage (#11858) --- esphome/components/network/ip_address.h | 8 ++++---- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 4 ++-- esphome/components/wifi/wifi_component_libretiny.cpp | 4 ++-- esphome/components/wifi/wifi_component_pico_w.cpp | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd96..5ec6450cce 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -118,10 +118,10 @@ struct IPAddress { operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } #endif - bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) - bool is_ip4() { return IP_IS_V4(&ip_addr_); } - bool is_ip6() { return IP_IS_V6(&ip_addr_); } - bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } + bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) + bool is_ip4() const { return IP_IS_V4(&ip_addr_); } + bool is_ip6() const { return IP_IS_V6(&ip_addr_); } + bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 713e6f223f..d37367b88c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,7 +426,7 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); - bool wifi_sta_ip_config_(optional manual_ip); + bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); @@ -434,7 +434,7 @@ class WiFiComponent : public Component { bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP - bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7..b787446a39 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t }; #endif -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -730,7 +730,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6b..824adb5cf5 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return true; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -884,7 +884,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esp_err_t err; // enable AP diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e831..eea7a7e933 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return true; } bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -434,7 +434,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7025ba16bd..54f03f803d 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { if (!manual_ip.has_value()) { return true; } @@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esphome::network::IPAddress ip_address, gateway, subnet, dns; if (manual_ip.has_value()) { ip_address = manual_ip->static_ip; From 5a550cc579bee3f7c07ef7c9ccdc707ac776d836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:36 -0600 Subject: [PATCH 6/9] [api] Eliminate heap allocations when transmitting Event types (#11773) --- esphome/components/api/api_connection.cpp | 14 +++---- esphome/components/api/api_connection.h | 48 ++++------------------- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7eb61f08b6..ca9ddaedf4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { +void APIConnection::send_event(event::Event *event, const char *event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1650,9 +1650,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1822,7 +1820,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); @@ -1835,10 +1833,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284fa11a95..a77c93a2d5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - - // No destructor - cleanup must be called explicitly with message_type + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; @@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif - } - private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; @@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } From 2f39b10baa9348c68d54413ad60177a6d0050995 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:46 -0600 Subject: [PATCH 7/9] [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) --- esphome/components/esp32_ble_tracker/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 054cbaa7df..bbf7992fa4 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } + void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { uint64_t u64_addr = device.address_uint64(); From 5f0fa68d732cd672fc83a249148bcde130bb812a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:57 -0600 Subject: [PATCH 8/9] [esp32_ble] Use stack allocation for MAC formatting in dump_config (#11860) --- esphome/components/esp32_ble/ble.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8bbb21e3ca..d0bfb6f843 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -634,11 +634,13 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; + format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + mac_s, io_capability_s); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } From 29a50da6355b2ad936dc78a19efb4cc4eeedc57e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:27:06 -0600 Subject: [PATCH 9/9] [wifi] Use stack allocation for BSSID formatting in logging (#11859) --- esphome/components/wifi/wifi_component.cpp | 24 +++++++++++-------- .../wifi/wifi_component_esp8266.cpp | 6 +++-- .../wifi/wifi_component_esp_idf.cpp | 6 +++-- .../wifi/wifi_component_libretiny.cpp | 6 +++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f..e33cd7cf2d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -668,25 +668,25 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority - std::string bssid_formatted; + char bssid_s[18]; int8_t priority = 0; if (ap.get_bssid().has_value()) { - bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); + format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); priority = this->get_sta_priority(ap.get_bssid().value()); } ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), - priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, + this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); + ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -787,6 +787,8 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); if (this->is_disabled()) { @@ -809,9 +811,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), - wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), + wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); @@ -1390,8 +1392,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), - format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b787446a39..78f336ab15 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -525,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 824adb5cf5..df29565554 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -746,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index eea7a7e933..7f0c35c8c8 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -299,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason;