From b44d2183aa3790b54be9282248226b3b4ff8eb82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:19:12 +0000 Subject: [PATCH 01/17] Bump aioesphomeapi from 37.2.3 to 37.2.4 (#10050) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bfdb08323e..6f79e86bdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.2.3 +aioesphomeapi==37.2.4 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From dbaf2cdd5030de58b495236e940e00d2f6a13feb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 15:46:06 -1000 Subject: [PATCH 02/17] [core] Replace std::find and std::max_element with simple loops to reduce binary size (#10044) --- esphome/core/application.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3ac17849dd..0467b0b57f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -459,24 +459,25 @@ void Application::unregister_socket_fd(int fd) { if (fd < 0) return; - auto it = std::find(this->socket_fds_.begin(), this->socket_fds_.end(), fd); - if (it != this->socket_fds_.end()) { + for (size_t i = 0; i < this->socket_fds_.size(); i++) { + if (this->socket_fds_[i] != fd) + continue; + // Swap with last element and pop - O(1) removal since order doesn't matter - if (it != this->socket_fds_.end() - 1) { - std::swap(*it, this->socket_fds_.back()); - } + if (i < this->socket_fds_.size() - 1) + this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { - if (this->socket_fds_.empty()) { - this->max_fd_ = -1; - } else { - // Find new max using std::max_element - this->max_fd_ = *std::max_element(this->socket_fds_.begin(), this->socket_fds_.end()); + this->max_fd_ = -1; + for (int sock_fd : this->socket_fds_) { + if (sock_fd > this->max_fd_) + this->max_fd_ = sock_fd; } } + return; } } From d86e1e29a9af9063429d25629cf872d1a682d9b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 15:51:50 -1000 Subject: [PATCH 03/17] [core] Convert components, devices, and areas vectors to static allocation (#10020) --- esphome/core/application.h | 100 +++++++++++++++++-------------------- esphome/core/config.py | 12 ++--- esphome/core/defines.h | 5 +- esphome/core/helpers.h | 10 ++++ 4 files changed, 66 insertions(+), 61 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index b7824a254b..4eb4984f71 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -214,14 +214,6 @@ class Application { #endif /// Reserve space for components to avoid memory fragmentation - void reserve_components(size_t count) { this->components_.reserve(count); } - -#ifdef USE_AREAS - void reserve_area(size_t count) { this->areas_.reserve(count); } -#endif -#ifdef USE_DEVICES - void reserve_device(size_t count) { this->devices_.reserve(count); } -#endif /// Register the component in this Application instance. template C *register_component(C *c) { @@ -316,7 +308,7 @@ class Application { } \ return nullptr; \ } - const std::vector &get_devices() { return this->devices_; } + const auto &get_devices() { return this->devices_; } #else #define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \ @@ -328,7 +320,7 @@ class Application { } #endif // USE_DEVICES #ifdef USE_AREAS - const std::vector &get_areas() { return this->areas_; } + const auto &get_areas() { return this->areas_; } #endif #ifdef USE_BINARY_SENSOR auto &get_binary_sensors() const { return this->binary_sensors_; } @@ -462,12 +454,7 @@ class Application { const char *comment_{nullptr}; const char *compilation_time_{nullptr}; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - - // Vectors (largest members) - std::vector components_{}; - + // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components // ================================================= // Components are partitioned into [active | inactive] sections: @@ -485,12 +472,54 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop std::vector looping_components_{}; +#ifdef USE_SOCKET_SELECT_SUPPORT + std::vector socket_fds_; // Vector of all monitored socket file descriptors +#endif + + // std::string members (typically 24-32 bytes each) + std::string name_; + std::string friendly_name_; + + // size_t members + size_t dump_config_at_{SIZE_MAX}; + + // 4-byte members + uint32_t last_loop_{0}; + uint32_t loop_component_start_time_{0}; + +#ifdef USE_SOCKET_SELECT_SUPPORT + int max_fd_{-1}; // Highest file descriptor number for select() +#endif + + // 2-byte members (grouped together for alignment) + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ + uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration + + // 1-byte members (grouped together to minimize padding) + uint8_t app_state_{0}; + bool name_add_mac_suffix_; + bool in_loop_{false}; + volatile bool has_pending_enable_loop_requests_{false}; + +#ifdef USE_SOCKET_SELECT_SUPPORT + bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes +#endif + +#ifdef USE_SOCKET_SELECT_SUPPORT + // Variable-sized members + fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes + fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ +#endif + + // StaticVectors (largest members - contain actual array data inline) + StaticVector components_{}; #ifdef USE_DEVICES - std::vector devices_{}; + StaticVector devices_{}; #endif #ifdef USE_AREAS - std::vector areas_{}; + StaticVector areas_{}; #endif #ifdef USE_BINARY_SENSOR StaticVector binary_sensors_{}; @@ -556,41 +585,6 @@ class Application { #ifdef USE_UPDATE StaticVector updates_{}; #endif - -#ifdef USE_SOCKET_SELECT_SUPPORT - std::vector socket_fds_; // Vector of all monitored socket file descriptors -#endif - - // String members - std::string name_; - std::string friendly_name_; - - // 4-byte members - uint32_t last_loop_{0}; - uint32_t loop_component_start_time_{0}; - -#ifdef USE_SOCKET_SELECT_SUPPORT - int max_fd_{-1}; // Highest file descriptor number for select() -#endif - - // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) - uint16_t looping_components_active_end_{0}; - uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration - - // 1-byte members (grouped together to minimize padding) - uint8_t app_state_{0}; - bool name_add_mac_suffix_; - bool in_loop_{false}; - volatile bool has_pending_enable_loop_requests_{false}; - -#ifdef USE_SOCKET_SELECT_SUPPORT - bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes - - // Variable-sized members at end - fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes - fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ -#endif }; /// Global storage of Application pointer - only one Application can exist. diff --git a/esphome/core/config.py b/esphome/core/config.py index 6a87bab730..90768a4b09 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -459,10 +459,8 @@ async def to_code(config: ConfigType) -> None: config[CONF_NAME_ADD_MAC_SUFFIX], ) ) - # Reserve space for components to avoid reallocation during registration - cg.add( - cg.RawStatement(f"App.reserve_components({len(CORE.component_ids)});"), - ) + # Define component count for static allocation + cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) CORE.add_job(_add_platform_defines) @@ -531,8 +529,8 @@ async def to_code(config: ConfigType) -> None: all_areas.extend(config[CONF_AREAS]) if all_areas: - cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});")) cg.add_define("USE_AREAS") + cg.add_define("ESPHOME_AREA_COUNT", len(all_areas)) for area_conf in all_areas: area_id: core.ID = area_conf[CONF_ID] @@ -549,9 +547,9 @@ async def to_code(config: ConfigType) -> None: if not devices: return - # Reserve space for devices - cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});")) + # Define device count for static allocation cg.add_define("USE_DEVICES") + cg.add_define("ESPHOME_DEVICE_COUNT", len(devices)) # Process each device for dev_conf in devices: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 3ed0af91eb..996dbc7e8d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -240,7 +240,10 @@ #define USE_DASHBOARD_IMPORT -// Default entity counts for static analysis +// Default counts for static analysis +#define ESPHOME_COMPONENT_COUNT 50 +#define ESPHOME_DEVICE_COUNT 10 +#define ESPHOME_AREA_COUNT 10 #define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 #define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 #define ESPHOME_ENTITY_BUTTON_COUNT 1 diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b05cc11029..b5fe59c4fd 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,8 @@ template class StaticVector { using value_type = T; using iterator = typename std::array::iterator; using const_iterator = typename std::array::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; private: std::array data_{}; @@ -114,6 +117,7 @@ template class StaticVector { } size_t size() const { return count_; } + bool empty() const { return count_ == 0; } T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } @@ -123,6 +127,12 @@ template class StaticVector { iterator end() { return data_.begin() + count_; } const_iterator begin() const { return data_.begin(); } const_iterator end() const { return data_.begin() + count_; } + + // Reverse iterators + reverse_iterator rbegin() { return reverse_iterator(end()); } + reverse_iterator rend() { return reverse_iterator(begin()); } + const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } + const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } }; ///@} From cd6cf074d9b6fdcc9b06c7e309f8b1d2ac11dda2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 15:56:06 -1000 Subject: [PATCH 04/17] [core] Replace std::stable_sort with insertion sort to save 3.5KB flash (#10035) --- esphome/core/application.cpp | 48 ++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0467b0b57f..73bf13ab7c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -34,6 +34,44 @@ namespace esphome { static const char *const TAG = "app"; +// Helper function for insertion sort of components by setup priority +// Using insertion sort instead of std::stable_sort saves ~1.3KB of flash +// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) +// IMPORTANT: This sort is stable (preserves relative order of equal elements), +// which is necessary to maintain user-defined component order for same priority +template static void insertion_sort_by_setup_priority(Iterator first, Iterator last) { + for (auto it = first + 1; it != last; ++it) { + auto key = *it; + float key_priority = key->get_actual_setup_priority(); + auto j = it - 1; + + // Using '<' (not '<=') ensures stability - equal priority components keep their order + while (j >= first && (*j)->get_actual_setup_priority() < key_priority) { + *(j + 1) = *j; + j--; + } + *(j + 1) = key; + } +} + +// Helper function for insertion sort of components by loop priority +// IMPORTANT: This sort is stable (preserves relative order of equal elements), +// which is required when components are re-sorted during setup() if they block +template static void insertion_sort_by_loop_priority(Iterator first, Iterator last) { + for (auto it = first + 1; it != last; ++it) { + auto key = *it; + float key_priority = key->get_loop_priority(); + auto j = it - 1; + + // Using '<' (not '<=') ensures stability - equal priority components keep their order + while (j >= first && (*j)->get_loop_priority() < key_priority) { + *(j + 1) = *j; + j--; + } + *(j + 1) = key; + } +} + void Application::register_component_(Component *comp) { if (comp == nullptr) { ESP_LOGW(TAG, "Tried to register null component!"); @@ -51,9 +89,9 @@ void Application::register_component_(Component *comp) { void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); - std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) { - return a->get_actual_setup_priority() > b->get_actual_setup_priority(); - }); + + // Sort by setup priority using our helper function + insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end()); // Initialize looping_components_ early so enable_pending_loops_() works during setup this->calculate_looping_components_(); @@ -69,8 +107,8 @@ void Application::setup() { if (component->can_proceed()) continue; - std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1, - [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); + // Sort components 0 through i by loop priority + insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1); do { uint8_t new_app_state = STATUS_LED_WARNING; From 3fbbdb4589ef306aeac29ea672320860ad15821d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:00:56 -1000 Subject: [PATCH 05/17] [web_server_idf] Replace std::find_if with simple loop to reduce binary size (#10042) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/web_server_idf/web_server_idf.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 734259093e..483fae4f08 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -423,14 +423,14 @@ void AsyncEventSourceResponse::destroy(void *ptr) { void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); - auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), - [&item](const DeferredEvent &test) -> bool { return test == item; }); - - if (iter != this->deferred_queue_.end()) { - (*iter) = item; - } else { - this->deferred_queue_.push_back(item); + // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size + for (auto &event : this->deferred_queue_) { + if (event == item) { + event = item; + return; + } } + this->deferred_queue_.push_back(item); } void AsyncEventSourceResponse::process_deferred_queue_() { From c9d865a0610e5023dbd5a6d9b85068b580a3c3ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:02:10 -1000 Subject: [PATCH 06/17] [core] Optimize Application::pre_setup() to reduce duplicate MAC address operations (#10039) --- esphome/core/application.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 4eb4984f71..4120afff53 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -101,12 +101,9 @@ class Application { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { - this->name_ = name + "-" + get_mac_address().substr(6); - if (friendly_name.empty()) { - this->friendly_name_ = ""; - } else { - this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6); - } + const std::string mac_suffix = get_mac_address().substr(6); + this->name_ = name + "-" + mac_suffix; + this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix; } else { this->name_ = name; this->friendly_name_ = friendly_name; From a75f73dbf0efec8d2ab886e5dbabb2931721ba10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:03:37 -1000 Subject: [PATCH 07/17] [web_server] Reduce binary size by using EntityBase and minimizing template instantiations (#10033) --- esphome/components/web_server/web_server.cpp | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 880145a2a1..a8d94d80da 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -376,23 +376,32 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { } #endif -#define set_json_id(root, obj, sensor, start_config) \ - (root)["id"] = sensor; \ - if (((start_config) == DETAIL_ALL)) { \ - (root)["name"] = (obj)->get_name(); \ - (root)["icon"] = (obj)->get_icon(); \ - (root)["entity_category"] = (obj)->get_entity_category(); \ - if ((obj)->is_disabled_by_default()) \ - (root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \ +// Helper functions to reduce code size by avoiding macro expansion +static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { + root["id"] = id; + if (start_config == DETAIL_ALL) { + root["name"] = obj->get_name(); + root["icon"] = obj->get_icon(); + root["entity_category"] = obj->get_entity_category(); + bool is_disabled = obj->is_disabled_by_default(); + if (is_disabled) + root["is_disabled_by_default"] = is_disabled; } +} -#define set_json_value(root, obj, sensor, value, start_config) \ - set_json_id((root), (obj), sensor, start_config); \ - (root)["value"] = value; +template +static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, + JsonDetail start_config) { + set_json_id(root, obj, id, start_config); + root["value"] = value; +} -#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config); \ - (root)["state"] = state; +template +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, + const std::string &state, const T &value, JsonDetail start_config) { + set_json_value(root, obj, id, value, start_config); + root["state"] = state; +} // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { From 494a1a216c4b7ec77fb76745320d74ee32055bd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:09:12 -1000 Subject: [PATCH 08/17] [web_server] Conditionally compile authentication code to save flash memory (#10022) --- esphome/components/web_server/__init__.py | 1 + esphome/components/web_server_base/web_server_base.cpp | 2 ++ esphome/components/web_server_base/web_server_base.h | 6 ++++++ esphome/components/web_server_idf/web_server_idf.cpp | 2 ++ esphome/components/web_server_idf/web_server_idf.h | 2 ++ esphome/core/defines.h | 3 +++ tests/components/web_server/test.esp32-idf.yaml | 5 +++++ 7 files changed, 21 insertions(+) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8ead14dcac..695757e137 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -298,6 +298,7 @@ async def to_code(config): if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if CONF_AUTH in config: + cg.add_define("USE_WEBSERVER_AUTH") cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index e1c2bc0b25..6e7097338c 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -14,9 +14,11 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av void WebServerBase::add_handler(AsyncWebHandler *handler) { // remove all handlers +#ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } +#endif this->handlers_.push_back(handler); if (this->server_ != nullptr) { this->server_->addHandler(handler); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index a475238a37..cfca776ee1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -41,6 +41,7 @@ class MiddlewareHandler : public AsyncWebHandler { AsyncWebHandler *next_; }; +#ifdef USE_WEBSERVER_AUTH struct Credentials { std::string username; std::string password; @@ -79,6 +80,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler { protected: Credentials *credentials_; }; +#endif } // namespace internal @@ -108,8 +110,10 @@ class WebServerBase : public Component { std::shared_ptr get_server() const { return server_; } float get_setup_priority() const override; +#ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); } +#endif void add_handler(AsyncWebHandler *handler); @@ -121,7 +125,9 @@ class WebServerBase : public Component { uint16_t port_{80}; std::shared_ptr server_{nullptr}; std::vector handlers_; +#ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; +#endif }; } // namespace web_server_base diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 483fae4f08..40fb015b99 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -223,6 +223,7 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code this->rsp_ = rsp; } +#ifdef USE_WEBSERVER_AUTH bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { if (username == nullptr || password == nullptr || *username == 0) { return true; @@ -261,6 +262,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } +#endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { auto find = this->params_.find(name); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index e8e40ef9b0..76540ef232 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -115,9 +115,11 @@ class AsyncWebServerRequest { // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } +#ifdef USE_WEBSERVER_AUTH bool authenticate(const char *username, const char *password) const; // NOLINTNEXTLINE(readability-identifier-naming) void requestAuthentication(const char *realm = nullptr) const; +#endif void redirect(const std::string &url); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 996dbc7e8d..56de0127a6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -163,6 +163,7 @@ #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_OTA #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_SORTING @@ -210,6 +211,7 @@ {} #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT #endif @@ -226,6 +228,7 @@ #define USE_SOCKET_IMPL_LWIP_SOCKETS #define USE_SOCKET_SELECT_SUPPORT #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT #endif diff --git a/tests/components/web_server/test.esp32-idf.yaml b/tests/components/web_server/test.esp32-idf.yaml index 7e6658e20e..24b292d0d6 100644 --- a/tests/components/web_server/test.esp32-idf.yaml +++ b/tests/components/web_server/test.esp32-idf.yaml @@ -1 +1,6 @@ <<: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password From 9aad0733efa87a2e5c9bca42cbf48e899aefb76d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:14:17 -1000 Subject: [PATCH 09/17] [core] Update to esptool 5.0+ command syntax (#10011) --- esphome/__main__.py | 10 +++++----- esphome/components/esp32/post_build.py.script | 6 +++--- esphome/platformio_api.py | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 341c1fa893..5e45b7f213 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -277,20 +277,20 @@ def upload_using_esptool(config, port, file, speed): def run_esptool(baud_rate): cmd = [ - "esptool.py", + "esptool", "--before", - "default_reset", + "default-reset", "--after", - "hard_reset", + "hard-reset", "--baud", str(baud_rate), "--port", port, "--chip", mcu, - "write_flash", + "write-flash", "-z", - "--flash_size", + "--flash-size", "detect", ] for img in flash_images: diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 586f12e00b..c995214232 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -93,8 +93,8 @@ def merge_factory_bin(source, target, env): "esptool", "--chip", chip, - "merge_bin", - "--flash_size", + "merge-bin", + "--flash-size", flash_size, "--output", str(output_path), @@ -110,7 +110,7 @@ def merge_factory_bin(source, target, env): if result == 0: print(f"Successfully created {output_path}") else: - print(f"Error: esptool merge_bin failed with code {result}") + print(f"Error: esptool merge-bin failed with code {result}") def esp32_copy_ota_bin(source, target, env): diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 7415ec9794..21124fc859 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -61,6 +61,7 @@ FILTER_PLATFORMIO_LINES = [ r"Advanced Memory Usage is available via .*", r"Merged .* ELF section", r"esptool.py v.*", + r"esptool v.*", r"Checking size .*", r"Retrieving maximum program size .*", r"PLATFORM: .*", From ef372eeeb7f6b22bfa8fe7a42e387e56ac0ecaf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:19:24 -1000 Subject: [PATCH 10/17] [wifi] Replace std::stable_sort with insertion sort to save 2.4KB flash (#10037) --- esphome/components/wifi/wifi_component.cpp | 74 +++++++++++++++------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 98f75894f4..f815ab73c2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -505,6 +505,54 @@ void WiFiComponent::start_scanning() { this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } +// Helper function for WiFi scan result comparison +// Returns true if 'a' should be placed before 'b' in the sorted order +[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) { + // Matching networks always come before non-matching + if (a.get_matches() && !b.get_matches()) + return true; + if (!a.get_matches() && b.get_matches()) + return false; + + if (a.get_matches() && b.get_matches()) { + // For APs with the same SSID, always prefer stronger signal + // This helps with mesh networks and multiple APs + if (a.get_ssid() == b.get_ssid()) { + return a.get_rssi() > b.get_rssi(); + } + + // For different SSIDs, check priority first + if (a.get_priority() != b.get_priority()) + return a.get_priority() > b.get_priority(); + // If priorities are equal, prefer stronger signal + return a.get_rssi() > b.get_rssi(); + } + + // Both don't match - sort by signal strength + return a.get_rssi() > b.get_rssi(); +} + +// Helper function for insertion sort of WiFi scan results +// Using insertion sort instead of std::stable_sort saves flash memory +// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) +// IMPORTANT: This sort is stable (preserves relative order of equal elements) +static void insertion_sort_scan_results(std::vector &results) { + const size_t size = results.size(); + for (size_t i = 1; i < size; i++) { + // Make a copy to avoid issues with move semantics during comparison + WiFiScanResult key = results[i]; + int32_t j = i - 1; + + // Move elements that are worse than key to the right + // For stability, we only move if key is strictly better than results[j] + while (j >= 0 && wifi_scan_result_is_better(key, results[j])) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } +} + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { @@ -535,30 +583,8 @@ void WiFiComponent::check_scanning_finished() { } } - std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(), - [](const WiFiScanResult &a, const WiFiScanResult &b) { - // return true if a is better than b - if (a.get_matches() && !b.get_matches()) - return true; - if (!a.get_matches() && b.get_matches()) - return false; - - if (a.get_matches() && b.get_matches()) { - // For APs with the same SSID, always prefer stronger signal - // This helps with mesh networks and multiple APs - if (a.get_ssid() == b.get_ssid()) { - return a.get_rssi() > b.get_rssi(); - } - - // For different SSIDs, check priority first - if (a.get_priority() != b.get_priority()) - return a.get_priority() > b.get_priority(); - // If priorities are equal, prefer stronger signal - return a.get_rssi() > b.get_rssi(); - } - - return a.get_rssi() > b.get_rssi(); - }); + // Sort scan results using insertion sort for better memory efficiency + insertion_sort_scan_results(this->scan_result_); for (auto &res : this->scan_result_) { char bssid_s[18]; From 6a5eb460efef63c2fa91c2ba1df0c53f245ef4e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:27:05 -1000 Subject: [PATCH 11/17] [esp32] Add framework migration warning for upcoming ESP-IDF default change (#10030) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 62 ++++++++++++++++++++++++++++ esphome/util.py | 8 +++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 05a79553a4..c43cafc100 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -680,6 +680,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ) +class _FrameworkMigrationWarning: + shown = False + + +def _show_framework_migration_message(name: str, variant: str) -> None: + """Show a friendly message about framework migration when defaulting to Arduino.""" + if _FrameworkMigrationWarning.shown: + return + _FrameworkMigrationWarning.shown = True + + from esphome.log import AnsiFore, color + + message = ( + color( + AnsiFore.BOLD_CYAN, + f"💡 IMPORTANT: {name} doesn't have a framework specified!", + ) + + "\n\n" + + f"Currently, {variant} defaults to the Arduino framework.\n" + + color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n") + + "\n" + + "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n" + + "\n" + + "Why change? ESP-IDF offers:\n" + + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") + + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") + + color( + AnsiFore.GREEN, + " 🔧 Active development and testing by ESPHome developers\n", + ) + + "\n" + + "Trade-offs:\n" + + color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n") + + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + + "\n" + + "What should I do?\n" + + color(AnsiFore.CYAN, " Option 1") + + ": Migrate to ESP-IDF (recommended)\n" + + " Add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: esp-idf\n") + + "\n" + + color(AnsiFore.CYAN, " Option 2") + + ": Keep using Arduino (still supported)\n" + + " Add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: arduino\n") + + "\n" + + "Need help? Check out the migration guide:\n" + + color( + AnsiFore.BLUE, + "https://esphome.io/guides/esp32_arduino_to_idf.html", + ) + ) + _LOGGER.warning(message) + + def _set_default_framework(config): if CONF_FRAMEWORK not in config: config = config.copy() @@ -688,6 +746,10 @@ def _set_default_framework(config): if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO + # Show the migration message + _show_framework_migration_message( + config.get(CONF_NAME, "This device"), variant + ) else: config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF diff --git a/esphome/util.py b/esphome/util.py index 3b346371bc..9aa0f6b9d8 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -345,5 +345,11 @@ def get_esp32_arduino_flash_error_help() -> str | None: + "2. Clean build files and compile again\n" + "\n" + "Note: ESP-IDF uses less flash space and provides better performance.\n" - + "Some Arduino-specific libraries may need alternatives.\n\n" + + "Some Arduino-specific libraries may need alternatives.\n" + + "\n" + + "For detailed migration instructions, see:\n" + + color( + AnsiFore.BLUE, + "https://esphome.io/guides/esp32_arduino_to_idf.html\n\n", + ) ) From c0c0a423626b66abc5e35dbe6c02e116c566328d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 16:37:47 -1000 Subject: [PATCH 12/17] [api] Use static allocation for areas and devices in DeviceInfoResponse (#10038) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 4 ++-- esphome/components/api/api_connection.cpp | 12 ++++++++---- esphome/components/api/api_pb2.cpp | 12 ++++++++---- esphome/components/api/api_pb2.h | 6 +++--- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e0b2c19a21..67e91cc8e3 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -250,8 +250,8 @@ message DeviceInfoResponse { // Supports receiving and saving api encryption key bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"]; - repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"]; - repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"]; + repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"]; + repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"]; // Top-level area info to phase out suggested_area AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5fff270c99..cdeabb5cac 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1462,18 +1462,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { resp.api_encryption_supported = true; #endif #ifdef USE_DEVICES + size_t device_index = 0; for (auto const &device : App.get_devices()) { - resp.devices.emplace_back(); - auto &device_info = resp.devices.back(); + if (device_index >= ESPHOME_DEVICE_COUNT) + break; + auto &device_info = resp.devices[device_index++]; device_info.device_id = device->get_device_id(); device_info.set_name(StringRef(device->get_name())); device_info.area_id = device->get_area_id(); } #endif #ifdef USE_AREAS + size_t area_index = 0; for (auto const &area : App.get_areas()) { - resp.areas.emplace_back(); - auto &area_info = resp.areas.back(); + if (area_index >= ESPHOME_AREA_COUNT) + break; + auto &area_info = resp.areas[area_index++]; area_info.area_id = area->get_area_id(); area_info.set_name(StringRef(area->get_name())); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8c14153155..5dddc79b49 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -115,12 +115,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(19, this->api_encryption_supported); #endif #ifdef USE_DEVICES - for (auto &it : this->devices) { + for (const auto &it : this->devices) { buffer.encode_message(20, it, true); } #endif #ifdef USE_AREAS - for (auto &it : this->areas) { + for (const auto &it : this->areas) { buffer.encode_message(21, it, true); } #endif @@ -167,10 +167,14 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { size.add_bool(2, this->api_encryption_supported); #endif #ifdef USE_DEVICES - size.add_repeated_message(2, this->devices); + for (const auto &it : this->devices) { + size.add_message_object_force(2, it); + } #endif #ifdef USE_AREAS - size.add_repeated_message(2, this->areas); + for (const auto &it : this->areas) { + size.add_message_object_force(2, it); + } #endif #ifdef USE_AREAS size.add_message_object(2, this->area); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0bc75ef00b..d43d3c61b7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -490,7 +490,7 @@ class DeviceInfo : public ProtoMessage { class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 211; + static constexpr uint8_t ESTIMATED_SIZE = 247; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -543,10 +543,10 @@ class DeviceInfoResponse : public ProtoMessage { bool api_encryption_supported{false}; #endif #ifdef USE_DEVICES - std::vector devices{}; + std::array devices{}; #endif #ifdef USE_AREAS - std::vector areas{}; + std::array areas{}; #endif #ifdef USE_AREAS AreaInfo area{}; From 4d683d5a69d0f3e54e0a5a827c9a6b2e02cc4cb1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:45:35 +1200 Subject: [PATCH 13/17] [AI] Add note about the defines.h file needing to include all new defines added (#10054) --- .ai/instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 6504c7370d..6c002f9617 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -168,6 +168,8 @@ This document provides essential context for AI models interacting with this pro * `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers. * `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting. * **CI/CD Pipeline:** Defined in `.github/workflows`. +* **Static Analysis & Development:** + * `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms. ## 6. Development & Testing Workflow From a5f1661643aa04f534d90690b76cd08313e9e5e8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:43:04 +1200 Subject: [PATCH 14/17] [nfc] Rename ``binary_sensor`` source files (#10053) --- .../binary_sensor/{binary_sensor.cpp => nfc_binary_sensor.cpp} | 2 +- .../nfc/binary_sensor/{binary_sensor.h => nfc_binary_sensor.h} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename esphome/components/nfc/binary_sensor/{binary_sensor.cpp => nfc_binary_sensor.cpp} (99%) rename esphome/components/nfc/binary_sensor/{binary_sensor.h => nfc_binary_sensor.h} (100%) diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.cpp b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp similarity index 99% rename from esphome/components/nfc/binary_sensor/binary_sensor.cpp rename to esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp index 8f1f6acd51..bc19fa7213 100644 --- a/esphome/components/nfc/binary_sensor/binary_sensor.cpp +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp @@ -1,4 +1,4 @@ -#include "binary_sensor.h" +#include "nfc_binary_sensor.h" #include "../nfc_helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.h b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h similarity index 100% rename from esphome/components/nfc/binary_sensor/binary_sensor.h rename to esphome/components/nfc/binary_sensor/nfc_binary_sensor.h From 3007ca4d57de1dce20e05c471a336bffea2f8bb4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:55:46 +1200 Subject: [PATCH 15/17] [core] Move docs url generator to helpers.py (#10056) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/config_validation.py | 12 ++---------- esphome/helpers.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 84ffd9941e..9aaeb9f9e8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -87,7 +87,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) -from esphome.helpers import add_class_to_obj, list_starts_with +from esphome.helpers import add_class_to_obj, docs_url, list_starts_with from esphome.schema_extractors import ( SCHEMA_EXTRACT, schema_extractor, @@ -666,14 +666,6 @@ def only_with_framework( if suggestions is None: suggestions = {} - version = Version.parse(ESPHOME_VERSION) - if version.is_beta: - docs_format = "https://beta.esphome.io/components/{path}" - elif version.is_dev: - docs_format = "https://next.esphome.io/components/{path}" - else: - docs_format = "https://esphome.io/components/{path}" - def validator_(obj): if CORE.target_framework not in frameworks: err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" @@ -681,7 +673,7 @@ def only_with_framework( (component, docs_path) = suggestion err_str += f"\nPlease use '{component}'" if docs_path: - err_str += f": {docs_format.format(path=docs_path)}" + err_str += f": {docs_url(path=f'components/{docs_path}')}" raise Invalid(err_str) return obj diff --git a/esphome/helpers.py b/esphome/helpers.py index d1f3080e34..f722dc3f7c 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -9,6 +9,8 @@ import re import tempfile from urllib.parse import urlparse +from esphome.const import __version__ as ESPHOME_VERSION + _LOGGER = logging.getLogger(__name__) IS_MACOS = platform.system() == "Darwin" @@ -503,3 +505,20 @@ _DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]") def sanitize(value): """Same behaviour as `helpers.cpp` method `str_sanitize`.""" return _DISALLOWED_CHARS.sub("_", value) + + +def docs_url(path: str) -> str: + """Return the URL to the documentation for a given path.""" + # Local import to avoid circular import + from esphome.config_validation import Version + + version = Version.parse(ESPHOME_VERSION) + if version.is_beta: + docs_format = "https://beta.esphome.io/{path}" + elif version.is_dev: + docs_format = "https://next.esphome.io/{path}" + else: + docs_format = "https://esphome.io/{path}" + + path = path.removeprefix("/") + return docs_format.format(path=path) From bb3ebaf95572ff48f5633a4bd8777de106e856ce Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:55:54 +1000 Subject: [PATCH 16/17] [font] Catch file load exception (#10058) Co-authored-by: clydeps --- esphome/components/font/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7d9a35647e..4ecc76c561 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -15,6 +15,7 @@ from freetype import ( FT_LOAD_RENDER, FT_LOAD_TARGET_MONO, Face, + FT_Exception, ft_pixel_mode_mono, ) import requests @@ -94,7 +95,14 @@ class FontCache(MutableMapping): return self.store[self._keytransform(item)] def __setitem__(self, key, value): - self.store[self._keytransform(key)] = Face(str(value)) + transformed = self._keytransform(key) + try: + self.store[transformed] = Face(str(value)) + except FT_Exception as exc: + file = transformed.split(":", 1) + raise cv.Invalid( + f"{file[0].capitalize()} {file[1]} is not a valid font file" + ) from exc FONT_CACHE = FontCache() From 7c297366c72709000a573ba76c6fa019f0f86166 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 18:57:59 -1000 Subject: [PATCH 17/17] [esp32_ble_tracker] Remove unnecessary STOPPED scanner state to reduce latency (#10055) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 61 ++++++++----------- .../esp32_ble_tracker/esp32_ble_tracker.h | 16 +++-- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index e0029ad15b..254eddd1d9 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -185,9 +185,6 @@ void ESP32BLETracker::loop() { ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); } } - if (this->scanner_state_ == ScannerState::STOPPED) { - this->end_of_scan_(); // Change state to IDLE - } if (this->scanner_state_ == ScannerState::FAILED || (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { this->stop_scan_(); @@ -278,8 +275,6 @@ void ESP32BLETracker::stop_scan_() { ESP_LOGE(TAG, "Scan is starting while trying to stop."); } else if (this->scanner_state_ == ScannerState::STOPPING) { ESP_LOGE(TAG, "Scan is already stopping while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan is already stopped while trying to stop."); } return; } @@ -306,8 +301,6 @@ void ESP32BLETracker::start_scan_(bool first) { ESP_LOGE(TAG, "Cannot start scan while already stopping."); } else if (this->scanner_state_ == ScannerState::FAILED) { ESP_LOGE(TAG, "Cannot start scan while already failed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Cannot start scan while already stopped."); } return; } @@ -342,21 +335,6 @@ void ESP32BLETracker::start_scan_(bool first) { } } -void ESP32BLETracker::end_of_scan_() { - // The lock must be held when calling this function. - if (this->scanner_state_ != ScannerState::STOPPED) { - ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped."); - return; - } - ESP_LOGD(TAG, "End of scan, set scanner state to IDLE."); - this->already_discovered_.clear(); - this->cancel_timeout("scan"); - - for (auto *listener : this->listeners_) - listener->on_scan_end(); - this->set_scanner_state_(ScannerState::IDLE); -} - void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); @@ -389,6 +367,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + // Note: This handler is called from the main loop context, not directly from the BT task. + // The esp32_ble component queues events via enqueue_ble_event() and processes them in loop(). switch (event) { case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: this->gap_scan_set_param_complete_(param->scan_param_cmpl); @@ -409,11 +389,13 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga } void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { + // Note: This handler is called from the main loop context via esp32_ble's event queue. + // However, we still use a lock-free ring buffer to batch results efficiently. ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - // Lock-free SPSC ring buffer write (Producer side) - // This runs in the ESP-IDF Bluetooth stack callback thread + // Ring buffer write (Producer side) + // Even though we're in the main loop, the ring buffer design allows efficient batching // IMPORTANT: Only this thread writes to ring_write_index_ // Load our own index with relaxed ordering (we're the only writer) @@ -445,15 +427,15 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { ESP_LOGE(TAG, "Scan was in failed state when scan completed."); } else if (this->scanner_state_ == ScannerState::IDLE) { ESP_LOGE(TAG, "Scan was idle when scan completed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when scan completed."); } } - this->set_scanner_state_(ScannerState::STOPPED); + // Scan completed naturally, perform cleanup and transition to IDLE + this->cleanup_scan_state_(false); } } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); if (param.status == ESP_BT_STATUS_DONE) { this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; @@ -463,6 +445,7 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t: } void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); this->scan_start_failed_ = param.status; if (this->scanner_state_ != ScannerState::STARTING) { @@ -474,8 +457,6 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble ESP_LOGE(TAG, "Scan was in failed state when start complete."); } else if (this->scanner_state_ == ScannerState::IDLE) { ESP_LOGE(TAG, "Scan was idle when start complete."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when start complete."); } } if (param.status == ESP_BT_STATUS_SUCCESS) { @@ -490,6 +471,8 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble } void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task + // This allows us to safely transition to IDLE state and perform cleanup without race conditions ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); if (this->scanner_state_ != ScannerState::STOPPING) { if (this->scanner_state_ == ScannerState::RUNNING) { @@ -500,11 +483,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ ESP_LOGE(TAG, "Scan was in failed state when stop complete."); } else if (this->scanner_state_ == ScannerState::IDLE) { ESP_LOGE(TAG, "Scan was idle when stop complete."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when stop complete."); } } - this->set_scanner_state_(ScannerState::STOPPED); + + // Perform cleanup and transition to IDLE + this->cleanup_scan_state_(true); } void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -794,9 +777,6 @@ void ESP32BLETracker::dump_config() { case ScannerState::STOPPING: ESP_LOGCONFIG(TAG, " Scanner State: STOPPING"); break; - case ScannerState::STOPPED: - ESP_LOGCONFIG(TAG, " Scanner State: STOPPED"); - break; case ScannerState::FAILED: ESP_LOGCONFIG(TAG, " Scanner State: FAILED"); break; @@ -881,6 +861,17 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { } #endif // USE_ESP32_BLE_DEVICE +void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { + ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); + this->already_discovered_.clear(); + this->cancel_timeout("scan"); + + for (auto *listener : this->listeners_) + listener->on_scan_end(); + + this->set_scanner_state_(ScannerState::IDLE); +} + } // namespace esphome::esp32_ble_tracker #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index e1119c0e18..c274e64b12 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -158,18 +158,16 @@ enum class ClientState : uint8_t { }; enum class ScannerState { - // Scanner is idle, init state, set from the main loop when processing STOPPED + // Scanner is idle, init state IDLE, - // Scanner is starting, set from the main loop only + // Scanner is starting STARTING, - // Scanner is running, set from the ESP callback only + // Scanner is running RUNNING, - // Scanner failed to start, set from the ESP callback only + // Scanner failed to start FAILED, - // Scanner is stopping, set from the main loop only + // Scanner is stopping STOPPING, - // Scanner is stopped, set from the ESP callback only - STOPPED, }; enum class ConnectionType : uint8_t { @@ -262,8 +260,6 @@ class ESP32BLETracker : public Component, void stop_scan_(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); - /// Called when a scan ends - void end_of_scan_(); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. @@ -274,6 +270,8 @@ class ESP32BLETracker : public Component, void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed. void set_scanner_state_(ScannerState state); + /// Common cleanup logic when transitioning scanner to IDLE state + void cleanup_scan_state_(bool is_stop_complete); uint8_t app_id_{0};