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 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/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{}; 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/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/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() 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 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/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) { 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 734259093e..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); @@ -423,14 +425,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_() { 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/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]; 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/core/application.cpp b/esphome/core/application.cpp index 3ac17849dd..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; @@ -459,24 +497,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; } } diff --git a/esphome/core/application.h b/esphome/core/application.h index b7824a254b..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; @@ -214,14 +211,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 +305,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 +317,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 +451,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 +469,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 +582,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..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 @@ -240,7 +243,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()); } }; ///@} 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) 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: .*", 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", + ) ) 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 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