mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'dev' into fold_ring_buffer_esp32_ble_tracker
This commit is contained in:
		| @@ -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: | ||||
|   | ||||
| @@ -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"]; | ||||
|   | ||||
| @@ -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())); | ||||
|   } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<DeviceInfo> devices{}; | ||||
|   std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{}; | ||||
| #endif | ||||
| #ifdef USE_AREAS | ||||
|   std::vector<AreaInfo> areas{}; | ||||
|   std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{}; | ||||
| #endif | ||||
| #ifdef USE_AREAS | ||||
|   AreaInfo area{}; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #include "binary_sensor.h" | ||||
| #include "nfc_binary_sensor.h" | ||||
| #include "../nfc_helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| 
 | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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<typename T> | ||||
| 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<typename T> | ||||
| 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) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<AsyncWebServer> 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<AsyncWebServer> server_{nullptr}; | ||||
|   std::vector<AsyncWebHandler *> handlers_; | ||||
| #ifdef USE_WEBSERVER_AUTH | ||||
|   internal::Credentials credentials_; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace web_server_base | ||||
|   | ||||
| @@ -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_() { | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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<WiFiScanResult> &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]; | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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<typename Iterator> 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<typename Iterator> 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; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<class C> C *register_component(C *c) { | ||||
| @@ -316,7 +305,7 @@ class Application { | ||||
|     } \ | ||||
|     return nullptr; \ | ||||
|   } | ||||
|   const std::vector<Device *> &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<Area *> &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<Component *> 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<Component *> looping_components_{}; | ||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | ||||
|   std::vector<int> 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<Component *, ESPHOME_COMPONENT_COUNT> components_{}; | ||||
|  | ||||
| #ifdef USE_DEVICES | ||||
|   std::vector<Device *> devices_{}; | ||||
|   StaticVector<Device *, ESPHOME_DEVICE_COUNT> devices_{}; | ||||
| #endif | ||||
| #ifdef USE_AREAS | ||||
|   std::vector<Area *> areas_{}; | ||||
|   StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{}; | ||||
| #endif | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{}; | ||||
| @@ -556,41 +582,6 @@ class Application { | ||||
| #ifdef USE_UPDATE | ||||
|   StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | ||||
|   std::vector<int> 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. | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
| #include <functional> | ||||
| #include <iterator> | ||||
| #include <limits> | ||||
| #include <memory> | ||||
| #include <string> | ||||
| @@ -100,6 +101,8 @@ template<typename T, size_t N> class StaticVector { | ||||
|   using value_type = T; | ||||
|   using iterator = typename std::array<T, N>::iterator; | ||||
|   using const_iterator = typename std::array<T, N>::const_iterator; | ||||
|   using reverse_iterator = std::reverse_iterator<iterator>; | ||||
|   using const_reverse_iterator = std::reverse_iterator<const_iterator>; | ||||
|  | ||||
|  private: | ||||
|   std::array<T, N> data_{}; | ||||
| @@ -114,6 +117,7 @@ template<typename T, size_t N> 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<typename T, size_t N> 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()); } | ||||
| }; | ||||
|  | ||||
| ///@} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: .*", | ||||
|   | ||||
| @@ -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", | ||||
|         ) | ||||
|     ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user