From 74b075d3cff8fea73f931880ace85af5cb621c7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:03:17 -1000 Subject: [PATCH 01/43] [codegen] Add static storage class to global variables for size optimization (#12616) --- esphome/components/lvgl/__init__.py | 2 ++ esphome/components/lvgl/lvcode.py | 4 ++-- esphome/components/mapping/__init__.py | 2 +- esphome/cpp_generator.py | 22 +++++++++++++++------- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 19c258fcd5..c9cad1ac90 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -256,9 +256,11 @@ async def to_code(configs): True, type=lv_font_t.operator("ptr").operator("const"), ) + # static=False because LV_FONT_CUSTOM_DECLARE creates an extern declaration cg.new_variable( globfont_id, MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), + static=False, ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index c11597131f..e2c70642a8 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -337,7 +337,7 @@ def lv_Pvariable(type, name) -> MockObj: """ if isinstance(name, str): name = ID(name, True, type) - decl = VariableDeclarationExpression(type, "*", name) + decl = VariableDeclarationExpression(type, "*", name, static=True) CORE.add_global(decl) var = MockObj(name, "->") CORE.register_variable(name, var) @@ -353,7 +353,7 @@ def lv_variable(type, name) -> MockObj: """ if isinstance(name, str): name = ID(name, True, type) - decl = VariableDeclarationExpression(type, "", name) + decl = VariableDeclarationExpression(type, "", name, static=True) CORE.add_global(decl) var = MockObj(name, ".") CORE.register_variable(name, var) diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 94c7c10a82..a36b414fd5 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -133,7 +133,7 @@ async def to_code(config): value_type, ) var = MockObj(varid, ".") - decl = VariableDeclarationExpression(varid.type, "", varid) + decl = VariableDeclarationExpression(varid.type, "", varid, static=True) add_global(decl) CORE.register_variable(varid, var) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 1a47b346b7..ddccb574e4 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -51,15 +51,19 @@ class AssignmentExpression(Expression): class VariableDeclarationExpression(Expression): - __slots__ = ("type", "modifier", "name") + __slots__ = ("type", "modifier", "name", "static") - def __init__(self, type_, modifier, name): + def __init__( + self, type_: "MockObj", modifier: str, name: ID, *, static: bool = False + ) -> None: self.type = type_ self.modifier = modifier self.name = name + self.static = static - def __str__(self): - return f"{self.type} {self.modifier}{self.name}" + def __str__(self) -> str: + prefix = "static " if self.static else "" + return f"{prefix}{self.type} {self.modifier}{self.name}" class ExpressionList(Expression): @@ -507,13 +511,17 @@ def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) -> CORE.add(RawStatement("}")) # output closing curly brace -def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": +def new_variable( + id_: ID, rhs: SafeExpType, type_: "MockObj" = None, *, static: bool = True +) -> "MockObj": """Declare and define a new variable, not pointer type, in the code generation. :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). + :param static: If True (default), declare with static storage class for optimization. + Set to False when the variable must have external linkage (e.g., to match library declarations). :return: The new variable as a MockObj. """ @@ -522,7 +530,7 @@ def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj obj = MockObj(id_, ".") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "", id_) + decl = VariableDeclarationExpression(id_.type, "", id_, static=static) CORE.add_global(decl) assignment = AssignmentExpression(None, "", id_, rhs) CORE.add(assignment) @@ -544,7 +552,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_) + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) CORE.add_global(decl) assignment = AssignmentExpression(None, None, id_, rhs) CORE.add(assignment) From 1756fc31b057a2221226c5f23c4c49c04f4baacf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:54:17 -1000 Subject: [PATCH 02/43] [api] Use union for iterators to reduce APIConnection size by ~16 bytes (#12563) --- esphome/components/api/api_connection.cpp | 75 +++++++++++++++++------ esphome/components/api/api_connection.h | 32 +++++++--- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 28970a321c..ecbad96a64 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef USE_ESP8266 #include @@ -96,8 +97,7 @@ static const int CAMERA_STOP_STREAM = 5000; return; #endif // USE_DEVICES -APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) - : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { +APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto &noise_ctx = parent->get_noise_ctx(); if (noise_ctx.has_psk()) { @@ -136,6 +136,7 @@ void APIConnection::start() { } APIConnection::~APIConnection() { + this->destroy_active_iterator_(); #ifdef USE_BLUETOOTH_PROXY if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); @@ -148,6 +149,32 @@ APIConnection::~APIConnection() { #endif } +void APIConnection::destroy_active_iterator_() { + switch (this->active_iterator_) { + case ActiveIterator::LIST_ENTITIES: + this->iterator_storage_.list_entities.~ListEntitiesIterator(); + break; + case ActiveIterator::INITIAL_STATE: + this->iterator_storage_.initial_state.~InitialStateIterator(); + break; + case ActiveIterator::NONE: + break; + } + this->active_iterator_ = ActiveIterator::NONE; +} + +void APIConnection::begin_iterator_(ActiveIterator type) { + this->destroy_active_iterator_(); + this->active_iterator_ = type; + if (type == ActiveIterator::LIST_ENTITIES) { + new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this); + this->iterator_storage_.list_entities.begin(); + } else { + new (&this->iterator_storage_.initial_state) InitialStateIterator(this); + this->iterator_storage_.initial_state.begin(); + } +} + void APIConnection::loop() { if (this->flags_.next_close) { // requested a disconnect @@ -190,23 +217,35 @@ void APIConnection::loop() { this->process_batch_(); } - if (!this->list_entities_iterator_.completed()) { - this->process_iterator_batch_(this->list_entities_iterator_); - } else if (!this->initial_state_iterator_.completed()) { - this->process_iterator_batch_(this->initial_state_iterator_); - - // If we've completed initial states, process any remaining and clear the flag - if (this->initial_state_iterator_.completed()) { - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); + switch (this->active_iterator_) { + case ActiveIterator::LIST_ENTITIES: + if (this->iterator_storage_.list_entities.completed()) { + this->destroy_active_iterator_(); + if (this->flags_.state_subscription) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } + } else { + this->process_iterator_batch_(this->iterator_storage_.list_entities); } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); - } + break; + case ActiveIterator::INITIAL_STATE: + if (this->iterator_storage_.initial_state.completed()) { + this->destroy_active_iterator_(); + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); + } else { + this->process_iterator_batch_(this->iterator_storage_.initial_state); + } + break; + case ActiveIterator::NONE: + break; } if (this->flags_.sent_ping) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 7351b5082f..845661f183 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -208,10 +208,14 @@ class APIConnection final : public APIServerConnection { bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_ping_response(const PingRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override; - void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } + void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } void subscribe_states(const SubscribeStatesRequest &msg) override { this->flags_.state_subscription = true; - this->initial_state_iterator_.begin(); + // Start initial state iterator only if no iterator is active + // If list_entities is running, we'll start initial_state when it completes + if (this->active_iterator_ == ActiveIterator::NONE) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } } void subscribe_logs(const SubscribeLogsRequest &msg) override { this->flags_.log_subscription = msg.level; @@ -501,10 +505,22 @@ class APIConnection final : public APIServerConnection { std::unique_ptr helper_; APIServer *parent_; - // Group 2: Larger objects (must be 4-byte aligned) - // These contain vectors/pointers internally, so putting them early ensures good alignment - InitialStateIterator initial_state_iterator_; - ListEntitiesIterator list_entities_iterator_; + // Group 2: Iterator union (saves ~16 bytes vs separate iterators) + // These iterators are never active simultaneously - list_entities runs to completion + // before initial_state begins, so we use a union with explicit construction/destruction. + enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE }; + + union IteratorUnion { + ListEntitiesIterator list_entities; + InitialStateIterator initial_state; + // Constructor/destructor do nothing - use placement new/explicit destructor + IteratorUnion() {} + ~IteratorUnion() {} + } iterator_storage_; + + // Helper methods for iterator lifecycle management + void destroy_active_iterator_(); + void begin_iterator_(ActiveIterator type); #ifdef USE_CAMERA std::unique_ptr image_reader_; #endif @@ -619,7 +635,9 @@ class APIConnection final : public APIServerConnection { // 2-byte types immediately after flags_ (no padding between them) uint16_t client_api_version_major_{0}; uint16_t client_api_version_minor_{0}; - // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + // 1-byte type to fill padding + ActiveIterator active_iterator_{ActiveIterator::NONE}; + // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical From 1bdbc4cb85eacaf4790516d2de6e3dbe9528dbb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:54:55 -1000 Subject: [PATCH 03/43] [esp32_ble] Avoid string allocation when setting BLE device name (#12579) --- esphome/components/esp32_ble/ble.cpp | 26 +++++++++++++++++--------- esphome/core/helpers.cpp | 20 +++++++++++++------- esphome/core/helpers.h | 12 ++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index a279f7d2a4..42f8ab8fd4 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -256,8 +256,11 @@ bool ESP32BLE::ble_setup_() { } #endif + // BLE device names are limited to 20 characters + // Buffer: 20 chars + null terminator + constexpr size_t ble_name_max_len = 21; + char name_buffer[ble_name_max_len]; const char *device_name; - std::string name_with_suffix; if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { @@ -268,23 +271,28 @@ bool ESP32BLE::ble_setup_() { char mac_addr[mac_address_len]; get_mac_address_into_buffer(mac_addr); const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - name_with_suffix = - make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); - device_name = name_with_suffix.c_str(); + make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr, + mac_address_suffix_len); + device_name = name_buffer; } else { device_name = this->name_; } } else { - name_with_suffix = App.get_name(); - if (name_with_suffix.length() > 20) { + const std::string &app_name = App.get_name(); + size_t name_len = app_name.length(); + if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { // Keep first 13 chars and last 7 chars (MAC suffix), remove middle - name_with_suffix.erase(13, name_with_suffix.length() - 20); + memcpy(name_buffer, app_name.c_str(), 13); + memcpy(name_buffer + 13, app_name.c_str() + name_len - 7, 7); } else { - name_with_suffix.resize(20); + memcpy(name_buffer, app_name.c_str(), 20); } + name_buffer[20] = '\0'; + } else { + memcpy(name_buffer, app_name.c_str(), name_len + 1); // Include null terminator } - device_name = name_with_suffix.c_str(); + device_name = name_buffer; } err = esp_ble_gap_set_device_name(device_name); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 156f41a2dc..2f76b6c17d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -244,17 +244,16 @@ std::string str_sprintf(const char *fmt, ...) { // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, - size_t suffix_len) { - char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; +size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, + const char *suffix_ptr, size_t suffix_len) { size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix - if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { - // NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2, + if (total_len >= buffer_size) { + // NOTE: This calculation could underflow if suffix_len >= buffer_size - 2, // but this is safe because this helper is only called with small suffixes: // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. - name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator + name_len = buffer_size - suffix_len - 2; // -2 for separator and null terminator total_len = name_len + 1 + suffix_len; } @@ -262,7 +261,14 @@ std::string make_name_with_suffix(const char *name, size_t name_len, char sep, c buffer[name_len] = sep; memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); buffer[total_len] = '\0'; - return std::string(buffer, total_len); + return total_len; +} + +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len) { + char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; + size_t len = make_name_with_suffix_to(buffer, sizeof(buffer), name, name_len, sep, suffix_ptr, suffix_len); + return std::string(buffer, len); } std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6028c93ce2..02d050d2d1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -549,6 +549,18 @@ std::string make_name_with_suffix(const std::string &name, char sep, const char std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, size_t suffix_len); +/// Zero-allocation version: format name + separator + suffix directly into buffer. +/// @param buffer Output buffer (must have space for result + null terminator) +/// @param buffer_size Size of the output buffer +/// @param name The base name string +/// @param name_len Length of the name +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return Length written (excluding null terminator) +size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, + const char *suffix_ptr, size_t suffix_len); + ///@} /// @name Parsing & formatting From 265ad9d2640fa5a9bfa4b02ae1c98cd566ca8503 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:55:28 -1000 Subject: [PATCH 04/43] [esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601) --- esphome/components/api/api_connection.cpp | 63 ++++++++++++------- esphome/components/api/api_connection.h | 4 ++ esphome/components/esp32_camera/__init__.py | 5 +- .../components/esp32_camera/esp32_camera.cpp | 37 +++++++---- .../components/esp32_camera/esp32_camera.h | 5 +- 5 files changed, 74 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ecbad96a64..b5628f654e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -270,33 +270,17 @@ void APIConnection::loop() { } } -#ifdef USE_CAMERA - if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); - bool done = this->image_reader_->available() == to_send; - - CameraImageResponse msg; - msg.key = camera::Camera::instance()->get_object_id_hash(); - msg.set_data(this->image_reader_->peek_data_buffer(), to_send); - msg.done = done; -#ifdef USE_DEVICES - msg.device_id = camera::Camera::instance()->get_device_id(); -#endif - - if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { - this->image_reader_->consume_data(to_send); - if (done) { - this->image_reader_->return_image(); - } - } - } -#endif - #ifdef USE_API_HOMEASSISTANT_STATES if (state_subs_at_ >= 0) { this->process_state_subscriptions_(); } #endif + +#ifdef USE_CAMERA + // Process camera last - state updates are higher priority + // (missing a frame is fine, missing a state update is not) + this->try_send_camera_image_(); +#endif } bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { @@ -1099,6 +1083,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #endif #ifdef USE_CAMERA +void APIConnection::try_send_camera_image_() { + if (!this->image_reader_) + return; + + // Send as many chunks as possible without blocking + while (this->image_reader_->available()) { + if (!this->helper_->can_write_without_blocking()) + return; + + uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); + bool done = this->image_reader_->available() == to_send; + + CameraImageResponse msg; + msg.key = camera::Camera::instance()->get_object_id_hash(); + msg.set_data(this->image_reader_->peek_data_buffer(), to_send); + msg.done = done; +#ifdef USE_DEVICES + msg.device_id = camera::Camera::instance()->get_device_id(); +#endif + + if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + return; // Send failed, try again later + } + this->image_reader_->consume_data(to_send); + if (done) { + this->image_reader_->return_image(); + return; + } + } +} void APIConnection::set_camera_state(std::shared_ptr image) { if (!this->flags_.state_subscription) return; @@ -1106,8 +1120,11 @@ void APIConnection::set_camera_state(std::shared_ptr image) return; if (this->image_reader_->available()) return; - if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) + if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) { this->image_reader_->set_image(std::move(image)); + // Try to send immediately to reduce latency + this->try_send_camera_image_(); + } } uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 845661f183..6753e68749 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -296,6 +296,10 @@ class APIConnection final : public APIServerConnection { // Helper function to handle authentication completion void complete_authentication_(); +#ifdef USE_CAMERA + void try_send_camera_image_(); +#endif + #ifdef USE_API_HOMEASSISTANT_STATES void process_state_subscriptions_(); #endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index ca37cb392d..db6244fb3f 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c +from esphome.components import i2c, socket from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv @@ -27,7 +27,7 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera"] +AUTO_LOAD = ["camera", "socket"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -324,6 +324,7 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") + socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index a3677330ca..06ba7ff16f 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -11,6 +11,7 @@ namespace esphome { namespace esp32_camera { static const char *const TAG = "esp32_camera"; +static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; #if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif @@ -42,12 +43,12 @@ void ESP32Camera::setup() { this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, - "framebuffer_task", // name - 1024, // stack size - this, // task pv params - 1, // priority - nullptr, // handle - 1 // core + "framebuffer_task", // name + FRAMEBUFFER_TASK_STACK_SIZE, // stack size + this, // task pv params + 1, // priority + nullptr, // handle + 1 // core ); } @@ -167,6 +168,19 @@ void ESP32Camera::dump_config() { } void ESP32Camera::loop() { + // Fast path: skip all work when truly idle + // (no current image, no pending requests, and not time for idle request yet) + const uint32_t now = App.get_loop_component_start_time(); + if (!this->current_image_ && !this->has_requested_image_()) { + // Only check idle interval when we're otherwise idle + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(camera::IDLE); + } else { + return; + } + } + // check if we can return the image if (this->can_return_image_()) { // return image @@ -175,13 +189,6 @@ void ESP32Camera::loop() { this->current_image_.reset(); } - // request idle image every idle_update_interval - const uint32_t now = App.get_loop_component_start_time(); - if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { - this->last_idle_request_ = now; - this->request_image(camera::IDLE); - } - // Check if we should fetch a new image if (!this->has_requested_image_()) return; @@ -421,6 +428,10 @@ void ESP32Camera::framebuffer_task(void *pv) { while (true) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + // Only wake the main loop if there's a pending request to consume the frame + if (that->has_requested_image_()) { + App.wake_loop_threadsafe(); + } // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index a49fca6511..e97eb27c70 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -205,8 +206,8 @@ class ESP32Camera : public camera::Camera { esp_err_t init_error_{ESP_OK}; std::shared_ptr current_image_; - uint8_t single_requesters_{0}; - uint8_t stream_requesters_{0}; + std::atomic single_requesters_{0}; + std::atomic stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; std::vector listeners_; From 6383fe4598a07057cbede7760c9e735b7bc4ff04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:56:33 -1000 Subject: [PATCH 05/43] [core] Add zero-allocation object_id methods (#12578) --- esphome/components/api/api_connection.h | 15 ++---- esphome/components/web_server/web_server.cpp | 8 ++-- esphome/components/web_server/web_server.h | 12 ++--- .../components/web_server/web_server_v1.cpp | 3 +- esphome/core/entity_base.cpp | 46 ++++++++++++++----- esphome/core/entity_base.h | 29 ++++++------ esphome/core/helpers.cpp | 8 ---- esphome/core/helpers.h | 7 +++ 8 files changed, 70 insertions(+), 58 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6753e68749..6363116900 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -323,17 +323,10 @@ class APIConnection final : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // Try to use static reference first to avoid allocation - StringRef static_ref = entity->get_object_id_ref_for_api_(); - // Store dynamic string outside the if-else to maintain lifetime - std::string object_id; - if (!static_ref.empty()) { - msg.set_object_id(static_ref); - } else { - // Dynamic case - need to allocate - object_id = entity->get_object_id(); - msg.set_object_id(StringRef(object_id)); - } + // Get object_id with zero heap allocation + // Static case returns direct reference, dynamic case uses buffer + char object_id_buf[OBJECT_ID_MAX_LEN]; + msg.set_object_id(entity->get_object_id_to(object_id_buf)); if (entity->has_own_name()) { msg.set_name(entity->get_name()); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6870a1dc87..207eafad5c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -404,9 +404,11 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { // Helper functions to reduce code size by avoiding macro expansion static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { - char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null - const auto &object_id = obj->get_object_id(); - snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + char id_buf[160]; // prefix + dash + object_id (up to 128) + null + size_t len = strlen(prefix); + memcpy(id_buf, prefix, len); // NOLINT(bugprone-not-null-terminated-result) - null added by write_object_id_to + id_buf[len++] = '-'; + obj->write_object_id_to(id_buf + len, sizeof(id_buf) - len); root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { root[ESPHOME_F("name")] = obj->get_name(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index bb69d57872..98234ec1ae 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -52,14 +52,10 @@ struct UrlMatch { } bool id_equals_entity(EntityBase *entity) const { - // Zero-copy comparison using StringRef - StringRef static_ref = entity->get_object_id_ref_for_api_(); - if (!static_ref.empty()) { - return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; - } - // Fallback to allocation (rare) - const auto &obj_id = entity->get_object_id(); - return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; + // Get object_id with zero heap allocation + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = entity->get_object_id_to(object_id_buf); + return id && id_len == object_id.size() && memcmp(id, object_id.c_str(), id_len) == 0; } bool method_equals(const char *str) const { diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 4f0d0cd1a9..cbc25b9dec 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -15,7 +15,8 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string & stream->print("\" id=\""); stream->print(klass.c_str()); stream->print("-"); - stream->print(obj->get_object_id().c_str()); + char object_id_buf[OBJECT_ID_MAX_LEN]; + stream->print(obj->get_object_id_to(object_id_buf).c_str()); stream->print("\">"); stream->print(obj->get_name().c_str()); stream->print(""); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 046f99d8cc..b7616a9ad3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -60,15 +60,6 @@ std::string EntityBase::get_object_id() const { // `App.get_friendly_name()` is constant. return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; } -StringRef EntityBase::get_object_id_ref_for_api_() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - // Return empty for dynamic case (MAC suffix) - if (this->is_object_id_dynamic_()) { - return EMPTY_STRING; - } - // For static case, return the string or empty if null - return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_); -} void EntityBase::set_object_id(const char *object_id) { this->object_id_c_str_ = object_id; this->calc_object_id_(); @@ -82,8 +73,41 @@ void EntityBase::set_name_and_object_id(const char *name, const char *object_id) // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { - this->object_id_hash_ = - fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); + char buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_object_id_to(buf); + this->object_id_hash_ = fnv1_hash(object_id.c_str()); +} + +// Format dynamic object_id: sanitized snake_case of friendly_name +static size_t format_dynamic_object_id(char *buf, size_t buf_size) { + const std::string &name = App.get_friendly_name(); + size_t len = std::min(name.size(), buf_size - 1); + for (size_t i = 0; i < len; i++) { + buf[i] = to_sanitized_char(to_snake_case_char(name[i])); + } + buf[len] = '\0'; + return len; +} + +size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const { + if (this->is_object_id_dynamic_()) { + return format_dynamic_object_id(buf, buf_size); + } + const char *src = this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; + size_t len = strlen(src); + if (len >= buf_size) + len = buf_size - 1; + memcpy(buf, src, len); + buf[len] = '\0'; + return len; +} + +StringRef EntityBase::get_object_id_to(std::span buf) const { + if (this->is_object_id_dynamic_()) { + size_t len = format_dynamic_object_id(buf.data(), buf.size()); + return StringRef(buf.data(), len); + } + return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index fdf3f6300a..eb1ba46c94 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -1,7 +1,8 @@ #pragma once -#include #include +#include +#include #include "string_ref.h" #include "helpers.h" #include "log.h" @@ -12,14 +13,8 @@ namespace esphome { -// Forward declaration for friend access -namespace api { -class APIConnection; -} // namespace api - -namespace web_server { -struct UrlMatch; -} // namespace web_server +// Maximum size for object_id buffer (friendly_name max ~120 + margin) +static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, @@ -47,6 +42,15 @@ class EntityBase { // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); + /// Get object_id with zero heap allocation + /// For static case: returns StringRef to internal storage (buffer unused) + /// For dynamic case: formats into buffer and returns StringRef to buffer + StringRef get_object_id_to(std::span buf) const; + + /// Write object_id directly to buffer, returns length written (excluding null) + /// Useful for building compound strings without intermediate buffer + size_t write_object_id_to(char *buf, size_t buf_size) const; + // Get/set whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } void set_internal(bool internal) { this->flags_.internal = internal; } @@ -125,13 +129,6 @@ class EntityBase { } protected: - friend class api::APIConnection; - friend struct web_server::UrlMatch; - - // Get object_id as StringRef when it's static (for API usage) - // Returns empty StringRef if object_id is dynamic (needs allocation) - StringRef get_object_id_ref_for_api_() const; - void calc_object_id_(); /// Check if the object_id is dynamic (changes with MAC suffix) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2f76b6c17d..f55f53f16b 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -189,14 +189,6 @@ template std::string str_ctype_transform(const std::string &str) } std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } -// Convert char to snake_case: lowercase and spaces to underscores -static constexpr char to_snake_case_char(char c) { - return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; -} -// Sanitize char: keep alphanumerics, dashes, underscores; replace others with underscore -static constexpr char to_sanitized_char(char c) { - return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; -} std::string str_snake_case(const std::string &str) { std::string result = str; for (char &c : result) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 02d050d2d1..b575a14d14 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -516,9 +516,16 @@ std::string str_until(const std::string &str, char ch); std::string str_lower_case(const std::string &str); /// Convert the string to upper case. std::string str_upper_case(const std::string &str); + +/// Convert a single char to snake_case: lowercase and space to underscore. +constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } /// Convert the string to snake case (lowercase with underscores). std::string str_snake_case(const std::string &str); +/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. +constexpr char to_sanitized_char(char c) { + return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; +} /// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. std::string str_sanitize(const std::string &str); From 84b5d9b21c90f8e38e360762b4368bbf8b29e511 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:00:12 -0500 Subject: [PATCH 06/43] [core] Remove deprecated config options from before 2025 (#12622) Co-authored-by: Claude --- esphome/components/bedjet/climate/__init__.py | 25 ++----------------- esphome/components/bh1750/sensor.py | 10 -------- esphome/components/ethernet/__init__.py | 4 --- esphome/components/i2c/__init__.py | 4 --- esphome/components/remote_base/__init__.py | 15 +---------- esphome/components/sensor/__init__.py | 3 --- esphome/components/tca9548a/__init__.py | 3 +-- .../components/template/switch/__init__.py | 4 --- esphome/components/tuya/light/__init__.py | 6 ----- esphome/components/uart/__init__.py | 4 --- esphome/components/wifi/__init__.py | 4 --- 11 files changed, 4 insertions(+), 78 deletions(-) diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index 0da2107d43..4de9dcca0b 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -1,12 +1,7 @@ import esphome.codegen as cg -from esphome.components import ble_client, climate +from esphome.components import climate import esphome.config_validation as cv -from esphome.const import ( - CONF_HEAT_MODE, - CONF_RECEIVE_TIMEOUT, - CONF_TEMPERATURE_SOURCE, - CONF_TIME_ID, -) +from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child @@ -38,22 +33,6 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend( - # TODO: remove compat layer. - { - cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid( - "The 'ble_client_id' option has been removed. Please migrate " - "to the new `bedjet_id` option in the `bedjet` component.\n" - "See https://esphome.io/components/climate/bedjet/" - ), - cv.Optional(CONF_TIME_ID): cv.invalid( - "The 'time_id' option has been moved to the `bedjet` component." - ), - cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid( - "The 'receive_timeout' option has been moved to the `bedjet` component." - ), - } - ) .extend(BEDJET_CLIENT_SCHEMA) ) diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index 7c7eecb88c..36af5aeef9 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -20,16 +20,6 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.Optional("resolution"): cv.invalid( - "The 'resolution' option has been removed. The optimal value is now dynamically calculated." - ), - cv.Optional("measurement_duration"): cv.invalid( - "The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated." - ), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x23)) ) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e1ed327fb9..f140f395e4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -220,10 +220,6 @@ BASE_SCHEMA = cv.Schema( cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional("enable_mdns"): cv.invalid( - "This option has been removed. Please use the [disabled] option under the " - "new mdns component instead." - ), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 7706484e97..b7436ccc39 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -237,10 +237,6 @@ def i2c_device_schema(default_address): """ schema = { cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), - cv.Optional("multiplexer"): cv.invalid( - "This option has been removed, please see " - "the tca9584a docs for the updated way to use multiplexers" - ), } if default_address is None: schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d24d24b000..9d3e655c57 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -108,9 +108,6 @@ def register_trigger(name, type, data_type): validator = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), - cv.Optional(CONF_RECEIVER_ID): cv.invalid( - "This has been removed in ESPHome 2022.3.0 and the trigger attaches directly to the parent receiver." - ), } ) registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator) @@ -207,13 +204,7 @@ validate_binary_sensor = cv.validate_registry_entry( "remote receiver", BINARY_SENSOR_REGISTRY ) TRIGGER_REGISTRY = SimpleRegistry() -DUMPER_REGISTRY = Registry( - { - cv.Optional(CONF_RECEIVER_ID): cv.invalid( - "This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver." - ), - } -) +DUMPER_REGISTRY = Registry() def validate_dumpers(value): @@ -480,10 +471,6 @@ COOLIX_BASE_SCHEMA = cv.Schema( { cv.Required(CONF_FIRST): cv.hex_int_range(0, 16777215), cv.Optional(CONF_SECOND, default=0): cv.hex_int_range(0, 16777215), - cv.Optional(CONF_DATA): cv.invalid( - "'data' option has been removed in ESPHome 2023.8. " - "Use the 'first' and 'second' options instead." - ), } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 027d9a69b8..83b2656661 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -304,9 +304,6 @@ _SENSOR_SCHEMA = ( cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, cv.Optional(CONF_ENTITY_CATEGORY): sensor_entity_category, - cv.Optional("last_reset_type"): cv.invalid( - "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values." - ), cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All( cv.requires_component("mqtt"), diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py index cef779de2e..72973a54ad 100644 --- a/esphome/components/tca9548a/__init__.py +++ b/esphome/components/tca9548a/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID, CONF_SCAN +from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID CODEOWNERS = ["@andreashergert1984"] @@ -18,7 +18,6 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(TCA9548AComponent), - cv.Optional(CONF_SCAN): cv.invalid("This option has been removed"), cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list( { cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel), diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index e86657510f..8ae5a07dc3 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC, - CONF_RESTORE_STATE, CONF_STATE, CONF_TURN_OFF_ACTION, CONF_TURN_ON_ACTION, @@ -44,9 +43,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( single=True ), - cv.Optional(CONF_RESTORE_STATE): cv.invalid( - "The restore_state option has been removed in 2023.7.0. Use the restore_mode option instead" - ), } ) .extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 1d2286e3c7..4d2ccba8b1 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -37,10 +37,6 @@ COLOR_TYPES = { TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) -COLOR_CONFIG_ERROR = ( - "This option has been removed, use color_datapoint and color_type instead." -) - CONFIG_SCHEMA = cv.All( light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( { @@ -49,8 +45,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_RGB_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), - cv.Optional(CONF_HSV_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t, cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True), cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 6494aaa286..9baa6ebd81 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -19,7 +19,6 @@ from esphome.const import ( CONF_DUMMY_RECEIVER_ID, CONF_FLOW_CONTROL_PIN, CONF_ID, - CONF_INVERT, CONF_LAMBDA, CONF_NUMBER, CONF_PORT, @@ -304,9 +303,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PARITY, default="NONE"): cv.enum( UART_PARITY_OPTIONS, upper=True ), - cv.Optional(CONF_INVERT): cv.invalid( - "This option has been removed. Please instead use invert in the tx/rx pin schemas." - ), cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2c10506011..fb23837e78 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -348,10 +348,6 @@ CONFIG_SCHEMA = cv.All( cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, - cv.Optional("enable_mdns"): cv.invalid( - "This option has been removed. Please use the [disabled] option under the " - "new mdns component instead." - ), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( From cd45fe0c3a72eeeb474e8c748c3dc7368288f38c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 22 Dec 2025 13:13:03 -0600 Subject: [PATCH 07/43] [thermostat] Optimizations to reduce binary size (#12621) --- .../thermostat/thermostat_climate.cpp | 54 +++++++++++++++---- .../thermostat/thermostat_climate.h | 29 +++------- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index e588407c4a..d5fb259dad 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace thermostat { +namespace esphome::thermostat { static const char *const TAG = "thermostat.climate"; @@ -66,10 +65,12 @@ void ThermostatClimate::setup() { } void ThermostatClimate::loop() { - for (auto &timer : this->timer_) { - if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) { + uint32_t now = App.get_loop_component_start_time(); + for (uint8_t i = 0; i < THERMOSTAT_TIMER_COUNT; i++) { + auto &timer = this->timer_[i]; + if (timer.active && (now - timer.started >= timer.time)) { timer.active = false; - timer.func(); + this->call_timer_callback_(static_cast(i)); } } } @@ -916,8 +917,42 @@ uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_in return this->timer_[timer_index].time; } -std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) { - return this->timer_[timer_index].func; +void ThermostatClimate::call_timer_callback_(ThermostatClimateTimerIndex timer_index) { + switch (timer_index) { + case THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME: + this->cooling_max_run_time_timer_callback_(); + break; + case THERMOSTAT_TIMER_COOLING_OFF: + this->cooling_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_COOLING_ON: + this->cooling_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_FAN_MODE: + this->fan_mode_timer_callback_(); + break; + case THERMOSTAT_TIMER_FANNING_OFF: + this->fanning_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_FANNING_ON: + this->fanning_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME: + this->heating_max_run_time_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_OFF: + this->heating_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_ON: + this->heating_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_IDLE_ON: + this->idle_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_COUNT: + default: + break; + } } void ThermostatClimate::cooling_max_run_time_timer_callback_() { @@ -1344,7 +1379,7 @@ void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex t ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms); this->timer_[timer_index].active = false; // Trigger the timer callback immediately - this->timer_[timer_index].func(); + this->call_timer_callback_(timer_index); return; } else { // Adjust timer to run for remaining time - keep original start time @@ -1672,5 +1707,4 @@ ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float defau float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} -} // namespace thermostat -} // namespace esphome +} // namespace esphome::thermostat diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 2443af58d6..564b6127b3 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace thermostat { +namespace esphome::thermostat { enum HumidificationAction : uint8_t { THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0, @@ -41,13 +40,11 @@ enum OnBootRestoreFrom : uint8_t { struct ThermostatClimateTimer { ThermostatClimateTimer() = default; - ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) - : active(active), time(time), started(started), func(std::move(func)) {} + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started) : active(active), time(time), started(started) {} bool active; uint32_t time; uint32_t started; - std::function func; }; struct ThermostatClimateTargetTempConfig { @@ -266,7 +263,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool cancel_timer_(ThermostatClimateTimerIndex timer_index); bool timer_active_(ThermostatClimateTimerIndex timer_index); uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index); - std::function timer_cbf_(ThermostatClimateTimerIndex timer_index); + /// Call the appropriate timer callback based on timer index + void call_timer_callback_(ThermostatClimateTimerIndex timer_index); /// Enhanced timer duration setter with running timer adjustment void set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time); @@ -534,27 +532,16 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_humidity_control_trigger_{nullptr}; /// Climate action timers - std::array timer_{ - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)), - }; + std::array timer_{}; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") FixedVector custom_preset_config_{}; - /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) const char *default_custom_preset_{nullptr}; }; -} // namespace thermostat -} // namespace esphome +} // namespace esphome::thermostat From 08c0f65f30e711e48b4af20d1ce77d2d7ca30b28 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 22 Dec 2025 13:13:18 -0600 Subject: [PATCH 08/43] [sprinkler] Remove internal latching valve support (#12603) --- esphome/components/sprinkler/__init__.py | 109 ++++-------- esphome/components/sprinkler/sprinkler.cpp | 195 +++++---------------- esphome/components/sprinkler/sprinkler.h | 51 +----- 3 files changed, 84 insertions(+), 271 deletions(-) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 2dccb6896a..50c69f9496 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( UNIT_MINUTE, UNIT_SECOND, ) +from esphome.helpers import docs_url AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] @@ -162,55 +163,9 @@ def validate_sprinkler(config): raise cv.Invalid( f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OPEN_DELAY}" ) - if ( - CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID not in valve - ) or ( - CONF_PUMP_ON_SWITCH_ID in valve and CONF_PUMP_OFF_SWITCH_ID not in valve - ): + if CONF_VALVE_SWITCH_ID not in valve: raise cv.Invalid( - f"Both {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID} must be specified for latching pump configuration" - ) - if CONF_PUMP_SWITCH_ID in valve and ( - CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"Do not specify {CONF_PUMP_OFF_SWITCH_ID} or {CONF_PUMP_ON_SWITCH_ID} when using {CONF_PUMP_SWITCH_ID}" - ) - if CONF_PUMP_PULSE_DURATION not in sprinkler_controller and ( - CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"{CONF_PUMP_PULSE_DURATION} must be specified when using {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID}" - ) - if ( - CONF_VALVE_OFF_SWITCH_ID in valve - and CONF_VALVE_ON_SWITCH_ID not in valve - ) or ( - CONF_VALVE_ON_SWITCH_ID in valve - and CONF_VALVE_OFF_SWITCH_ID not in valve - ): - raise cv.Invalid( - f"Both {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified for latching valve configuration" - ) - if CONF_VALVE_SWITCH_ID in valve and ( - CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"Do not specify {CONF_VALVE_OFF_SWITCH_ID} or {CONF_VALVE_ON_SWITCH_ID} when using {CONF_VALVE_SWITCH_ID}" - ) - if CONF_VALVE_PULSE_DURATION not in sprinkler_controller and ( - CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"{CONF_VALVE_PULSE_DURATION} must be specified when using {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID}" - ) - if ( - CONF_VALVE_SWITCH_ID not in valve - and CONF_VALVE_OFF_SWITCH_ID not in valve - and CONF_VALVE_ON_SWITCH_ID not in valve - ): - raise cv.Invalid( - f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" + f"{CONF_VALVE_SWITCH_ID} must be specified in valve configuration" ) if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve: raise cv.Invalid( @@ -290,8 +245,15 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( ), key=CONF_NAME, ), - cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), - cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), + # Removed latching pump keys - accepted for validation error reporting + cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( @@ -321,8 +283,15 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( switch.switch_schema(SprinklerControllerSwitch), key=CONF_NAME, ), - cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.use_id(switch.Switch), - cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.use_id(switch.Switch), + # Removed latching valve keys - accepted for validation error reporting + cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Optional(CONF_VALVE_SWITCH_ID): cv.use_id(switch.Switch), } ) @@ -410,8 +379,15 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( validate_min_max, key=CONF_NAME, ), - cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, + # Removed latching valve keys - accepted for validation error reporting + cv.Optional(CONF_PUMP_PULSE_DURATION): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_VALVE_PULSE_DURATION): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Exclusive( CONF_PUMP_START_PUMP_DELAY, "pump_start_xxxx_delay" ): cv.positive_time_period_seconds, @@ -765,35 +741,10 @@ async def to_code(config): valve_index, valve_switch, valve[CONF_RUN_DURATION] ) ) - elif CONF_VALVE_OFF_SWITCH_ID in valve and CONF_VALVE_ON_SWITCH_ID in valve: - valve_switch_off = await cg.get_variable( - valve[CONF_VALVE_OFF_SWITCH_ID] - ) - valve_switch_on = await cg.get_variable(valve[CONF_VALVE_ON_SWITCH_ID]) - cg.add( - var.configure_valve_switch_pulsed( - valve_index, - valve_switch_off, - valve_switch_on, - sprinkler_controller[CONF_VALVE_PULSE_DURATION], - valve[CONF_RUN_DURATION], - ) - ) if CONF_PUMP_SWITCH_ID in valve: pump = await cg.get_variable(valve[CONF_PUMP_SWITCH_ID]) cg.add(var.configure_valve_pump_switch(valve_index, pump)) - elif CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID in valve: - pump_off = await cg.get_variable(valve[CONF_PUMP_OFF_SWITCH_ID]) - pump_on = await cg.get_variable(valve[CONF_PUMP_ON_SWITCH_ID]) - cg.add( - var.configure_valve_pump_switch_pulsed( - valve_index, - pump_off, - pump_on, - sprinkler_controller[CONF_PUMP_PULSE_DURATION], - ) - ) if CONF_RUN_DURATION_NUMBER in valve: num_rd_var = await number.new_number( diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 69452f2e9e..ca9f85abd8 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -11,70 +11,6 @@ namespace esphome::sprinkler { static const char *const TAG = "sprinkler"; -SprinklerSwitch::SprinklerSwitch() {} -SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {} -SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration) - : pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {} - -bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } - -void SprinklerSwitch::loop() { - if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) { - this->pinned_millis_ = 0; // reset tracker - if (this->off_switch_->state) { - this->off_switch_->turn_off(); - } - if (this->on_switch_->state) { - this->on_switch_->turn_off(); - } - } -} - -void SprinklerSwitch::turn_off() { - if (!this->state()) { // do nothing if we're already in the requested state - return; - } - if (this->off_switch_ != nullptr) { // latching valve, start a pulse - if (!this->off_switch_->state) { - this->off_switch_->turn_on(); - } - this->pinned_millis_ = millis(); - } else if (this->on_switch_ != nullptr) { // non-latching valve - this->on_switch_->turn_off(); - } - this->state_ = false; -} - -void SprinklerSwitch::turn_on() { - if (this->state()) { // do nothing if we're already in the requested state - return; - } - if (this->off_switch_ != nullptr) { // latching valve, start a pulse - if (!this->on_switch_->state) { - this->on_switch_->turn_on(); - } - this->pinned_millis_ = millis(); - } else if (this->on_switch_ != nullptr) { // non-latching valve - this->on_switch_->turn_on(); - } - this->state_ = true; -} - -bool SprinklerSwitch::state() { - if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured... - return this->on_switch_->state; // ...so just return the pump switch state - } - return this->state_; -} - -void SprinklerSwitch::sync_valve_state(bool latch_state) { - if (this->is_latching_valve()) { - this->state_ = latch_state; - } else if (this->on_switch_ != nullptr) { - this->state_ = this->on_switch_->state; - } -} - void SprinklerControllerNumber::setup() { float value; if (!this->restore_value_) { @@ -219,8 +155,8 @@ void SprinklerValveOperator::start() { this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_ if (this->start_delay_is_valve_delay_) { this->pump_on_(); - } else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve - this->valve_on_(); // to ensure consistent run time + } else if (!this->pump_switch()->state) { // if the pump is already on, wait to switch on the valve + this->valve_on_(); // to ensure consistent run time } } else { this->run_(); // there is no start_delay_, so just start the pump and valve @@ -240,8 +176,8 @@ void SprinklerValveOperator::stop() { } else { this->valve_off_(); } - if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... - this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time + if (this->pump_switch()->state) { // if the pump is still on at this point, it may be in use... + this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time } } else { this->kill_(); // there is no stop_delay_, so just stop the pump and valve @@ -274,7 +210,7 @@ uint32_t SprinklerValveOperator::time_remaining() { SprinklerState SprinklerValveOperator::state() { return this->state_; } -SprinklerSwitch *SprinklerValveOperator::pump_switch() { +switch_::Switch *SprinklerValveOperator::pump_switch() { if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { return nullptr; } @@ -285,48 +221,50 @@ SprinklerSwitch *SprinklerValveOperator::pump_switch() { } void SprinklerValveOperator::pump_off_() { - if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + auto *pump = this->pump_switch(); + if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! - this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump + pump->turn_off(); // if no controller was set, just switch off the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does - this->controller_->set_pump_state(this->pump_switch(), false); + this->controller_->set_pump_state(pump, false); this->state_ = state; } } void SprinklerValveOperator::pump_on_() { - if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + auto *pump = this->pump_switch(); + if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! - this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump + pump->turn_on(); // if no controller was set, just switch on the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does - this->controller_->set_pump_state(this->pump_switch(), true); + this->controller_->set_pump_state(pump, true); this->state_ = state; } } void SprinklerValveOperator::valve_off_() { - if (this->valve_ == nullptr) { // safety first! + if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first! return; } - if (this->valve_->valve_switch.state()) { - this->valve_->valve_switch.turn_off(); + if (this->valve_->valve_switch->state) { + this->valve_->valve_switch->turn_off(); } } void SprinklerValveOperator::valve_on_() { - if (this->valve_ == nullptr) { // safety first! + if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first! return; } - if (!this->valve_->valve_switch.state()) { - this->valve_->valve_switch.turn_on(); + if (!this->valve_->valve_switch->state) { + this->valve_->valve_switch->turn_on(); } } @@ -401,12 +339,6 @@ Sprinkler::Sprinkler(const std::string &name) { void Sprinkler::setup() { this->all_valves_off_(true); } void Sprinkler::loop() { - for (auto &p : this->pump_) { - p.loop(); - } - for (auto &v : this->valve_) { - v.valve_switch.loop(); - } for (auto &vo : this->valve_op_) { vo.loop(); } @@ -423,10 +355,15 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll new_valve->controller_switch = valve_sw; new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional { - if (this->valve_pump_switch(new_valve_number) != nullptr) { - return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state(); + auto *valve = this->valve_switch(new_valve_number); + auto *pump = this->valve_pump_switch(new_valve_number); + if (valve == nullptr) { + return false; } - return this->valve_switch(new_valve_number)->state(); + if (pump != nullptr) { + return valve->state && pump->state; + } + return valve->state; }); new_valve->valve_turn_off_automation = @@ -496,18 +433,7 @@ void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_n void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { - this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); - this->valve_[valve_number].run_duration = run_duration; - } -} - -void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, - switch_::Switch *valve_switch_on, uint32_t pulse_duration, - uint32_t run_duration) { - if (this->is_a_valid_valve(valve_number)) { - this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off); - this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); - this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); + this->valve_[valve_number].valve_switch = valve_switch; this->valve_[valve_number].run_duration = run_duration; } } @@ -515,31 +441,12 @@ void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Swit void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) { if (this->is_a_valid_valve(valve_number)) { for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump - if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have... - this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + if (this->pump_[i] == pump_switch) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the pump vector... return; // ...and we are done } - } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it - this->pump_.resize(this->pump_.size() + 1); - this->pump_.back().set_on_switch(pump_switch); - this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump - } -} - -void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, - switch_::Switch *pump_switch_on, uint32_t pulse_duration) { - if (this->is_a_valid_valve(valve_number)) { - for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump - if ((this->pump_[i].off_switch() == pump_switch_off) && - (this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have... - this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... - return; // ...and we are done - } - } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it - this->pump_.resize(this->pump_.size() + 1); - this->pump_.back().set_off_switch(pump_switch_off); - this->pump_.back().set_on_switch(pump_switch_on); - this->pump_.back().set_pulse_duration(pulse_duration); + } // if we end up here, no pumps matched, so add a new one + this->pump_.push_back(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump } } @@ -1041,7 +948,7 @@ size_t Sprinkler::number_of_valves() { return this->valve_.size(); } bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); } -bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { +bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) { if (pump_switch == nullptr) { return false; // we can't do anything if there's nothing to check } @@ -1054,8 +961,7 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) { // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest - if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && - (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { + if (vo.pump_switch() == pump_switch) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || @@ -1074,13 +980,12 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { if (valve_pump == nullptr) { return false; // valve has no pump, so this pump isn't in use by it } - return (pump_switch->off_switch() == valve_pump->off_switch()) && - (pump_switch->on_switch() == valve_pump->on_switch()); + return pump_switch == valve_pump; } return false; } -void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { +void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) { if (pump_switch == nullptr) { return; // we can't do anything if there's nothing to check } @@ -1091,15 +996,10 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { if (controller != this) { // dummy check if (controller->pump_in_use(pump_switch)) { hold_pump_on = true; // if another controller says it's using this pump, keep it on - // at this point we know if there exists another SprinklerSwitch that is "on" with its - // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects } } } if (hold_pump_on) { - // at this point we know if there exists another SprinklerSwitch that is "on" with its - // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects... - pump_switch->sync_valve_state(true); // ...so ensure our state is consistent ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it"); } @@ -1107,8 +1007,6 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { pump_switch->turn_on(); } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) { pump_switch->turn_off(); - } else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done... - pump_switch->sync_valve_state(false); // ...this only impacts latching valves } } @@ -1274,23 +1172,23 @@ SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) { return nullptr; } -SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) { +switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { - return &this->valve_[valve_number].valve_switch; + return this->valve_[valve_number].valve_switch; } return nullptr; } -SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) { +switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { - return &this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + return this->pump_[this->valve_[valve_number].pump_switch_index.value()]; } return nullptr; } -SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { +switch_::Switch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { if (pump_index < this->pump_.size()) { - return &this->pump_[pump_index]; + return this->pump_[pump_index]; } return nullptr; } @@ -1454,8 +1352,9 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { void Sprinkler::all_valves_off_(const bool include_pump) { for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) { - if (this->valve_[valve_index].valve_switch.state()) { - this->valve_[valve_index].valve_switch.turn_off(); + auto *valve_sw = this->valve_[valve_index].valve_switch; + if ((valve_sw != nullptr) && valve_sw->state) { + valve_sw->turn_off(); } if (include_pump) { this->set_pump_state(this->valve_pump_switch(valve_index), false); @@ -1754,10 +1653,6 @@ void Sprinkler::dump_config() { " Name: %s\n" " Run Duration: %" PRIu32 " seconds", valve_number, this->valve_name(valve_number), this->valve_run_duration(valve_number)); - if (this->valve_[valve_number].valve_switch.pulse_duration()) { - ESP_LOGCONFIG(TAG, " Pulse Duration: %" PRIu32 " milliseconds", - this->valve_[valve_number].valve_switch.pulse_duration()); - } } if (!this->pump_.empty()) { ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size()); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 7aa33c2df9..25e2d42446 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -35,7 +35,6 @@ enum SprinklerValveRunRequestOrigin : uint8_t { class Sprinkler; // this component class SprinklerControllerNumber; // number components that appear in the front end; based on number core class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core -class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps class SprinklerValveRunRequest; // tells the sprinkler controller what valve to run and for how long as well as what // SprinklerValveOperator is handling it @@ -43,34 +42,6 @@ template class StartSingleValveAction; template class ShutdownAction; template class ResumeOrStartAction; -class SprinklerSwitch { - public: - SprinklerSwitch(); - SprinklerSwitch(switch_::Switch *sprinkler_switch); - SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration); - - bool is_latching_valve(); // returns true if configured as a latching valve - void loop(); // called as a part of loop(), used for latching valve pulses - uint32_t pulse_duration() { return this->pulse_duration_; } - bool state(); // returns the switch's current state - void set_off_switch(switch_::Switch *off_switch) { this->off_switch_ = off_switch; } - void set_on_switch(switch_::Switch *on_switch) { this->on_switch_ = on_switch; } - void set_pulse_duration(uint32_t pulse_duration) { this->pulse_duration_ = pulse_duration; } - void sync_valve_state( - bool latch_state); // syncs internal state to switch; if latching valve, sets state to latch_state - void turn_off(); // sets internal flag and actuates the switch - void turn_on(); // sets internal flag and actuates the switch - switch_::Switch *off_switch() { return this->off_switch_; } - switch_::Switch *on_switch() { return this->on_switch_; } - - protected: - bool state_{false}; - uint32_t pulse_duration_{0}; - uint64_t pinned_millis_{0}; - switch_::Switch *off_switch_{nullptr}; // only used for latching valves - switch_::Switch *on_switch_{nullptr}; // used for both latching and non-latching valves -}; - struct SprinklerQueueItem { size_t valve_number; uint32_t run_duration; @@ -88,7 +59,7 @@ struct SprinklerValve { SprinklerControllerNumber *run_duration_number; SprinklerControllerSwitch *controller_switch; SprinklerControllerSwitch *enable_switch; - SprinklerSwitch valve_switch; + switch_::Switch *valve_switch; uint32_t run_duration; optional pump_switch_index; bool valve_cycle_complete; @@ -155,7 +126,7 @@ class SprinklerValveOperator { uint32_t run_duration(); // returns the desired run duration in seconds uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) SprinklerState state(); // returns the valve's state/status - SprinklerSwitch *pump_switch(); // returns this SprinklerValveOperator's pump's SprinklerSwitch + switch_::Switch *pump_switch(); // returns this SprinklerValveOperator's pump switch protected: void pump_off_(); @@ -228,13 +199,9 @@ class Sprinkler : public Component { /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); - void configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, - switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration); /// configure a valve's associated pump switch object void configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch); - void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, - switch_::Switch *pump_switch_on, uint32_t pulse_duration); /// configure a valve's run duration number component void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number); @@ -383,10 +350,10 @@ class Sprinkler : public Component { bool is_a_valid_valve(size_t valve_number); /// returns true if the pump the pointer points to is in use - bool pump_in_use(SprinklerSwitch *pump_switch); + bool pump_in_use(switch_::Switch *pump_switch); /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller - void set_pump_state(SprinklerSwitch *pump_switch, bool state); + void set_pump_state(switch_::Switch *pump_switch, bool state); /// returns the amount of time in seconds required for all valves uint32_t total_cycle_time_all_valves(); @@ -419,13 +386,13 @@ class Sprinkler : public Component { SprinklerControllerSwitch *enable_switch(size_t valve_number); /// returns a pointer to a valve's switch object - SprinklerSwitch *valve_switch(size_t valve_number); + switch_::Switch *valve_switch(size_t valve_number); /// returns a pointer to a valve's pump switch object - SprinklerSwitch *valve_pump_switch(size_t valve_number); + switch_::Switch *valve_pump_switch(size_t valve_number); /// returns a pointer to a valve's pump switch object - SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index); + switch_::Switch *valve_pump_switch_by_pump_index(size_t pump_index); protected: /// returns true if valve number is enabled @@ -577,8 +544,8 @@ class Sprinkler : public Component { /// Queue of valves to activate next, regardless of auto-advance std::vector queued_valves_; - /// Sprinkler valve pump objects - std::vector pump_; + /// Sprinkler valve pump switches + std::vector pump_; /// Sprinkler valve objects std::vector valve_; From 918bc4b74f2f9af49d1c3efa8846d392e1ee324e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:41:14 -0500 Subject: [PATCH 09/43] [esp32] Remove remaining using_esp_idf checks (#12623) Co-authored-by: Claude --- esphome/components/captive_portal/__init__.py | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/i2s_audio/__init__.py | 5 +++-- esphome/components/improv_serial/__init__.py | 2 +- esphome/components/mdns/__init__.py | 6 ++---- esphome/components/network/__init__.py | 10 +++++----- esphome/components/wifi/__init__.py | 4 ++-- esphome/core/__init__.py | 5 +++++ 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 25d0a22083..763e2e4ec5 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: auto_load = ["web_server_base", "ota.web_server"] - if CORE.using_esp_idf: + if CORE.is_esp32: auto_load.append("socket") return auto_load diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index b7436ccc39..56e0c8e4ab 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -146,7 +146,7 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") - if CORE.using_esp_idf and get_esp32_variant() in ESP32_I2C_CAPABILITIES: + if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] if len(full_config) > max_num: diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 61c5ca4ec1..d3128c5f4c 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -232,6 +232,8 @@ def validate_use_legacy(value): if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): raise cv.Invalid("Arduino supports only the legacy i2s driver") _set_use_legacy_driver(value[CONF_USE_LEGACY]) + elif CORE.using_arduino: + _set_use_legacy_driver(True) return value @@ -261,8 +263,7 @@ def _final_validate(_): def use_legacy(): - legacy_driver = _get_use_legacy_driver() - return not (CORE.using_esp_idf and not legacy_driver) + return _get_use_legacy_driver() FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 7f88b17e11..9a2ac2f40f 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -26,7 +26,7 @@ def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") - if CORE.using_esp_idf and ( + if CORE.is_esp32 and ( logger_conf[CONF_HARDWARE_UART] == USB_CDC and get_esp32_variant() == VARIANT_ESP32S3 ): diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 99b728b249..3088d8ad7e 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -157,14 +157,12 @@ async def to_code(config): return if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("ESPmDNS", None) - elif CORE.is_esp8266: + if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - if CORE.using_esp_idf: + if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.9.1") cg.add_define("USE_MDNS") diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index d7a51fb0c6..5b63bbfce9 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -156,7 +156,7 @@ async def to_code(config): "High performance networking disabled by user configuration (overriding component request)" ) - if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + if CORE.is_esp32 and should_enable: # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() @@ -210,12 +210,12 @@ async def to_code(config): "USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT] ) if CORE.is_esp32: - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: + if CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True) + else: + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) elif enable_ipv6: cg.add_build_flag("-DCONFIG_LWIP_IPV6") cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index fb23837e78..232e8d4f27 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -464,7 +464,7 @@ async def to_code(config): ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add_define("USE_WIFI_AP") - elif CORE.is_esp32 and CORE.using_esp_idf: + elif CORE.is_esp32 and not CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) @@ -509,7 +509,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) # Apply high performance WiFi settings if high performance networking is enabled - if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + if CORE.is_esp32 and has_high_performance_networking(): # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index ad9844a3bf..3baec93186 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -798,6 +798,11 @@ class EsphomeCore: @property def using_esp_idf(self): + _LOGGER.warning( + "CORE.using_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + "Use CORE.is_esp32 and/or CORE.using_arduino instead." + ) return self.target_framework == "esp-idf" @property From c8b531ac06d569197da9b6740dd7062f1a7e77d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:13:51 -1000 Subject: [PATCH 10/43] [safe_mode] Defer preference sync in clean_rtc to avoid blocking event loop (#12625) --- esphome/components/safe_mode/safe_mode.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f8e5d7d8e5..c933222273 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -141,7 +141,14 @@ uint32_t SafeModeComponent::read_rtc_() { return val; } -void SafeModeComponent::clean_rtc() { this->write_rtc_(0); } +void SafeModeComponent::clean_rtc() { + // Save without sync - preferences will be written at shutdown or by IntervalSyncer. + // This avoids blocking the loop for 50+ ms on flash write. If the device crashes + // before sync, the boot wasn't really successful anyway and the counter should + // remain incremented. + uint32_t val = 0; + this->rtc_.save(&val); +} void SafeModeComponent::on_safe_shutdown() { if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) From bdbe72b7f15e0f0dc861546565798dfa9ed86fa9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:14:11 -1000 Subject: [PATCH 11/43] [web_server] Make internal JSON helper methods private (#12624) --- esphome/components/web_server/web_server.cpp | 182 +++++++++---------- esphome/components/web_server/web_server.h | 105 ++++++----- 2 files changed, 154 insertions(+), 133 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 207eafad5c..ece9d65121 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -455,7 +455,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->sensor_json(obj, obj->state, detail); + std::string data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -463,12 +463,12 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); } std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { +std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -500,7 +500,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json(obj, obj->state, detail); + std::string data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -508,15 +508,15 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const request->send(404); } std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); } std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -542,7 +542,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->switch_json(obj, obj->state, detail); + std::string data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -584,12 +584,12 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); } std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } -std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { +std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -610,7 +610,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->button_json(obj, detail); + std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("press")) { this->defer([obj]() { obj->press(); }); @@ -624,12 +624,12 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_STATE); + return web_server->button_json_((button::Button *) (source), DETAIL_STATE); } std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_ALL); + return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { +std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -655,7 +655,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json(obj, obj->state, detail); + std::string data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -663,14 +663,14 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con request->send(404); } std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); } std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -696,7 +696,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->fan_json(obj, detail); + std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); @@ -738,12 +738,12 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc request->send(404); } std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_STATE); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { +std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -776,7 +776,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->light_json(obj, detail); + std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); @@ -816,12 +816,12 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_STATE); + return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_ALL); + return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { +std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -854,7 +854,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->cover_json(obj, detail); + std::string data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -903,12 +903,12 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { +std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -942,7 +942,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->number_json(obj, obj->state, detail); + std::string data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -962,12 +962,12 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); } std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } -std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { +std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1009,7 +1009,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->date_json(obj, detail); + std::string data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1035,12 +1035,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_STATE); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { +std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1072,7 +1072,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->time_json(obj, detail); + std::string data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1097,12 +1097,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_STATE); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { +std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1134,7 +1134,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->datetime_json(obj, detail); + std::string data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1159,12 +1159,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur request->send(404); } std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_STATE); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { +std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1199,7 +1199,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->text_json(obj, obj->state, detail); + std::string data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1219,12 +1219,12 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); } std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { +std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1255,7 +1255,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); + std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1276,13 +1276,13 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { +std::string WebServer::select_json_(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1312,7 +1312,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->climate_json(obj, detail); + std::string data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1342,13 +1342,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); } std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); } -std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { +std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1456,7 +1456,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->lock_json(obj, obj->state, detail); + std::string data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1498,12 +1498,12 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); } std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } -std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { +std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1530,7 +1530,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->valve_json(obj, detail); + std::string data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1577,12 +1577,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_STATE); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { +std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1614,7 +1614,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); + std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1655,18 +1655,18 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(404); } std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_STATE); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_STATE); } std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_ALL); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_ALL); } -std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1696,7 +1696,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->event_json(obj, "", detail); + std::string data = this->event_json_(obj, "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1711,14 +1711,14 @@ static std::string get_event_type(event::Event *event) { std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_STATE); + return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); } // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_ALL); + return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); } -std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { +std::string WebServer::event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1764,7 +1764,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->update_json(obj, detail); + std::string data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1782,13 +1782,13 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { +std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 98234ec1ae..0078146284 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -271,8 +271,6 @@ class WebServer : public Controller, static std::string sensor_state_json_generator(WebServer *web_server, void *source); static std::string sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the sensor state with its value as a JSON string. - std::string sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config); #endif #ifdef USE_SWITCH @@ -283,8 +281,6 @@ class WebServer : public Controller, static std::string switch_state_json_generator(WebServer *web_server, void *source); static std::string switch_all_json_generator(WebServer *web_server, void *source); - /// Dump the switch state with its value as a JSON string. - std::string switch_json(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON @@ -293,8 +289,6 @@ class WebServer : public Controller, static std::string button_state_json_generator(WebServer *web_server, void *source); static std::string button_all_json_generator(WebServer *web_server, void *source); - /// Dump the button details with its value as a JSON string. - std::string button_json(button::Button *obj, JsonDetail start_config); #endif #ifdef USE_BINARY_SENSOR @@ -305,8 +299,6 @@ class WebServer : public Controller, static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the binary sensor state with its value as a JSON string. - std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); #endif #ifdef USE_FAN @@ -317,8 +309,6 @@ class WebServer : public Controller, static std::string fan_state_json_generator(WebServer *web_server, void *source); static std::string fan_all_json_generator(WebServer *web_server, void *source); - /// Dump the fan state as a JSON string. - std::string fan_json(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT @@ -329,8 +319,6 @@ class WebServer : public Controller, static std::string light_state_json_generator(WebServer *web_server, void *source); static std::string light_all_json_generator(WebServer *web_server, void *source); - /// Dump the light state as a JSON string. - std::string light_json(light::LightState *obj, JsonDetail start_config); #endif #ifdef USE_TEXT_SENSOR @@ -341,8 +329,6 @@ class WebServer : public Controller, static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); - /// Dump the text sensor state with its value as a JSON string. - std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_COVER @@ -353,8 +339,6 @@ class WebServer : public Controller, static std::string cover_state_json_generator(WebServer *web_server, void *source); static std::string cover_all_json_generator(WebServer *web_server, void *source); - /// Dump the cover state as a JSON string. - std::string cover_json(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER @@ -364,8 +348,6 @@ class WebServer : public Controller, static std::string number_state_json_generator(WebServer *web_server, void *source); static std::string number_all_json_generator(WebServer *web_server, void *source); - /// Dump the number state with its value as a JSON string. - std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATE @@ -375,8 +357,6 @@ class WebServer : public Controller, static std::string date_state_json_generator(WebServer *web_server, void *source); static std::string date_all_json_generator(WebServer *web_server, void *source); - /// Dump the date state with its value as a JSON string. - std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_TIME @@ -386,8 +366,6 @@ class WebServer : public Controller, static std::string time_state_json_generator(WebServer *web_server, void *source); static std::string time_all_json_generator(WebServer *web_server, void *source); - /// Dump the time state with its value as a JSON string. - std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATETIME @@ -397,8 +375,6 @@ class WebServer : public Controller, static std::string datetime_state_json_generator(WebServer *web_server, void *source); static std::string datetime_all_json_generator(WebServer *web_server, void *source); - /// Dump the datetime state with its value as a JSON string. - std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_TEXT @@ -408,8 +384,6 @@ class WebServer : public Controller, static std::string text_state_json_generator(WebServer *web_server, void *source); static std::string text_all_json_generator(WebServer *web_server, void *source); - /// Dump the text state with its value as a JSON string. - std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT @@ -419,8 +393,6 @@ class WebServer : public Controller, static std::string select_state_json_generator(WebServer *web_server, void *source); static std::string select_all_json_generator(WebServer *web_server, void *source); - /// Dump the select state with its value as a JSON string. - std::string select_json(select::Select *obj, const char *value, JsonDetail start_config); #endif #ifdef USE_CLIMATE @@ -430,8 +402,6 @@ class WebServer : public Controller, static std::string climate_state_json_generator(WebServer *web_server, void *source); static std::string climate_all_json_generator(WebServer *web_server, void *source); - /// Dump the climate details - std::string climate_json(climate::Climate *obj, JsonDetail start_config); #endif #ifdef USE_LOCK @@ -442,8 +412,6 @@ class WebServer : public Controller, static std::string lock_state_json_generator(WebServer *web_server, void *source); static std::string lock_all_json_generator(WebServer *web_server, void *source); - /// Dump the lock state with its value as a JSON string. - std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif #ifdef USE_VALVE @@ -454,8 +422,6 @@ class WebServer : public Controller, static std::string valve_state_json_generator(WebServer *web_server, void *source); static std::string valve_all_json_generator(WebServer *web_server, void *source); - /// Dump the valve state as a JSON string. - std::string valve_json(valve::Valve *obj, JsonDetail start_config); #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -466,9 +432,6 @@ class WebServer : public Controller, static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); - /// Dump the alarm_control_panel state with its value as a JSON string. - std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); #endif #ifdef USE_EVENT @@ -479,9 +442,6 @@ class WebServer : public Controller, /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); - - /// Dump the event details with its value as a JSON string. - std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config); #endif #ifdef USE_UPDATE @@ -492,8 +452,6 @@ class WebServer : public Controller, static std::string update_state_json_generator(WebServer *web_server, void *source); static std::string update_all_json_generator(WebServer *web_server, void *source); - /// Dump the update state with its value as a JSON string. - std::string update_json(update::UpdateEntity *obj, JsonDetail start_config); #endif /// Override the web handler's canHandle method. @@ -593,6 +551,69 @@ class WebServer : public Controller, const char *js_include_{nullptr}; #endif bool expose_log_{true}; + + private: +#ifdef USE_SENSOR + std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_SWITCH + std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_BUTTON + std::string button_json_(button::Button *obj, JsonDetail start_config); +#endif +#ifdef USE_BINARY_SENSOR + std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_FAN + std::string fan_json_(fan::Fan *obj, JsonDetail start_config); +#endif +#ifdef USE_LIGHT + std::string light_json_(light::LightState *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT_SENSOR + std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_COVER + std::string cover_json_(cover::Cover *obj, JsonDetail start_config); +#endif +#ifdef USE_NUMBER + std::string number_json_(number::Number *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATE + std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_TIME + std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATETIME + std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT + std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_SELECT + std::string select_json_(select::Select *obj, const char *value, JsonDetail start_config); +#endif +#ifdef USE_CLIMATE + std::string climate_json_(climate::Climate *obj, JsonDetail start_config); +#endif +#ifdef USE_LOCK + std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); +#endif +#ifdef USE_VALVE + std::string valve_json_(valve::Valve *obj, JsonDetail start_config); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif +#ifdef USE_EVENT + std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config); +#endif +#ifdef USE_UPDATE + std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); +#endif }; } // namespace web_server From f238f9331272c25bf073b921f443e62e75c73c6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:37:51 -1000 Subject: [PATCH 12/43] [core] Move comment to PROGMEM on ESP8266 (#12554) --- esphome/components/web_server/web_server.cpp | 4 +- esphome/core/application.h | 23 +++--- esphome/core/build_info_data.h | 2 + esphome/core/config.py | 3 +- esphome/writer.py | 25 +++++-- tests/dummy_main.cpp | 2 +- tests/unit_tests/test_writer.py | 76 +++++++++++++++++--- 7 files changed, 105 insertions(+), 30 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ece9d65121..b0731f335b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -287,7 +287,9 @@ std::string WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root[ESPHOME_F("comment")] = App.get_comment_ref(); + char comment_buffer[ESPHOME_COMMENT_SIZE]; + App.get_comment_string(comment_buffer); + root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else diff --git a/esphome/core/application.h b/esphome/core/application.h index d2146a6c16..13461b3ebd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -12,6 +12,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/progmem.h" #include "esphome/core/scheduler.h" #include "esphome/core/string_ref.h" #include "esphome/core/version.h" @@ -107,8 +108,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, - bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -127,7 +127,6 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->comment_ = comment; } #ifdef USE_DEVICES @@ -264,10 +263,19 @@ class Application { return ""; } - /// Get the comment of this Application set by pre_setup(). - std::string get_comment() const { return this->comment_; } - /// Get the comment as StringRef (avoids allocation) - StringRef get_comment_ref() const { return StringRef(this->comment_); } + /// Copy the comment string into the provided buffer + /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) + void get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); + buffer[buffer.size() - 1] = '\0'; + } + + /// Get the comment of this Application as a string + std::string get_comment() { + char buffer[ESPHOME_COMMENT_SIZE]; + this->get_comment_string(buffer); + return std::string(buffer); + } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } @@ -513,7 +521,6 @@ class Application { // Pointer-sized members first Component *current_component_{nullptr}; - const char *comment_{nullptr}; // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components diff --git a/esphome/core/build_info_data.h b/esphome/core/build_info_data.h index 5e424ffaca..02bb465e44 100644 --- a/esphome/core/build_info_data.h +++ b/esphome/core/build_info_data.h @@ -7,4 +7,6 @@ #define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT #define ESPHOME_BUILD_TIME 1700000000 // NOLINT +#define ESPHOME_COMMENT_SIZE 1 // NOLINT static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000"; +static const char ESPHOME_COMMENT_STR[] = ""; diff --git a/esphome/core/config.py b/esphome/core/config.py index 507a39b401..5e32b9380d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -209,7 +209,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), cv.Optional(CONF_AREA): validate_area_config, - cv.Optional(CONF_COMMENT): cv.string, + cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { @@ -505,7 +505,6 @@ async def to_code(config: ConfigType) -> None: cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config.get(CONF_COMMENT, ""), config[CONF_NAME_ADD_MAC_SUFFIX], ) ) diff --git a/esphome/writer.py b/esphome/writer.py index 183fff8730..9ae40e417a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -21,6 +21,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, get_str_env, is_ha_addon, read_file, @@ -271,7 +272,7 @@ def copy_src_tree(): "esphome", "core", "build_info_data.h" ) build_info_json_path = CORE.relative_build_path("build_info.json") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Defensively force a rebuild if the build_info files don't exist, or if # there was a config change which didn't actually cause a source change @@ -292,7 +293,9 @@ def copy_src_tree(): if sources_changed: write_file( build_info_data_h_path, - generate_build_info_data_h(config_hash, build_time, build_time_str), + generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ), ) write_file( build_info_json_path, @@ -332,31 +335,39 @@ def generate_version_h(): ) -def get_build_info() -> tuple[int, int, str]: +def get_build_info() -> tuple[int, int, str, str]: """Calculate build_info values from current config. Returns: - Tuple of (config_hash, build_time, build_time_str) + Tuple of (config_hash, build_time, build_time_str, comment) """ config_hash = CORE.config_hash build_time = int(time.time()) build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time)) - return config_hash, build_time, build_time_str + comment = CORE.comment or "" + return config_hash, build_time, build_time_str, comment def generate_build_info_data_h( - config_hash: int, build_time: int, build_time_str: str + config_hash: int, build_time: int, build_time_str: str, comment: str ) -> str: - """Generate build_info_data.h header with config hash and build time.""" + """Generate build_info_data.h header with config hash, build time, and comment.""" + # cpp_string_escape returns '"escaped"', slice off the quotes since template has them + escaped_comment = cpp_string_escape(comment)[1:-1] + # +1 for null terminator + comment_size = len(comment) + 1 return f"""#pragma once // Auto-generated build_info data #define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT #define ESPHOME_BUILD_TIME {build_time} // NOLINT +#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT #ifdef USE_ESP8266 #include static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; #else static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; #endif """ diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 5849f4eb95..e6fe733807 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", false); + App.pre_setup("livingroom", "LivingRoom", false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 06a7d5dbdf..f354d71bb7 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1186,8 +1186,9 @@ def test_get_build_info_new_build( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "Test comment" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1195,6 +1196,7 @@ def test_get_build_info_new_build( assert isinstance(build_time_str, str) # Verify build_time_str format matches expected pattern assert len(build_time_str) >= 19 # e.g., "2025-12-15 16:27:44 +0000" + assert comment == "Test comment" @patch("esphome.writer.CORE") @@ -1206,6 +1208,7 @@ def test_get_build_info_always_returns_current_time( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with matching config_hash and version existing_build_time = 1700000000 @@ -1222,7 +1225,7 @@ def test_get_build_info_always_returns_current_time( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 # get_build_info now always returns current time @@ -1240,6 +1243,7 @@ def test_get_build_info_config_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0xABCDEF00 # Different from existing + mock_core.comment = "" # Create existing build_info.json with different config_hash existing_build_time = 1700000000 @@ -1255,7 +1259,7 @@ def test_get_build_info_config_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0xABCDEF00 assert build_time != existing_build_time # New time generated @@ -1271,6 +1275,7 @@ def test_get_build_info_version_changed( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create existing build_info.json with different version existing_build_time = 1700000000 @@ -1286,7 +1291,7 @@ def test_get_build_info_version_changed( ) with patch("esphome.writer.__version__", "2025.1.0-dev"): # New version - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert build_time != existing_build_time # New time generated @@ -1302,11 +1307,12 @@ def test_get_build_info_invalid_json( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create invalid JSON file build_info_path.write_text("not valid json {{{") - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1322,12 +1328,13 @@ def test_get_build_info_missing_keys( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" # Create JSON with missing keys build_info_path.write_text(json.dumps({"config_hash": 0x12345678})) with patch("esphome.writer.__version__", "2025.1.0-dev"): - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() assert config_hash == 0x12345678 assert isinstance(build_time, int) @@ -1343,8 +1350,9 @@ def test_get_build_info_build_time_str_format( build_info_path = tmp_path / "build_info.json" mock_core.relative_build_path.return_value = build_info_path mock_core.config_hash = 0x12345678 + mock_core.comment = "" - config_hash, build_time, build_time_str = get_build_info() + config_hash, build_time, build_time_str, comment = get_build_info() # Verify the format matches "%Y-%m-%d %H:%M:%S %z" # e.g., "2025-12-15 16:27:44 +0000" @@ -1357,36 +1365,73 @@ def test_generate_build_info_data_h_format() -> None: config_hash = 0x12345678 build_time = 1700000000 build_time_str = "2023-11-14 22:13:20 +0000" + comment = "Test comment" - result = generate_build_info_data_h(config_hash, build_time, build_time_str) + result = generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ) assert "#pragma once" in result assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result assert "#define ESPHOME_BUILD_TIME 1700000000" in result + assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1 assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result + assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result def test_generate_build_info_data_h_esp8266_progmem() -> None: """Test generate_build_info_data_h includes PROGMEM for ESP8266.""" - result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test") + result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment") # Should have ESP8266 PROGMEM conditional assert "#ifdef USE_ESP8266" in result assert "#include " in result assert "PROGMEM" in result + # Both build time and comment should have PROGMEM versions + assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result + assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result def test_generate_build_info_data_h_hash_formatting() -> None: """Test generate_build_info_data_h formats hash with leading zeros.""" # Test with small hash value that needs leading zeros - result = generate_build_info_data_h(0x00000001, 0, "test") + result = generate_build_info_data_h(0x00000001, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result # Test with larger hash value - result = generate_build_info_data_h(0xFFFFFFFF, 0, "test") + result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "") assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result +def test_generate_build_info_data_h_comment_escaping() -> None: + r"""Test generate_build_info_data_h properly escapes special characters in comment. + + Uses cpp_string_escape which outputs octal escapes for special characters: + - backslash (ASCII 92) -> \134 + - double quote (ASCII 34) -> \042 + - newline (ASCII 10) -> \012 + """ + # Test backslash escaping (ASCII 92 = octal 134) + result = generate_build_info_data_h(0, 0, "test", "backslash\\here") + assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result + + # Test quote escaping (ASCII 34 = octal 042) + result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') + assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result + + # Test newline escaping (ASCII 10 = octal 012) + result = generate_build_info_data_h(0, 0, "test", "line1\nline2") + assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result + + +def test_generate_build_info_data_h_empty_comment() -> None: + """Test generate_build_info_data_h handles empty comment.""" + result = generate_build_info_data_h(0, 0, "test", "") + + assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator + assert 'ESPHOME_COMMENT_STR[] = ""' in result + + @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @patch("esphome.writer.walk_files") @@ -1445,6 +1490,7 @@ def test_copy_src_tree_writes_build_info_files( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "Test comment" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("core", mock_component)] @@ -1466,6 +1512,8 @@ def test_copy_src_tree_writes_build_info_files( assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content assert "#define ESPHOME_BUILD_TIME" in build_info_h_content assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content + assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content + assert "ESPHOME_COMMENT_STR" in build_info_h_content # Verify build_info.json was written build_info_json_path = build_path / "build_info.json" @@ -1517,6 +1565,7 @@ def test_copy_src_tree_detects_config_hash_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1578,6 +1627,7 @@ def test_copy_src_tree_detects_version_change( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1627,6 +1677,7 @@ def test_copy_src_tree_handles_invalid_build_info_json( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] @@ -1700,6 +1751,7 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [("test", mock_component)] @@ -1794,6 +1846,7 @@ def test_copy_src_tree_detects_removed_source_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] # No components = file should be removed @@ -1855,6 +1908,7 @@ def test_copy_src_tree_ignores_removed_generated_file( mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" mock_core.target_platform = "test_platform" mock_core.config = {} mock_iter_components.return_value = [] From af0d4d2c2cfd6c955bd2903af7a4039d7793c1cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 11:56:07 -1000 Subject: [PATCH 13/43] [web_server] Use stack buffers for value formatting to reduce flash usage (#12575) --- esphome/components/web_server/web_server.cpp | 68 +++++++++++--------- esphome/core/helpers.cpp | 34 ++++++---- esphome/core/helpers.h | 11 +++- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b0731f335b..df8a5364cf 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -432,7 +432,7 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state, const T &value, JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); root[ESPHOME_F("state")] = state; @@ -475,9 +475,10 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail JsonObject root = builder.root(); const auto uom_ref = obj->get_unit_of_measurement_ref(); - - std::string state = - std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); + char buf[VALUE_ACCURACY_MAX_LEN]; + const char *state = std::isnan(value) + ? "NA" + : (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf); set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -522,7 +523,7 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -974,21 +975,20 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail JsonObject root = builder.root(); const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); - std::string val_str = std::isnan(value) - ? "\"NaN\"" - : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state_str = std::isnan(value) ? "NA" - : value_accuracy_with_uom_to_string( - value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); + // Need two buffers: one for value, one for state with UOM + char val_buf[VALUE_ACCURACY_MAX_LEN]; + char state_buf[VALUE_ACCURACY_MAX_LEN]; + const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf); + const char *state_str = + std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf); set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); if (start_config == DETAIL_ALL) { - root[ESPHOME_F("min_value")] = - value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("max_value")] = - value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("step")] = - value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); + // ArduinoJson copies the string immediately, so we can reuse val_buf + root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf); + root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf); + root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf); root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) root[ESPHOME_F("uom")] = uom_ref; @@ -1230,8 +1230,8 @@ std::string WebServer::text_json_(text::Text *obj, const std::string &value, Jso json::JsonBuilder builder; JsonObject root = builder.root(); - std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; - set_json_icon_state_value(root, obj, "text", state, value, start_config); + const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str(); + set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config); root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); @@ -1359,6 +1359,7 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char buf[PSTR_LOCAL_SIZE]; + char temp_buf[VALUE_ACCURACY_MAX_LEN]; if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("modes")].to(); @@ -1395,8 +1396,10 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - root[ESPHOME_F("min_temp")] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); @@ -1419,23 +1422,26 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - if (!std::isnan(obj->current_temperature)) { - root[ESPHOME_F("current_temperature")] = value_accuracy_to_string(obj->current_temperature, current_accuracy); - } else { - root[ESPHOME_F("current_temperature")] = "NA"; - } + root[ESPHOME_F("current_temperature")] = + std::isnan(obj->current_temperature) + ? "NA" + : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - root[ESPHOME_F("target_temperature_low")] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root[ESPHOME_F("target_temperature_low")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf); root[ESPHOME_F("target_temperature_high")] = - value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + (value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf); if (!has_state) { - root[ESPHOME_F("state")] = value_accuracy_to_string( - (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, target_accuracy); + root[ESPHOME_F("state")] = + (value_accuracy_to_buf(temp_buf, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy), + temp_buf); } } else { - root[ESPHOME_F("target_temperature")] = value_accuracy_to_string(obj->target_temperature, target_accuracy); + root[ESPHOME_F("target_temperature")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf); if (!has_state) root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")]; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f55f53f16b..d7d32ea28f 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -383,23 +383,33 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - normalize_accuracy_decimals(value, accuracy_decimals); - char tmp[32]; // should be enough, but we should maybe improve this at some point. - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - return std::string(tmp); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); } -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); - // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") - // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short - char tmp[64]; + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); +} + +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement) { if (unit_of_measurement.empty()) { - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - } else { - snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + return value_accuracy_to_buf(buf, value, accuracy_decimals); } - return std::string(tmp); + normalize_accuracy_decimals(value, accuracy_decimals); + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); } int8_t step_to_accuracy_decimals(float step) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b575a14d14..769041160c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -886,8 +886,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); -/// Create a string from a value, an accuracy in decimals, and a unit of measurement. -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); + +/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) +static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; + +/// Format value with accuracy to buffer, returns chars written (excluding null) +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals); +/// Format value with accuracy and UOM to buffer, returns chars written (excluding null) +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); From 1b31253287343b150a01a3a04e8673dabc68f73e Mon Sep 17 00:00:00 2001 From: eoasmxd <42328021+eoasmxd@users.noreply.github.com> Date: Tue, 23 Dec 2025 06:19:48 +0800 Subject: [PATCH 14/43] Add Event Component to UART (#11765) Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/uart/event/__init__.py | 90 ++++++++++++++++++++ esphome/components/uart/event/uart_event.cpp | 48 +++++++++++ esphome/components/uart/event/uart_event.h | 31 +++++++ tests/components/uart/test.esp32-idf.yaml | 8 ++ tests/components/uart/test.esp8266-ard.yaml | 8 ++ 6 files changed, 186 insertions(+) create mode 100644 esphome/components/uart/event/__init__.py create mode 100644 esphome/components/uart/event/uart_event.cpp create mode 100644 esphome/components/uart/event/uart_event.h diff --git a/CODEOWNERS b/CODEOWNERS index 941c2e2849..f95d68a46d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -519,6 +519,7 @@ esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb +esphome/components/uart/event/* @eoasmxd esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli diff --git a/esphome/components/uart/event/__init__.py b/esphome/components/uart/event/__init__.py new file mode 100644 index 0000000000..64af318a11 --- /dev/null +++ b/esphome/components/uart/event/__init__.py @@ -0,0 +1,90 @@ +import esphome.codegen as cg +from esphome.components import event, uart +import esphome.config_validation as cv +from esphome.const import CONF_EVENT_TYPES, CONF_ID +from esphome.core import ID +from esphome.types import ConfigType + +from .. import uart_ns + +CODEOWNERS = ["@eoasmxd"] + +DEPENDENCIES = ["uart"] + +UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component) + + +def validate_event_types(value) -> list[tuple[str, str | list[int]]]: + if not isinstance(value, list): + raise cv.Invalid("Event type must be a list of key-value mappings.") + + processed: list[tuple[str, str | list[int]]] = [] + for item in value: + if not isinstance(item, dict): + raise cv.Invalid(f"Event type item must be a mapping (dictionary): {item}") + if len(item) != 1: + raise cv.Invalid( + f"Event type item must be a single key-value mapping: {item}" + ) + + # Get the single key-value pair + event_name, match_data = next(iter(item.items())) + + if not isinstance(event_name, str): + raise cv.Invalid(f"Event name (key) must be a string: {event_name}") + + try: + # Try to validate as list of hex bytes + match_data_bin = cv.ensure_list(cv.hex_uint8_t)(match_data) + processed.append((event_name, match_data_bin)) + continue + except cv.Invalid: + pass # Not binary, try string + + try: + # Try to validate as string + match_data_str = cv.string_strict(match_data) + processed.append((event_name, match_data_str)) + continue + except cv.Invalid: + pass # Not string either + + # If neither validation passed + raise cv.Invalid( + f"Event match data for '{event_name}' must be a string or a list of hex bytes. Invalid data: {match_data}" + ) + + if not processed: + raise cv.Invalid("event_types must contain at least one event mapping.") + + return processed + + +CONFIG_SCHEMA = ( + event.event_schema(UARTEvent) + .extend( + { + cv.Required(CONF_EVENT_TYPES): validate_event_types, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config: ConfigType) -> None: + event_names = [item[0] for item in config[CONF_EVENT_TYPES]] + var = await event.new_event(config, event_types=event_names) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]): + if isinstance(match_data, str): + match_data = [ord(c) for c in match_data] + + match_data_var_id = ID( + f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8 + ) + match_data_var = cg.static_const_array( + match_data_var_id, cg.ArrayInitializer(*match_data) + ) + cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data))) diff --git a/esphome/components/uart/event/uart_event.cpp b/esphome/components/uart/event/uart_event.cpp new file mode 100644 index 0000000000..02c5f2e631 --- /dev/null +++ b/esphome/components/uart/event/uart_event.cpp @@ -0,0 +1,48 @@ +#include "uart_event.h" +#include "esphome/core/log.h" +#include + +namespace esphome::uart { + +static const char *const TAG = "uart.event"; + +void UARTEvent::setup() {} + +void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); } + +void UARTEvent::loop() { this->read_data_(); } + +void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) { + this->matchers_.push_back({event_name, match_data, match_data_len}); + if (match_data_len > this->max_matcher_len_) { + this->max_matcher_len_ = match_data_len; + } +} + +void UARTEvent::read_data_() { + while (this->available()) { + uint8_t data; + this->read_byte(&data); + this->buffer_.push_back(data); + + bool match_found = false; + for (const auto &matcher : this->matchers_) { + if (this->buffer_.size() < matcher.data_len) { + continue; + } + + if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) { + this->trigger(matcher.event_name); + this->buffer_.clear(); + match_found = true; + break; + } + } + + if (!match_found && this->max_matcher_len_ > 0 && this->buffer_.size() > this->max_matcher_len_) { + this->buffer_.erase(this->buffer_.begin()); + } + } +} + +} // namespace esphome::uart diff --git a/esphome/components/uart/event/uart_event.h b/esphome/components/uart/event/uart_event.h new file mode 100644 index 0000000000..8a00b5894b --- /dev/null +++ b/esphome/components/uart/event/uart_event.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/event/event.h" +#include "esphome/components/uart/uart.h" +#include + +namespace esphome::uart { + +class UARTEvent : public event::Event, public UARTDevice, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len); + + protected: + struct EventMatcher { + const char *event_name; + const uint8_t *data; + size_t data_len; + }; + + void read_data_(); + std::vector matchers_; + std::vector buffer_; + size_t max_matcher_len_ = 0; +}; + +} // namespace esphome::uart diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 6ffd0d7282..2a97f9a5de 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -75,3 +75,11 @@ button: - uart.write: !lambda |- std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; return std::vector(cmd.begin(), cmd.end()); + +event: + - platform: uart + uart_id: uart_uart + name: "UART Event" + event_types: + - "string_event_A": "*A#" + - "bytes_event_B": [0x2A, 0x42, 0x23] diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index 566038ee3e..c2670b289a 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -31,3 +31,11 @@ button: name: "UART Button" uart_id: uart_uart data: [0xFF, 0xEE] + +event: + - platform: uart + uart_id: uart_uart + name: "UART Event" + event_types: + - "string_event_A": "*A#" + - "bytes_event_B": [0x2A, 0x42, 0x23] From b4c92dd8cbbf61810e876c3fbb3ca0ebaa199600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 14:29:47 -1000 Subject: [PATCH 15/43] [logger] Zephyr: Use k_str_out() with known length instead of printk() (#12619) --- esphome/components/logger/logger.h | 4 ++-- esphome/components/logger/logger_zephyr.cpp | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 36195b919a..2cedd7a76f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -118,11 +118,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny) +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny, Zephyr) // Allows single write call with newline included for efficiency // true: write_msg_ adds newline itself via puts()/println() (other platforms) // Newline should NOT be added to buffer -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index ec2ff3013c..330dfa96ec 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace esphome::logger { @@ -14,7 +15,7 @@ static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC void Logger::loop() { - if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { + if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; } static bool opened = false; @@ -62,18 +63,17 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t) { +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK - printk("%s\n", msg); + k_str_out(const_cast(msg), len); #endif - if (nullptr == this->uart_dev_) { + if (this->uart_dev_ == nullptr) { return; } - while (*msg) { - uart_poll_out(this->uart_dev_, *msg); - ++msg; + for (size_t i = 0; i < len; ++i) { + uart_poll_out(this->uart_dev_, msg[i]); } - uart_poll_out(this->uart_dev_, '\n'); } const LogString *Logger::get_uart_selection_() { From 7d5342bca5df37ba48e735342f72f3d615a01555 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 16:45:22 -1000 Subject: [PATCH 16/43] [logger] Host: Use fwrite() with explicit length and remove platform branching (#12628) --- esphome/components/logger/logger.cpp | 4 +-- esphome/components/logger/logger.h | 41 ++++++++--------------- esphome/components/logger/logger_host.cpp | 23 ++++++++----- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 21e2b44808..474eb9ec38 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,8 +65,8 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - // Add newline if platform needs it (ESP32 doesn't add via write_msg_) - this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + // Add newline before writing to console + this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); this->write_msg_(console_buffer, buffer_at); } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 2cedd7a76f..ba8d4667b6 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -117,17 +117,6 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; -// Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny, Zephyr) -// Allows single write call with newline included for efficiency -// true: write_msg_ adds newline itself via puts()/println() (other platforms) -// Newline should NOT be added to buffer -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) -static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; -#else -static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; -#endif - #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -259,22 +248,20 @@ class Logger : public Component { } } - // Helper to add newline to buffer for platforms that need it + // Helper to add newline to buffer before writing to console // Modifies buffer_at to include the newline - inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - if constexpr (!WRITE_MSG_ADDS_NEWLINE) { - // Add newline - don't need to maintain null termination - // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator - // This is safe because: - // 1. Callbacks already received the message (before we add newline) - // 2. write_msg_ receives the length explicitly (doesn't need null terminator) - if (*buffer_at < buffer_size) { - buffer[(*buffer_at)++] = '\n'; - } else if (buffer_size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - buffer[buffer_size - 1] = '\n'; - *buffer_at = buffer_size; - } + inline void HOT add_newline_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + // Add newline - don't need to maintain null termination + // write_msg_ receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; } } @@ -283,7 +270,7 @@ class Logger : public Component { inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { if (this->baud_rate_ > 0) { uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; - this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->add_newline_to_buffer_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); this->write_msg_(this->tx_buffer_ + offset, *len_ptr); } } diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index c5e1e6f865..cbca06e431 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,16 +3,23 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg, size_t) { - time_t rawtime; - struct tm *timeinfo; - char buffer[80]; +void HOT Logger::write_msg_(const char *msg, size_t len) { + static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" + // tx_buffer_size_ defaults to 512, so 768 covers default + headroom + char buffer[TIMESTAMP_LEN + 768]; + time_t rawtime; time(&rawtime); - timeinfo = localtime(&rawtime); - strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); - fputs(buffer, stdout); - puts(msg); + struct tm *timeinfo = localtime(&rawtime); + size_t pos = strftime(buffer, TIMESTAMP_LEN + 1, "[%H:%M:%S]", timeinfo); + + // Copy message (with newline already included by caller) + size_t copy_len = std::min(len, sizeof(buffer) - pos); + memcpy(buffer + pos, msg, copy_len); + pos += copy_len; + + // Single write for everything + fwrite(buffer, 1, pos, stdout); } void Logger::pre_setup() { global_logger = this; } From ffefa8929ede21b22c1c37d8415074699778a34c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:05:48 -0500 Subject: [PATCH 17/43] [cc1101] Fix packet mode RSSI/LQI (#12630) Co-authored-by: Claude --- esphome/components/cc1101/cc1101.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index f98afd94a1..7e5309e165 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -169,14 +169,16 @@ void CC1101Component::loop() { } // Read packet - uint8_t payload_length; + uint8_t payload_length, expected_rx; if (this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE)) { this->read_(Register::FIFO, &payload_length, 1); + expected_rx = payload_length + 1; } else { payload_length = this->state_.PKTLEN; + expected_rx = payload_length; } - if (payload_length == 0 || payload_length > 64) { - ESP_LOGW(TAG, "Invalid payload length: %u", payload_length); + if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) { + ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length); this->enter_idle_(); this->strobe_(Command::FRX); this->strobe_(Command::RX); @@ -186,13 +188,12 @@ void CC1101Component::loop() { this->packet_.resize(payload_length); this->read_(Register::FIFO, this->packet_.data(), payload_length); - // Read status and trigger - uint8_t status[2]; - this->read_(Register::FIFO, status, 2); - int8_t rssi_raw = static_cast(status[0]); - float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET; - bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0; - uint8_t lqi = status[1] & STATUS_LQI_MASK; + // Read status from registers (more reliable than FIFO status bytes due to timing issues) + this->read_(Register::RSSI); + this->read_(Register::LQI); + float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET; + bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0; + uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK; if (this->state_.CRC_EN == 0 || crc_ok) { this->packet_trigger_->trigger(this->packet_, rssi, lqi); } @@ -616,12 +617,15 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x01; // Set max RX FIFO threshold to ensure we only trigger on end-of-packet this->state_.FIFO_THR = 15; + // Don't append status bytes to FIFO - we read from registers instead + this->state_.APPEND_STATUS = 0; } else { // Configure GDO0 for serial data (async serial mode) this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { this->write_(Register::PKTCTRL0); + this->write_(Register::PKTCTRL1); this->write_(Register::IOCFG0); this->write_(Register::FIFOTHR); } From dc943d7e7a5cf2cb7be4081d69fe605ffe0e63ca Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 17 Dec 2025 04:52:28 +0100 Subject: [PATCH 18/43] [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ade7880/sensor.py | 2 +- esphome/components/cc1101/__init__.py | 2 +- esphome/components/esp32_camera/__init__.py | 2 +- esphome/components/esp8266_pwm/output.py | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/ledc/output.py | 4 +++- esphome/components/libretiny_pwm/output.py | 4 +++- esphome/components/pca9685/__init__.py | 2 +- esphome/components/rp2040_pwm/output.py | 2 +- esphome/components/sx126x/__init__.py | 8 ++++++-- esphome/components/sx127x/__init__.py | 8 ++++++-- 11 files changed, 25 insertions(+), 13 deletions(-) diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 39dbeb225f..beb74d7310 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ADE7880), cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( - cv.frequency, cv.Range(min=45.0, max=66.0) + cv.frequency, cv.float_range(min=45.0, max=66.0) ), cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 1971817fb1..e314da7079 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -165,7 +165,7 @@ CONFIG_MAP = { CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0), CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False), CONF_DC_BLOCKING_FILTER: cv.boolean, - CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)), + CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), CONF_CHANNEL: cv.uint8_t, diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 2ad48173f1..ca37cb392d 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All( { cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=8e6, max=20e6) + cv.frequency, cv.float_range(min=8e6, max=20e6) ), } ), diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index 1404ef8ac3..2ddf4b9014 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -16,7 +16,7 @@ def valid_pwm_pin(value): esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 9e7c9d702c..7706484e97 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All( nrf52="100kHz", ): cv.All( cv.frequency, - cv.Range(min=0, min_included=False), + cv.float_range(min=0, min_included=False), ), cv.Optional(CONF_TIMEOUT): cv.All( cv.only_with_framework(["arduino", "esp-idf"]), diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 2133c4daf9..7ce79aa514 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -45,7 +45,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), cv.Optional(CONF_PHASE_ANGLE): cv.All( cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0) diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 1eb4869da3..28556514d8 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 56101c2d62..0e238ff7da 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PCA9685Output), cv.Optional(CONF_FREQUENCY): cv.All( - cv.frequency, cv.Range(min=23.84, max=1525.88) + cv.frequency, cv.float_range(min=23.84, max=1525.88) ), cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean, cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum( diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index ac1892fa29..441a52de7f 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"] rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 4641db6483..ed878ed0d4 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -199,9 +199,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True ), diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index b569a75972..f3a9cca93f 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -196,9 +196,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BITSYNC): cv.boolean, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), From 1922455fa74082b8fc22455080b98cd848cdea04 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 19 Dec 2025 20:25:16 -0600 Subject: [PATCH 19/43] [wifi] Fix for `wifi_info` when static IP is configured (#12576) --- esphome/components/wifi/wifi_component_esp8266.cpp | 10 ++++++++++ esphome/components/wifi/wifi_component_esp_idf.cpp | 8 ++++++++ esphome/components/wifi/wifi_component_libretiny.cpp | 8 ++++++++ esphome/components/wifi/wifi_component_pico_w.cpp | 9 +++++++++ 4 files changed, 35 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3b1a442bdb..41594b947c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -528,6 +528,16 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { for (auto *listener : global_wifi_component->connect_state_listeners_) { listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); + config && config->get_manual_ip().has_value()) { + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), + global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); + } + } +#endif #endif break; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4a3c40a119..380e4ea7fd 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -739,6 +739,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { for (auto *listener : this->connect_state_listeners_) { listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 36003a6eb4..1012b0be6c 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -305,6 +305,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ for (auto *listener : this->connect_state_listeners_) { listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif #endif break; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 0228755432..c88aeb2d4f 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -259,6 +259,15 @@ void WiFiComponent::wifi_loop_() { for (auto *listener : this->connect_state_listeners_) { listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); } + // For static IP configurations, notify IP listeners immediately as the IP is already configured +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + s_sta_had_ip = true; + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif #endif } else if (!is_connected && s_sta_was_connected) { // Just disconnected From 726db746c815751618def87a15218ee228be8f98 Mon Sep 17 00:00:00 2001 From: Eduard Llull Date: Sat, 20 Dec 2025 16:59:14 +0100 Subject: [PATCH 20/43] [display_menu_base] Call on_value_ after updating the select (#12584) --- esphome/components/display_menu_base/menu_item.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 8224adf3fe..08f758045e 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_next(true).perform(); + this->on_value_(); changed = true; } @@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_previous(true).perform(); + this->on_value_(); changed = true; } From b055f5b4bf9783d59b72833976a23c2a835606ec Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Sat, 20 Dec 2025 10:18:20 -0800 Subject: [PATCH 21/43] [hub75] Bump esp-hub75 version to 0.1.7 (#12564) --- .clang-tidy.hash | 2 +- esphome/components/hub75/display.py | 46 ++++++++++++++--------------- esphome/idf_component.yml | 4 +++ platformio.ini | 2 -- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index a3322ba731..240b205158 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07 +4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9 diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 81dd4ffc1c..0518731a6a 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -93,35 +93,35 @@ CONF_DOUBLE_BUFFER = "double_buffer" CONF_MIN_REFRESH_RATE = "min_refresh_rate" # Map to hub75 library enums (in global namespace) -ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True) +Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True) SHIFT_DRIVERS = { - "GENERIC": ShiftDriver.GENERIC, - "FM6126A": ShiftDriver.FM6126A, - "ICN2038S": ShiftDriver.ICN2038S, - "FM6124": ShiftDriver.FM6124, - "MBI5124": ShiftDriver.MBI5124, - "DP3246": ShiftDriver.DP3246, + "GENERIC": Hub75ShiftDriver.GENERIC, + "FM6126A": Hub75ShiftDriver.FM6126A, + "ICN2038S": Hub75ShiftDriver.ICN2038S, + "FM6124": Hub75ShiftDriver.FM6124, + "MBI5124": Hub75ShiftDriver.MBI5124, + "DP3246": Hub75ShiftDriver.DP3246, } -PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True) +Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True) PANEL_LAYOUTS = { - "HORIZONTAL": PanelLayout.HORIZONTAL, - "TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN, - "TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN, - "BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP, - "BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP, - "TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG, - "TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG, - "BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG, - "BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG, + "HORIZONTAL": Hub75PanelLayout.HORIZONTAL, + "TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN, + "TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN, + "BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP, + "BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP, + "TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG, + "TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG, + "BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG, + "BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG, } -ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True) +Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) SCAN_PATTERNS = { - "STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN, - "FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH, - "FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH, - "FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH, + "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, + "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH, + "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH, + "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH, } Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) @@ -528,7 +528,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.1.6", + ref="0.1.7", ) # Set compile-time configuration via defines diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 9bb5967248..4573391bc1 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -27,3 +27,7 @@ dependencies: version: "1.7.6~1" rules: - if: "target in [esp32s2, esp32s3, esp32p4]" + esphome/esp-hub75: + version: 0.1.7 + rules: + - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" diff --git a/platformio.ini b/platformio.ini index d37c798c05..a27fb1f537 100644 --- a/platformio.ini +++ b/platformio.ini @@ -156,7 +156,6 @@ lib_deps = esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@2.0.1 ; audio - esphome/esp-hub75@0.1.6 ; hub75 build_flags = ${common:arduino.build_flags} @@ -180,7 +179,6 @@ lib_deps = droscy/esp_wireguard@0.4.2 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word esphome/esp-audio-libs@2.0.1 ; audio - esphome/esp-hub75@0.1.6 ; hub75 build_flags = ${common:idf.build_flags} -Wno-nonnull-compare From 086ec770ea3dfef98abafd3fe30deeecc6f61e3a Mon Sep 17 00:00:00 2001 From: Leo Bergolth Date: Sat, 20 Dec 2025 21:04:59 +0100 Subject: [PATCH 22/43] send NIL ("-") as timestamp if time source is not valid (#12588) --- esphome/components/syslog/esphome_syslog.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index f5c20c891e..851fb30c22 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -34,7 +34,15 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level]; } int pri = this->facility_ * 8 + severity; - auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S"); + auto now = this->time_->now(); + std::string timestamp; + if (now.is_valid()) { + timestamp = now.strftime("%b %e %H:%M:%S"); + } else { + // RFC 5424: A syslog application MUST use the NILVALUE as TIMESTAMP if the syslog application is incapable of + // obtaining system time. + timestamp = "-"; + } size_t len = message_len; // remove color formatting if (this->strip_ && message[0] == 0x1B && len > 11) { From 61ec3508ed75f826ab0f20dacceadc1f212985bf Mon Sep 17 00:00:00 2001 From: Anna Oake Date: Sun, 21 Dec 2025 19:04:17 +0100 Subject: [PATCH 23/43] [cc1101] Fix option defaults and move them to YAML (#12608) --- esphome/components/cc1101/__init__.py | 95 +++++++++++++++++---------- esphome/components/cc1101/cc1101.cpp | 17 ----- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index e314da7079..c205ff2f69 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -160,41 +160,63 @@ HYST_LEVEL = { "High": HystLevel.HYST_LEVEL_HIGH, } -# Config key -> Validator mapping +# Optional settings to generate setter calls for CONFIG_MAP = { - CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0), - CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False), - CONF_DC_BLOCKING_FILTER: cv.boolean, - CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), - CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), - CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), - CONF_CHANNEL: cv.uint8_t, - CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), - CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), - CONF_MSK_DEVIATION: cv.int_range(min=1, max=8), - CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000), - CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False), - CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean, - CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False), - CONF_MANCHESTER: cv.boolean, - CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7), - CONF_SYNC1: cv.hex_uint8_t, - CONF_SYNC0: cv.hex_uint8_t, - CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False), - CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False), - CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False), - CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7), - CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False), - CONF_LNA_PRIORITY: cv.boolean, - CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False), - CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False), - CONF_FREEZE: cv.enum(FREEZE, upper=False), - CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False), - CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False), - CONF_PACKET_MODE: cv.boolean, - CONF_PACKET_LENGTH: cv.uint8_t, - CONF_CRC_ENABLE: cv.boolean, - CONF_WHITENING: cv.boolean, + cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0), + cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum( + RX_ATTENUATION, upper=False + ), + cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean, + cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All( + cv.frequency, cv.float_range(min=300.0e6, max=928.0e6) + ), + cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All( + cv.frequency, cv.float_range(min=25000, max=788000) + ), + cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All( + cv.frequency, cv.float_range(min=58000, max=812000) + ), + cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t, + cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All( + cv.frequency, cv.float_range(min=25000, max=405000) + ), + cv.Optional(CONF_FSK_DEVIATION): cv.All( + cv.frequency, cv.float_range(min=1500, max=381000) + ), + cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8), + cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000), + cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False), + cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean, + cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum( + MODULATION, upper=False + ), + cv.Optional(CONF_MANCHESTER, default=False): cv.boolean, + cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7), + cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t, + cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t, + cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False), + cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum( + MAX_LNA_GAIN, upper=False + ), + cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False), + cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7), + cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum( + CARRIER_SENSE_REL_THR, upper=False + ), + cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean, + cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum( + FILTER_LENGTH_FSK_MSK, upper=False + ), + cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum( + FILTER_LENGTH_ASK_OOK, upper=False + ), + cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False), + cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False), + cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False), + cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean, + cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t, + cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_WHITENING, default=False): cv.boolean, } @@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), } ) - .extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()}) + .extend(CONFIG_MAP) .extend(cv.COMPONENT_SCHEMA) .extend(spi.spi_device_schema(cs_pin_required=True)), _validate_packet_mode, @@ -229,7 +251,8 @@ async def to_code(config): await cg.register_component(var, config) await spi.register_spi_device(var, config) - for key in CONFIG_MAP: + for opt in CONFIG_MAP: + key = opt.schema if key in config: cg.add(getattr(var, f"set_{key}")(config[key])) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 1fe402d6c6..f98afd94a1 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -98,25 +98,8 @@ CC1101Component::CC1101Component() { this->state_.LENGTH_CONFIG = 2; this->state_.FS_AUTOCAL = 1; - // Default Settings - this->set_frequency(433920000); - this->set_if_frequency(153000); - this->set_filter_bandwidth(203000); - this->set_channel(0); - this->set_channel_spacing(200000); - this->set_symbol_rate(5000); - this->set_sync_mode(SyncMode::SYNC_MODE_NONE); - this->set_carrier_sense_above_threshold(true); - this->set_modulation_type(Modulation::MODULATION_ASK_OOK); - this->set_magn_target(MagnTarget::MAGN_TARGET_42DB); - this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT); - this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3); - this->set_lna_priority(false); - this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES); - // CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence) memset(this->pa_table_, 0, sizeof(this->pa_table_)); - this->set_output_power(10.0f); } void CC1101Component::setup() { From 6054685daee93e6c4baad681df16024d88613112 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Dec 2025 09:04:43 -1000 Subject: [PATCH 24/43] [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) --- .../components/esp32_camera/esp32_camera.cpp | 18 +++++++++++++++++- esphome/components/esp32_camera/esp32_camera.h | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 5080a6f32d..a3677330ca 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -11,6 +11,9 @@ namespace esphome { namespace esp32_camera { static const char *const TAG = "esp32_camera"; +#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE +static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; +#endif /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { @@ -204,7 +207,20 @@ void ESP32Camera::loop() { } this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); - ESP_LOGD(TAG, "Got Image: len=%u", fb->len); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Got Image: len=%u", fb->len); +#else + // Initialize log time on first frame to ensure accurate interval measurement + if (this->frame_count_ == 0) { + this->last_log_time_ = now; + } + this->frame_count_++; + if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) { + ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000); + this->last_log_time_ = now; + this->frame_count_ = 0; + } +#endif for (auto *listener : this->listeners_) { listener->on_camera_image(this->current_image_); } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 54a7d6064a..a49fca6511 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera { uint32_t last_idle_request_{0}; uint32_t last_update_{0}; +#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE + uint32_t last_log_time_{0}; + uint16_t frame_count_{0}; +#endif #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C From c8fb694dcbdc1b837fcaf6683b13e43722bd6431 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:05:48 -0500 Subject: [PATCH 25/43] [cc1101] Fix packet mode RSSI/LQI (#12630) Co-authored-by: Claude --- esphome/components/cc1101/cc1101.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index f98afd94a1..7e5309e165 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -169,14 +169,16 @@ void CC1101Component::loop() { } // Read packet - uint8_t payload_length; + uint8_t payload_length, expected_rx; if (this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE)) { this->read_(Register::FIFO, &payload_length, 1); + expected_rx = payload_length + 1; } else { payload_length = this->state_.PKTLEN; + expected_rx = payload_length; } - if (payload_length == 0 || payload_length > 64) { - ESP_LOGW(TAG, "Invalid payload length: %u", payload_length); + if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) { + ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length); this->enter_idle_(); this->strobe_(Command::FRX); this->strobe_(Command::RX); @@ -186,13 +188,12 @@ void CC1101Component::loop() { this->packet_.resize(payload_length); this->read_(Register::FIFO, this->packet_.data(), payload_length); - // Read status and trigger - uint8_t status[2]; - this->read_(Register::FIFO, status, 2); - int8_t rssi_raw = static_cast(status[0]); - float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET; - bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0; - uint8_t lqi = status[1] & STATUS_LQI_MASK; + // Read status from registers (more reliable than FIFO status bytes due to timing issues) + this->read_(Register::RSSI); + this->read_(Register::LQI); + float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET; + bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0; + uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK; if (this->state_.CRC_EN == 0 || crc_ok) { this->packet_trigger_->trigger(this->packet_, rssi, lqi); } @@ -616,12 +617,15 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x01; // Set max RX FIFO threshold to ensure we only trigger on end-of-packet this->state_.FIFO_THR = 15; + // Don't append status bytes to FIFO - we read from registers instead + this->state_.APPEND_STATUS = 0; } else { // Configure GDO0 for serial data (async serial mode) this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { this->write_(Register::PKTCTRL0); + this->write_(Register::PKTCTRL1); this->write_(Register::IOCFG0); this->write_(Register::FIFOTHR); } From 0922f240e0ab48a8dd86e59876980481358a47ca Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:23:04 -0500 Subject: [PATCH 26/43] Bump version to 2025.12.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 4c533ec87f..d41459ea46 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.12.1 +PROJECT_NUMBER = 2025.12.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 8f9a3497ff..41bb419aaf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.1" +__version__ = "2025.12.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ebb6babb3d9db6952c0dd6af6c4cc14c898d94b1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:26:38 -0500 Subject: [PATCH 27/43] Fix hash --- .clang-tidy.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 240b205158..18379de92e 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9 +5969e705693278d984c5292e998df0cbaf34f7e1f04dfc7f7b7ad7168527bfa7 From 0c566c6f00b1bf2fc11a4bb019f1f9292c2f0d5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Dec 2025 06:59:07 -1000 Subject: [PATCH 28/43] [core] Deprecate get_object_id() and migrate remaining usages to get_object_id_to() (#12629) --- esphome/components/pid/pid_climate.cpp | 6 +++--- esphome/components/prometheus/prometheus_handler.cpp | 6 +++++- esphome/core/entity_base.h | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index fd74eabd87..2094c0e942 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -162,14 +162,14 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { float min_value = this->supports_cool_() ? -1.0f : 0.0f; float max_value = this->supports_heat_() ? 1.0f : 0.0f; this->autotuner_->config(min_value, max_value); - this->autotuner_->set_autotuner_id(this->get_object_id()); + this->autotuner_->set_autotuner_id(this->get_name()); ESP_LOGI(TAG, "%s: Autotune has started. This can take a long time depending on the " "responsiveness of your system. Your system " "output will be altered to deliberately oscillate above and below the setpoint multiple times. " "Until your sensor provides a reading, the autotuner may display \'nan\'", - this->get_object_id().c_str()); + this->get_name().c_str()); this->set_interval("autotune-progress", 10000, [this]() { if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) @@ -178,7 +178,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { if (mode != climate::CLIMATE_MODE_HEAT_COOL) { ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!", - this->get_object_id().c_str()); + this->get_name().c_str()); } } diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 4b5d834ebf..88b357041a 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -112,7 +112,11 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { std::string PrometheusHandler::relabel_id_(EntityBase *obj) { auto item = relabel_map_id_.find(obj); - return item == relabel_map_id_.end() ? obj->get_object_id() : item->second; + if (item != relabel_map_id_.end()) { + return item->second; + } + char object_id_buf[OBJECT_ID_MAX_LEN]; + return obj->get_object_id_to(object_id_buf).str(); } std::string PrometheusHandler::relabel_name_(EntityBase *obj) { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index eb1ba46c94..93f989934a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -33,6 +33,15 @@ class EntityBase { bool has_own_name() const { return this->flags_.has_own_name; } // Get the sanitized name of this Entity as an ID. + // Deprecated: object_id mangles names and all object_id methods are planned for removal. + // See https://github.com/esphome/backlog/issues/76 + // Now is the time to stop using object_id entirely. If you still need it temporarily, + // use get_object_id_to() which will remain available longer but will also eventually be removed. + ESPDEPRECATED("object_id mangles names and all object_id methods are planned for removal " + "(see https://github.com/esphome/backlog/issues/76). " + "Now is the time to stop using object_id. If still needed, use get_object_id_to() " + "which will remain available longer. get_object_id() will be removed in 2026.7.0", + "2025.12.0") std::string get_object_id() const; void set_object_id(const char *object_id); From 958a35e26279a302f6da4d0efd8b7191355b09d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:17:52 -1000 Subject: [PATCH 29/43] Bump aioesphomeapi from 43.5.0 to 43.6.0 (#12644) 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 5718ced617..e741a70f48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==43.5.0 +aioesphomeapi==43.6.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.17 # dashboard_import From 4f706636584137d9c697320928162c8743b03e91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Dec 2025 07:57:33 -1000 Subject: [PATCH 30/43] [alarm_control_panel] Use C++17 nested namespace and remove unused include (#12662) --- .../alarm_control_panel/alarm_control_panel.cpp | 6 ++---- .../components/alarm_control_panel/alarm_control_panel.h | 8 ++------ .../alarm_control_panel/alarm_control_panel_call.cpp | 6 ++---- .../alarm_control_panel/alarm_control_panel_call.h | 6 ++---- .../alarm_control_panel/alarm_control_panel_state.cpp | 6 ++---- .../alarm_control_panel/alarm_control_panel_state.h | 6 ++---- esphome/components/alarm_control_panel/automation.h | 6 ++---- 7 files changed, 14 insertions(+), 30 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index f938155dd3..89c0908a74 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -8,8 +8,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { static const char *const TAG = "alarm_control_panel"; @@ -115,5 +114,4 @@ void AlarmControlPanel::disarm(optional code) { call.perform(); } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 59ccf0e484..340f15bcd6 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "alarm_control_panel_call.h" #include "alarm_control_panel_state.h" @@ -9,8 +7,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { enum AlarmControlPanelFeature : uint8_t { // Matches Home Assistant values @@ -141,5 +138,4 @@ class AlarmControlPanel : public EntityBase { LazyCallbackManager ready_callback_{}; }; -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index 7bb9b9989c..5e98d58368 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { static const char *const TAG = "alarm_control_panel"; @@ -99,5 +98,4 @@ void AlarmControlPanelCall::perform() { } } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index 034e3142da..cff00900dd 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { class AlarmControlPanel; @@ -36,5 +35,4 @@ class AlarmControlPanelCall { void validate_(); }; -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index abe6f51995..862c620497 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,7 +1,6 @@ #include "alarm_control_panel_state.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { switch (state) { @@ -30,5 +29,4 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat } } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.h b/esphome/components/alarm_control_panel/alarm_control_panel_state.h index ad16222dc0..dd0b91f064 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.h @@ -3,8 +3,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { enum AlarmControlPanelState : uint8_t { ACP_STATE_DISARMED = 0, @@ -25,5 +24,4 @@ enum AlarmControlPanelState : uint8_t { */ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index af4a14e27a..ce5ceadb47 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "alarm_control_panel.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { /// Trigger on any state change class StateTrigger : public Trigger<> { @@ -165,5 +164,4 @@ template class AlarmControlPanelCondition : public Condition Date: Fri, 26 Dec 2025 07:58:46 -1000 Subject: [PATCH 31/43] [text_sensor] Return state by const reference to avoid copies (#12661) --- esphome/components/text_sensor/text_sensor.cpp | 4 ++-- esphome/components/text_sensor/text_sensor.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index ad1dc0f521..8dfb9dad05 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -78,8 +78,8 @@ void TextSensor::add_on_raw_state_callback(std::functionraw_callback_.add(std::move(callback)); } -std::string TextSensor::get_state() const { return this->state; } -std::string TextSensor::get_raw_state() const { +const std::string &TextSensor::get_state() const { return this->state; } +const std::string &TextSensor::get_raw_state() const { // Suppress deprecation warning - get_raw_state() is the replacement API #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 919bf81c8c..2cd8a65e87 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -37,9 +37,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { #pragma GCC diagnostic pop /// Getter-syntax for .state. - std::string get_state() const; + const std::string &get_state() const; /// Getter-syntax for .raw_state - std::string get_raw_state() const; + const std::string &get_raw_state() const; void publish_state(const std::string &state); From 0919017d496a8c3613b4fbaeff2447986781201b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Dec 2025 08:44:03 -1000 Subject: [PATCH 32/43] [wifi] Avoid unnecessary string copy in failed connection logging (#12659) --- esphome/components/wifi/wifi_component.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 242265344d..5fa894d8f9 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1523,12 +1523,12 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { return; // No BSSID to penalize } - // Get SSID for logging - std::string ssid; + // Get SSID for logging (use pointer to avoid copy) + const std::string *ssid = nullptr; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { - ssid = this->scan_result_[0].get_ssid(); + ssid = &this->scan_result_[0].get_ssid(); } else if (const WiFiAP *config = this->get_selected_sta_()) { - ssid = config->get_ssid(); + ssid = &config->get_ssid(); } // Only decrease priority on the last attempt for this phase @@ -1548,8 +1548,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } char bssid_s[18]; format_mac_addr_upper(failed_bssid.value().data(), bssid_s); - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, - old_priority, new_priority); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", + ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start From f1fecd22e30748759490ca10be1fd4848a08eb72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Dec 2025 08:44:17 -1000 Subject: [PATCH 33/43] [web_server] Move HTTP header strings to flash on ESP8266 (#12668) --- esphome/components/web_server/web_server.cpp | 23 +++++++------------ .../web_server_base/web_server_base.h | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index df8a5364cf..8a1ed49408 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -45,13 +45,6 @@ static const char *const TAG = "web_server"; static constexpr size_t PSTR_LOCAL_SIZE = 18; #define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) -#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS -static const char *const HEADER_PNA_NAME = "Private-Network-Access-Name"; -static const char *const HEADER_PNA_ID = "Private-Network-Access-ID"; -static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-Network"; -static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network"; -#endif - // Parse URL and return match info static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) { UrlMatch match{}; @@ -348,7 +341,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #elif USE_WEBSERVER_VERSION >= 2 @@ -368,10 +361,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); - response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); - response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); + response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); + response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); char mac_s[18]; - response->addHeader(HEADER_PNA_ID, get_mac_address_pretty_into_buffer(mac_s)); + response->addHeader(ESPHOME_F("Private-Network-Access-ID"), get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif @@ -385,7 +378,7 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #endif @@ -399,7 +392,7 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #endif @@ -1841,7 +1834,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { } #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) + if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) return true; #endif @@ -1974,7 +1967,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { + if (request->method() == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) { this->handle_pna_cors_request(request); return; } diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 54ec997671..7e95e00f29 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -100,7 +100,7 @@ class WebServerBase : public Component { } this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + DefaultHeaders::Instance().addHeader(ESPHOME_F("Access-Control-Allow-Origin"), ESPHOME_F("*")); this->server_->begin(); for (auto *handler : this->handlers_) From 5a2e0612a818a73543fca92d5126b9dda523d1b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Dec 2025 08:44:34 -1000 Subject: [PATCH 34/43] [web_server] Use C++17 nested namespace syntax (#12663) --- esphome/components/web_server/list_entities.cpp | 6 ++---- esphome/components/web_server/list_entities.h | 11 +++++------ esphome/components/web_server/ota/ota_web_server.cpp | 6 ++---- esphome/components/web_server/ota/ota_web_server.h | 6 ++---- esphome/components/web_server/server_index_v2.h | 6 ++---- esphome/components/web_server/server_index_v3.h | 6 ++---- esphome/components/web_server/web_server.cpp | 6 ++---- esphome/components/web_server/web_server.h | 6 ++---- esphome/components/web_server/web_server_v1.cpp | 6 ++---- 9 files changed, 21 insertions(+), 38 deletions(-) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 16b1d1e797..55beed812f 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -6,8 +6,7 @@ #include "web_server.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { #ifdef USE_ESP32 ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} @@ -157,6 +156,5 @@ bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { } #endif -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 5d9049b082..56fd91a8c6 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -4,13 +4,13 @@ #ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -namespace esphome { +namespace esphome::web_server_idf { #ifdef USE_ESP32 -namespace web_server_idf { class AsyncEventSource; -} #endif -namespace web_server { +} // namespace esphome::web_server_idf + +namespace esphome::web_server { #if !defined(USE_ESP32) && defined(USE_ARDUINO) class DeferredUpdateEventSource; @@ -99,6 +99,5 @@ class ListEntitiesIterator : public ComponentIterator { #endif }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index f612aa056c..572c351245 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -23,8 +23,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { static const char *const TAG = "web_server.ota"; @@ -236,7 +235,6 @@ void WebServerOTAComponent::setup() { void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h index a7170c0e34..53ff99899c 100644 --- a/esphome/components/web_server/ota/ota_web_server.h +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -7,8 +7,7 @@ #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { class WebServerOTAComponent : public ota::OTAComponent { public: @@ -20,7 +19,6 @@ class WebServerOTAComponent : public ota::OTAComponent { friend class OTARequestHandler; }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index e675d81552..b2d204c9e7 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcd, 0x7d, 0xdb, 0x72, 0xdb, 0xc6, 0xb6, 0xe0, 0xf3, @@ -644,8 +643,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, 0x92, 0xde, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, 0x4f, 0xc7, 0xf5, 0xfe, 0x1f, 0xe7, 0xe0, 0x1c, 0x12, 0xcf, 0x89, 0x00, 0x00}; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif #endif diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index 39518197a3..8a8ced9153 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0xeb, 0x7a, 0x1b, 0xb7, 0xb2, 0x20, 0xfa, @@ -4048,8 +4047,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x3b, 0x6c, 0x78, 0x02, 0xa6, 0xdc, 0xb4, 0xe8, 0xee, 0x6a, 0xc5, 0x97, 0x94, 0x7e, 0xd1, 0x9b, 0x83, 0x45, 0xb2, 0xf4, 0x87, 0xff, 0x07, 0x52, 0xaf, 0x09, 0x6c, 0x30, 0x6a, 0x03, 0x00}; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 8a1ed49408..f613d6bc36 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -36,8 +36,7 @@ #endif #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { static const char *const TAG = "web_server"; @@ -2105,6 +2104,5 @@ void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_na } #endif -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 0078146284..b9e852c745 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -33,8 +33,7 @@ extern const uint8_t ESPHOME_WEBSERVER_JS_INCLUDE[] PROGMEM; extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { @@ -616,6 +615,5 @@ class WebServer : public Controller, #endif }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index cbc25b9dec..e27306ad78 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -3,8 +3,7 @@ #if USE_WEBSERVER_VERSION == 1 -namespace esphome { -namespace web_server { +namespace esphome::web_server { void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, const std::function &action_func = nullptr) { @@ -215,6 +214,5 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { request->send(stream); } -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif From bdc087148a541241277ce13c489763c8064efa6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Dec 2025 12:52:41 -1000 Subject: [PATCH 35/43] [wifi_info] Reduce heap allocations in text sensor formatting (#12660) --- esphome/components/network/ip_address.h | 9 ++++ .../wifi_info/wifi_info_text_sensor.cpp | 45 +++++++++++++------ esphome/core/helpers.h | 24 ++++++++++ 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 3d8b062d0b..27cc212a47 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -40,6 +40,9 @@ using ip4_addr_t = in_addr; namespace esphome { namespace network { +/// Buffer size for IP address string (IPv6 max: 39 chars + null) +static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40; + struct IPAddress { public: #ifdef USE_HOST @@ -50,6 +53,10 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); } + /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. + char *str_to(char *buf) const { + return const_cast(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE)); + } #else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { @@ -128,6 +135,8 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } + /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. + char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } IPAddress &operator+=(uint8_t increase) { diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 56cf49028c..eae0f87b40 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -46,8 +46,13 @@ void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this void DNSAddressWifiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, const network::IPAddress &dns2) { - std::string dns_results = dns1.str() + " " + dns2.str(); - this->publish_state(dns_results); + // IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot + char buf[network::IP_ADDRESS_BUFFER_SIZE * 2]; + dns1.str_to(buf); + size_t len1 = strlen(buf); + buf[len1] = ' '; + dns2.str_to(buf + len1 + 1); + this->publish_state(buf); } /********************** @@ -58,22 +63,36 @@ void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_scan_result void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } +// Format: "SSID: -XXdB\n" - caller must ensure ssid_len + 9 bytes available in buffer +static char *format_scan_entry(char *buf, const char *ssid, size_t ssid_len, int8_t rssi) { + memcpy(buf, ssid, ssid_len); + buf += ssid_len; + *buf++ = ':'; + *buf++ = ' '; + buf = int8_to_str(buf, rssi); + *buf++ = 'd'; + *buf++ = 'B'; + *buf++ = '\n'; + return buf; +} + void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) { - std::string scan_results; + char buf[MAX_STATE_LENGTH + 1]; + char *ptr = buf; + const char *end = buf + MAX_STATE_LENGTH; + for (const auto &scan : results) { if (scan.get_is_hidden()) continue; + const std::string &ssid = scan.get_ssid(); + // Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9 + if (ptr + ssid.size() + 9 > end) + break; + ptr = format_scan_entry(ptr, ssid.c_str(), ssid.size(), scan.get_rssi()); + } - scan_results += scan.get_ssid(); - scan_results += ": "; - scan_results += esphome::to_string(scan.get_rssi()); - scan_results += "dB\n"; - } - // There's a limit of 255 characters per state; longer states just don't get sent so we truncate it - if (scan_results.length() > MAX_STATE_LENGTH) { - scan_results.resize(MAX_STATE_LENGTH); - } - this->publish_state(scan_results); + *ptr = '\0'; + this->publish_state(buf); } /*************** diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 769041160c..48a2313e2c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -684,6 +684,30 @@ inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + /// This always uses uppercase (A-F) for pretty/human-readable output inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } +/// Write int8 value to buffer without modulo operations. +/// Buffer must have at least 4 bytes free. Returns pointer past last char written. +inline char *int8_to_str(char *buf, int8_t val) { + int32_t v = val; + if (v < 0) { + *buf++ = '-'; + v = -v; + } + if (v >= 100) { + *buf++ = '1'; // int8 max is 128, so hundreds digit is always 1 + v -= 100; + // Must write tens digit (even if 0) after hundreds + int32_t tens = v / 10; + *buf++ = '0' + tens; + v -= tens * 10; + } else if (v >= 10) { + int32_t tens = v / 10; + *buf++ = '0' + tens; + v -= tens * 10; + } + *buf++ = '0' + v; + return buf; +} + /// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase) inline void format_mac_addr_upper(const uint8_t *mac, char *output) { for (size_t i = 0; i < 6; i++) { From 34067f8b15b7159e8391c9905a301e110d610e94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:29:15 -1000 Subject: [PATCH 36/43] [esp8266] Native OTA backend to reduce Arduino dependencies (#12675) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp8266/__init__.py | 8 +- .../esp8266/exclude_updater.py.script | 21 ++ .../components/esphome/ota/ota_esphome.cpp | 2 +- .../http_request/ota/ota_http_request.cpp | 2 +- esphome/components/ota/__init__.py | 2 +- .../ota/ota_backend_arduino_esp8266.cpp | 89 ----- .../ota/ota_backend_arduino_esp8266.h | 33 -- .../components/ota/ota_backend_esp8266.cpp | 356 ++++++++++++++++++ esphome/components/ota/ota_backend_esp8266.h | 58 +++ .../web_server/ota/ota_web_server.cpp | 7 +- 10 files changed, 446 insertions(+), 132 deletions(-) create mode 100644 esphome/components/esp8266/exclude_updater.py.script delete mode 100644 esphome/components/ota/ota_backend_arduino_esp8266.cpp delete mode 100644 esphome/components/ota/ota_backend_arduino_esp8266.h create mode 100644 esphome/components/ota/ota_backend_esp8266.cpp create mode 100644 esphome/components/ota/ota_backend_esp8266.h diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index a74f9ee8ce..c4969a79b2 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -191,7 +191,8 @@ async def to_code(config): cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option( - "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] + "extra_scripts", + ["pre:testing_mode.py", "pre:exclude_updater.py", "post:post_build.py"], ) conf = config[CONF_FRAMEWORK] @@ -278,3 +279,8 @@ def copy_files(): testing_mode_file, CORE.relative_build_path("testing_mode.py"), ) + exclude_updater_file = dir / "exclude_updater.py.script" + copy_file_if_changed( + exclude_updater_file, + CORE.relative_build_path("exclude_updater.py"), + ) diff --git a/esphome/components/esp8266/exclude_updater.py.script b/esphome/components/esp8266/exclude_updater.py.script new file mode 100644 index 0000000000..69331e3b03 --- /dev/null +++ b/esphome/components/esp8266/exclude_updater.py.script @@ -0,0 +1,21 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os + +# Filter out Updater.cpp from the Arduino core build +# This saves 228 bytes of .bss by not instantiating the global Update object +# ESPHome uses its own native OTA backend instead + + +def filter_updater_from_core(env, node): + """Filter callback to exclude Updater.cpp from framework build.""" + path = node.get_path() + if path.endswith("Updater.cpp"): + print(f"ESPHome: Excluding {os.path.basename(path)} from build (using native OTA backend)") + return None + return node + + +# Apply the filter to framework sources +env.AddBuildMiddleware(filter_updater_from_core, "**/cores/esp8266/Updater.cpp") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b589a6119f..f9984e1425 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -10,7 +10,7 @@ #endif #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 058579752e..2cd7489e38 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -7,7 +7,7 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 8bed9cee42..a514a7482f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -148,7 +148,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "ota_backend_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, "ota_backend_arduino_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp deleted file mode 100644 index 375c4e7200..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend_arduino_esp8266.h" -#include "ota_backend.h" - -#include "esphome/components/esp8266/preferences.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota { - -static const char *const TAG = "ota.arduino_esp8266"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { - // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space - if (image_size == 0) { - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; - } - bool ret = Update.begin(image_size, U_FLASH); - if (ret) { - esp8266::preferences_prevent_write(true); - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_BOOTSTRAP) - return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - if (error == UPDATE_ERROR_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - if (error == UPDATE_ERROR_SPACE) - return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { - Update.setMD5(md5); - this->md5_set_ = true; -} - -OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { - size_t written = Update.write(data, len); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP8266OTABackend::end() { - // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 - // This matches the behavior of the old web_server OTA implementation - bool success = Update.end(!this->md5_set_); - - // On ESP8266, Update.end() might return false even with error code 0 - // Check the actual error code to determine success - uint8_t error = Update.getError(); - - if (success || error == UPDATE_ERROR_OK) { - return OTA_RESPONSE_OK; - } - - ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP8266OTABackend::abort() { - Update.end(); - esp8266::preferences_prevent_write(false); -} - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h deleted file mode 100644 index e1b9015cc7..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace ota { - -class ArduinoESP8266OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - bool supports_compression() override { return true; } -#else - bool supports_compression() override { return false; } -#endif - - private: - bool md5_set_{false}; -}; - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp new file mode 100644 index 0000000000..4b84708cd9 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -0,0 +1,356 @@ +#ifdef USE_ESP8266 +#include "ota_backend_esp8266.h" +#include "ota_backend.h" + +#include "esphome/components/esp8266/preferences.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include + +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +// Note: FLASH_SECTOR_SIZE (0x1000) is already defined in spi_flash_geometry.h + +// Flash header offsets +static constexpr uint8_t FLASH_MODE_OFFSET = 2; + +// Firmware magic bytes +static constexpr uint8_t FIRMWARE_MAGIC = 0xE9; +static constexpr uint8_t GZIP_MAGIC_1 = 0x1F; +static constexpr uint8_t GZIP_MAGIC_2 = 0x8B; + +// ESP8266 flash memory base address (memory-mapped flash starts here) +static constexpr uint32_t FLASH_BASE_ADDRESS = 0x40200000; + +// Boot mode extraction from GPI register (bits 16-19 contain boot mode) +static constexpr int BOOT_MODE_SHIFT = 16; +static constexpr int BOOT_MODE_MASK = 0xf; + +// Boot mode indicating UART download mode (OTA not possible) +static constexpr int BOOT_MODE_UART_DOWNLOAD = 1; + +// Minimum buffer size when memory is constrained +static constexpr size_t MIN_BUFFER_SIZE = 256; + +namespace esphome::ota { + +static const char *const TAG = "ota.esp8266"; + +std::unique_ptr make_ota_backend() { return make_unique(); } + +OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space + if (image_size == 0) { + // Round down to sector boundary: subtract one sector, then mask to sector alignment + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + image_size = (ESP.getFreeSketchSpace() - FLASH_SECTOR_SIZE) & ~(FLASH_SECTOR_SIZE - 1); + } + + // Check boot mode - if boot mode is UART download mode, + // we will not be able to reset into normal mode once update is done + int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK; + if (boot_mode == BOOT_MODE_UART_DOWNLOAD) { + return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; + } + + // Check flash configuration - real size must be >= configured size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (!ESP.checkFlashConfig(false)) { + return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; + } + + // Get current sketch size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t sketch_size = ESP.getSketchSize(); + + // Size of current sketch rounded to sector boundary + uint32_t current_sketch_size = (sketch_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // Size of update rounded to sector boundary + uint32_t rounded_size = (image_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // End of available space for sketch and update (start of filesystem) + uint32_t update_end_address = FS_start - FLASH_BASE_ADDRESS; + + // Calculate start address for the update (write from end backwards) + this->start_address_ = (update_end_address > rounded_size) ? (update_end_address - rounded_size) : 0; + + // Check if there's enough space for both current sketch and update + if (this->start_address_ < current_sketch_size) { + return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; + } + + // Allocate buffer for sector writes (use smaller buffer if memory constrained) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + this->buffer_size_ = (ESP.getFreeHeap() > 2 * FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : MIN_BUFFER_SIZE; + + // ESP8266's umm_malloc guarantees 4-byte aligned allocations, which is required + // for spi_flash_write(). This is the same pattern used by Arduino's Updater class. + this->buffer_ = make_unique(this->buffer_size_); + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + this->current_address_ = this->start_address_; + this->image_size_ = image_size; + this->buffer_len_ = 0; + this->md5_set_ = false; + + // Disable WiFi sleep during update + wifi_set_sleep_type(NONE_SLEEP_T); + + // Prevent preference writes during update + esp8266::preferences_prevent_write(true); + + // Initialize MD5 computation + this->md5_.init(); + + ESP_LOGD(TAG, "OTA begin: start=0x%08" PRIX32 ", size=%zu", this->start_address_, image_size); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::set_update_md5(const char *md5) { + // Parse hex string to bytes + if (parse_hex(md5, this->expected_md5_, 16)) { + this->md5_set_ = true; + } +} + +OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + size_t written = 0; + while (written < len) { + // Calculate how much we can buffer + size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); + memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); + this->buffer_len_ += to_buffer; + written += to_buffer; + + // If buffer is full, write to flash + if (this->buffer_len_ == this->buffer_size_ && !this->write_buffer_()) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + return OTA_RESPONSE_OK; +} + +bool ESP8266OTABackend::erase_sector_if_needed_() { + if ((this->current_address_ % FLASH_SECTOR_SIZE) != 0) { + return true; // Not at sector boundary + } + + App.feed_wdt(); + if (spi_flash_erase_sector(this->current_address_ / FLASH_SECTOR_SIZE) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash erase failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::flash_write_() { + App.feed_wdt(); + if (spi_flash_write(this->current_address_, reinterpret_cast(this->buffer_.get()), this->buffer_len_) != + SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash write failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::write_buffer_() { + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_()) { + return false; + } + + // Patch flash mode in first sector if needed + // This is analogous to what esptool.py does when it receives a --flash_mode argument + bool is_first_sector = (this->current_address_ == this->start_address_); + uint8_t original_flash_mode = 0; + bool patched_flash_mode = false; + + // Only patch if we have enough bytes to access flash mode offset and it's not GZIP + if (is_first_sector && this->buffer_len_ > FLASH_MODE_OFFSET && this->buffer_[0] != GZIP_MAGIC_1) { + // Not GZIP compressed - check and patch flash mode + uint8_t current_flash_mode = this->get_flash_chip_mode_(); + uint8_t buffer_flash_mode = this->buffer_[FLASH_MODE_OFFSET]; + + if (buffer_flash_mode != current_flash_mode) { + original_flash_mode = buffer_flash_mode; + this->buffer_[FLASH_MODE_OFFSET] = current_flash_mode; + patched_flash_mode = true; + } + } + + if (!this->flash_write_()) { + return false; + } + + // Restore original flash mode for MD5 calculation + if (patched_flash_mode) { + this->buffer_[FLASH_MODE_OFFSET] = original_flash_mode; + } + + // Update MD5 with original (unpatched) data + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +bool ESP8266OTABackend::write_buffer_final_() { + // Similar to write_buffer_(), but without flash mode patching or MD5 update (for final padded write) + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_() || !this->flash_write_()) { + return false; + } + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +OTAResponseTypes ESP8266OTABackend::end() { + // Write any remaining buffered data + if (this->buffer_len_ > 0) { + // Add actual data to MD5 before padding + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + // Pad to 4-byte alignment for flash write + while (this->buffer_len_ % 4 != 0) { + this->buffer_[this->buffer_len_++] = 0xFF; + } + if (!this->write_buffer_final_()) { + this->abort(); + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + // Calculate actual bytes written + size_t actual_size = this->current_address_ - this->start_address_; + + // Check if any data was written + if (actual_size == 0) { + ESP_LOGE(TAG, "No data written"); + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Verify MD5 if set (strict mode), otherwise use lenient mode + // In lenient mode (no MD5), we accept whatever was written + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_bytes(this->expected_md5_)) { + ESP_LOGE(TAG, "MD5 mismatch"); + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } + } else { + // Lenient mode: adjust size to what was actually written + // This matches Arduino's Update.end(true) behavior + this->image_size_ = actual_size; + } + + // Verify firmware header + if (!this->verify_end_()) { + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Write eboot command to copy firmware on next boot + eboot_command ebcmd; + ebcmd.action = ACTION_COPY_RAW; + ebcmd.args[0] = this->start_address_; + ebcmd.args[1] = 0x00000; // Destination: start of flash + ebcmd.args[2] = this->image_size_; + eboot_command_write(&ebcmd); + + ESP_LOGI(TAG, "OTA update staged: 0x%08" PRIX32 " -> 0x00000, size=%zu", this->start_address_, this->image_size_); + + // Clean up + this->buffer_.reset(); + esp8266::preferences_prevent_write(false); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::abort() { + this->buffer_.reset(); + this->buffer_len_ = 0; + this->image_size_ = 0; + esp8266::preferences_prevent_write(false); +} + +bool ESP8266OTABackend::verify_end_() { + uint32_t buf; + if (spi_flash_read(this->start_address_, &buf, 4) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Failed to read firmware header"); + return false; + } + + uint8_t *bytes = reinterpret_cast(&buf); + + // Check for GZIP (compressed firmware) + if (bytes[0] == GZIP_MAGIC_1 && bytes[1] == GZIP_MAGIC_2) { + // GZIP compressed - can't verify further + return true; + } + + // Check firmware magic byte + if (bytes[0] != FIRMWARE_MAGIC) { + ESP_LOGE(TAG, "Invalid firmware magic: 0x%02X (expected 0x%02X)", bytes[0], FIRMWARE_MAGIC); + return false; + } + +#if !FLASH_MAP_SUPPORT + // Check if new firmware's flash size fits (only when auto-detection is disabled) + // With FLASH_MAP_SUPPORT (modern cores), flash size is auto-detected from chip + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t bin_flash_size = ESP.magicFlashChipSize((bytes[3] & 0xf0) >> 4); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (bin_flash_size > ESP.getFlashChipRealSize()) { + ESP_LOGE(TAG, "Firmware flash size (%" PRIu32 ") exceeds chip size (%" PRIu32 ")", bin_flash_size, + ESP.getFlashChipRealSize()); + return false; + } +#endif + + return true; +} + +uint8_t ESP8266OTABackend::get_flash_chip_mode_() { + uint32_t data; + if (spi_flash_read(0x0000, &data, 4) != SPI_FLASH_RESULT_OK) { + return 0; // Default to QIO + } + return (reinterpret_cast(&data))[FLASH_MODE_OFFSET]; +} + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h new file mode 100644 index 0000000000..a9d6dd2ccc --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -0,0 +1,58 @@ +#pragma once +#ifdef USE_ESP8266 +#include "ota_backend.h" + +#include "esphome/components/md5/md5.h" +#include "esphome/core/defines.h" + +#include + +namespace esphome::ota { + +/// OTA backend for ESP8266 using native SDK functions. +/// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM +/// by not having a global Update object in .bss. +class ESP8266OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + // Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0) + bool supports_compression() override { return true; } + + protected: + /// Erase flash sector if current address is at sector boundary + bool erase_sector_if_needed_(); + + /// Write buffer to flash (does not update address or clear buffer) + bool flash_write_(); + + /// Write buffered data to flash and update MD5 + bool write_buffer_(); + + /// Write buffered data to flash without MD5 update (for final padded write) + bool write_buffer_final_(); + + /// Verify the firmware header is valid + bool verify_end_(); + + /// Get current flash chip mode from flash header + uint8_t get_flash_chip_mode_(); + + std::unique_ptr buffer_; + size_t buffer_size_{0}; + size_t buffer_len_{0}; + + uint32_t start_address_{0}; + uint32_t current_address_{0}; + size_t image_size_{0}; + + md5::MD5Digest md5_{}; + uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest + bool md5_set_{false}; +}; + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 572c351245..b8bea40b84 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -10,9 +10,7 @@ #endif #ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include -#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) #include #endif #endif // USE_ARDUINO @@ -120,9 +118,6 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Platform-specific pre-initialization #ifdef USE_ARDUINO -#ifdef USE_ESP8266 - Update.runAsync(true); -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) if (Update.isRunning()) { Update.abort(); From e9f2d75aab3a8e31f82e82e30ac7d0bc811e617d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:34:45 -1000 Subject: [PATCH 37/43] [core] Add format_hex_to helper for zero-allocation hex formatting (#12670) --- esphome/components/one_wire/one_wire_bus.cpp | 6 ++++-- esphome/components/sx126x/sx126x.cpp | 4 +++- esphome/components/sx127x/sx127x.cpp | 4 +++- esphome/core/helpers.cpp | 13 +++++++++++++ esphome/core/helpers.h | 18 ++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index c2542177cf..27b7d58a0f 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -49,7 +49,8 @@ void OneWireBus::search() { break; auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); + char hex_buf[17]; + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex_to(hex_buf, address)); } else { this->devices_.push_back(address); } @@ -82,8 +83,9 @@ void OneWireBus::dump_devices_(const char *tag) { ESP_LOGW(tag, " Found no devices!"); } else { ESP_LOGCONFIG(tag, " Found devices:"); + char hex_buf[17]; // uint64_t = 16 hex chars + null for (auto &address : this->devices_) { - ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff))); + ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex_to(hex_buf, address), LOG_STR_ARG(get_model_str(address & 0xff))); } } } diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index bb59f26b79..707d6f1fbf 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -527,7 +527,9 @@ void SX126x::dump_config() { this->spreading_factor_, cr, this->preamble_size_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->is_failed()) { ESP_LOGE(TAG, "Configuring SX126x failed"); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 8e6db5dc9e..3185574b1a 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -476,7 +476,9 @@ void SX127x::dump_config() { ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { ESP_LOGCONFIG(TAG, diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index d7d32ea28f..5e361ecce2 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -297,6 +297,19 @@ std::string format_hex(const uint8_t *data, size_t length) { } std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + size_t max_bytes = (buffer_size - 1) / 2; + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + buffer[2 * i] = format_hex_char(data[i] >> 4); + buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F); + } + buffer[length * 2] = '\0'; + return buffer; +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 48a2313e2c..4319e32510 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -730,6 +730,24 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { output[12] = '\0'; } +/// Format byte array as lowercase hex to buffer (base implementation). +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); + +/// Format byte array as lowercase hex to buffer. Automatically deduces buffer size. +/// Truncates output if data exceeds buffer capacity. Returns pointer to buffer. +template inline char *format_hex_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 3, "Buffer must hold at least one hex byte (3 chars)"); + return format_hex_to(buffer, N, data, length); +} + +/// Format an unsigned integer in lowercased hex to buffer, starting with the most significant byte. +template::value, int> = 0> +inline char *format_hex_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 2 + 1, "Buffer too small for type"); + val = convert_big_endian(val); + return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); +} + /// Format the six-byte array \p mac into a MAC address. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. From a275f37135ae249196691d813b93abc5bcf60dd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:16 -1000 Subject: [PATCH 38/43] [udp] Use stack buffer for listen address logging in dump_config (#12667) --- esphome/components/udp/udp_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 9105ced21e..daa6c52f98 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -130,7 +130,8 @@ void UDPComponent::dump_config() { for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); if (this->listen_address_.has_value()) { - ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); + char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf)); } ESP_LOGCONFIG(TAG, " Broadcasting: %s\n" From be0bf1e5b92a736d0b7bdc6ae95f60a68e92e2ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:36 -1000 Subject: [PATCH 39/43] [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) --- esphome/components/lvgl/defines.py | 10 +++------- esphome/components/lvgl/lv_validation.py | 11 +++-------- esphome/components/lvgl/lvcode.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1d528b2f73..91077a1ff4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging -from typing import TYPE_CHECKING, Any +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS @@ -96,13 +96,9 @@ class LValidator: return None if isinstance(value, Lambda): # Local import to avoid circular import - from .lvcode import CodeContext, LambdaContext + from .lvcode import get_lambda_context_args - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 9c1dd22085..947e44b131 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any +from typing import Any import esphome.codegen as cg from esphome.components import image @@ -404,14 +404,9 @@ class TextValidator(LValidator): self, value: Any, args: list[tuple[SafeExpType, str]] | None = None ) -> Expression: # Local import to avoid circular import at module level + from .lvcode import get_lambda_context_args - from .lvcode import CodeContext, LambdaContext - - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index e2c70642a8..b79d1e88dd 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,4 +1,5 @@ import abc +from typing import TYPE_CHECKING from esphome import codegen as cg from esphome.config import Config @@ -200,6 +201,21 @@ class LvContext(LambdaContext): return self.add(*args) +def get_lambda_context_args() -> list[tuple[SafeExpType, str]]: + """Get automation parameters from the current lambda context if available. + + When called from outside LVGL's context (e.g., from interval), + CodeContext.code_context will be None, so return empty args. + """ + if CodeContext.code_context is None: + return [] + if TYPE_CHECKING: + # CodeContext base class doesn't define get_automation_parameters(), + # but LambdaContext and LvContext (the concrete implementations) do. + assert isinstance(CodeContext.code_context, LambdaContext) + return CodeContext.code_context.get_automation_parameters() + + class LocalVariable(MockObj): """ Create a local variable and enclose the code using it within a block. From f243e609a51b96771269b37e206e4aaaec811930 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:35:58 -1000 Subject: [PATCH 40/43] [wifi] Use StringRef and std::span in WiFiConnectStateListener to avoid allocations (#12672) --- esphome/components/wifi/wifi_component.h | 4 +++- esphome/components/wifi/wifi_component_esp8266.cpp | 5 +++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++-- esphome/components/wifi/wifi_component_libretiny.cpp | 5 +++-- esphome/components/wifi/wifi_component_pico_w.cpp | 7 +++++-- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 6 +++--- esphome/components/wifi_info/wifi_info_text_sensor.h | 6 ++++-- esphome/components/wifi_signal/wifi_signal_sensor.h | 4 +++- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 604efa8a7e..4f888292f1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -6,7 +6,9 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" +#include #include #include @@ -274,7 +276,7 @@ class WiFiScanResultsListener { */ class WiFiConnectStateListener { public: - virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; + virtual void on_wifi_connect_state(StringRef ssid, std::span bssid) = 0; }; /** Listener interface for WiFi power save mode changes. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 550b5579ff..598ae2d5b7 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -526,7 +526,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -559,8 +559,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connected = false; s_sta_connecting = false; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif break; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 212514af93..67314ae31f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -737,7 +737,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -772,8 +772,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 340537b228..2aa6fa3484 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -303,7 +303,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -357,8 +357,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ s_sta_connecting = false; #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif break; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 61709852ff..b755b8544f 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -256,8 +256,10 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_LISTENERS + String ssid = WiFi.SSID(); + bssid_t bssid = this->wifi_bssid(); for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); } // For static IP configurations, notify IP listeners immediately as the IP is already configured #ifdef USE_WIFI_MANUAL_IP @@ -275,8 +277,9 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } #endif } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index eae0f87b40..0cca3e16ef 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -103,8 +103,8 @@ void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_list void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { - this->publish_state(ssid); +void SSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { + this->publish_state(ssid.str()); } /**************** @@ -115,7 +115,7 @@ void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_lis void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { +void BSSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { char buf[18] = "unknown"; if (mac_address_is_valid(bssid.data())) { format_mac_addr_upper(bssid.data(), buf); diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index b2242372da..6beb1372f5 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -2,10 +2,12 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI #include +#include namespace esphome::wifi_info { @@ -52,7 +54,7 @@ class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, pub void dump_config() override; // WiFiConnectStateListener interface - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { @@ -61,7 +63,7 @@ class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, pu void dump_config() override; // WiFiConnectStateListener interface - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; class PowerSaveModeWiFiInfo final : public Component, diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index 9f581f1eb2..2e1f8cbb2b 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -2,9 +2,11 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI +#include namespace esphome::wifi_signal { #ifdef USE_WIFI_LISTENERS @@ -28,7 +30,7 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { #ifdef USE_WIFI_LISTENERS // WiFiConnectStateListener interface - update RSSI immediately on connect - void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override { this->update(); } + void on_wifi_connect_state(StringRef ssid, std::span bssid) override { this->update(); } #endif }; From a6097f4a0f7fd8229d12fdd61bf627b08aad7aff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:36:19 -1000 Subject: [PATCH 41/43] [wifi] Eliminate heap allocations in dump_config logging (#12664) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++---- esphome/components/wifi/wifi_component.h | 6 ++++++ .../components/wifi/wifi_component_esp8266.cpp | 12 ++++++++++++ .../components/wifi/wifi_component_esp_idf.cpp | 13 +++++++++++++ .../components/wifi/wifi_component_libretiny.cpp | 8 ++++++++ .../components/wifi/wifi_component_pico_w.cpp | 8 ++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5fa894d8f9..50c0938cf1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -899,12 +899,20 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " Disabled"); return; } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { - ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf)); } } int8_t rssi = wifi_rssi(); + // Use stack buffers for SSID and all IP addresses to avoid heap allocations + char ssid_buf[SSID_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'") "\n" " BSSID: " LOG_SECRET("%s") "\n" @@ -915,9 +923,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf), + wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf)); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid())); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4f888292f1..ff2bfe12a4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -61,6 +61,9 @@ namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; +/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) +static constexpr size_t SSID_BUFFER_SIZE = 33; + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -408,6 +411,9 @@ class WiFiComponent : public Component { network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); + /// Write SSID to buffer without heap allocation. + /// Returns pointer to buffer, or empty string if not connected. + const char *wifi_ssid_to(std::span buffer); bssid_t wifi_bssid(); int8_t wifi_rssi(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 598ae2d5b7..1c744648bb 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -913,6 +913,18 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + struct station_config conf {}; + if (!wifi_station_get_config(&conf)) { + buffer[0] = '\0'; + return buffer.data(); + } + // conf.ssid is uint8[32], not null-terminated if full + size_t len = strnlen(reinterpret_cast(conf.ssid), sizeof(conf.ssid)); + memcpy(buffer.data(), conf.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { if (WiFi.status() != WL_CONNECTED) return WIFI_RSSI_DISCONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 67314ae31f..b26ac3d2e2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1086,6 +1086,19 @@ std::string WiFiComponent::wifi_ssid() { size_t len = strnlen(ssid_s, sizeof(info.ssid)); return {ssid_s, len}; } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + wifi_ap_record_t info{}; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + buffer[0] = '\0'; + return buffer.data(); + } + // info.ssid is uint8[33], but only 32 bytes are SSID data + size_t len = strnlen(reinterpret_cast(info.ssid), 32); + memcpy(buffer.data(), info.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2aa6fa3484..9b8653d0db 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -554,6 +554,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct LibreTiny API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index b755b8544f..1aa737ff4a 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -214,6 +214,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct CYW43 API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } From 5e99dd14ae78a9c0a59f4595a91af945430c3ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 08:36:35 -1000 Subject: [PATCH 42/43] [ethernet] Eliminate heap allocations in dump_config logging (#12665) --- .../ethernet/ethernet_component.cpp | 28 +++++++++++++------ .../components/ethernet/ethernet_component.h | 2 ++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 793ebdec42..114000401f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -644,6 +644,12 @@ void EthernetComponent::dump_connect_params_() { dns_ip2 = dns_getserver(1); } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " IP Address: %s\n" " Hostname: '%s'\n" @@ -651,9 +657,9 @@ void EthernetComponent::dump_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - network::IPAddress(&ip.ip).str().c_str(), App.get_name().c_str(), - network::IPAddress(&ip.netmask).str().c_str(), network::IPAddress(&ip.gw).str().c_str(), - network::IPAddress(dns_ip1).str().c_str(), network::IPAddress(dns_ip2).str().c_str()); + network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); #if USE_NETWORK_IPV6 struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -665,12 +671,13 @@ void EthernetComponent::dump_connect_params_() { } #endif /* USE_NETWORK_IPV6 */ + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " MAC Address: %s\n" " Is Full Duplex: %s\n" " Link Speed: %u", - this->get_eth_mac_address_pretty().c_str(), YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), - this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -711,11 +718,16 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { } std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span buf) { uint8_t mac[6]; get_eth_mac_address_raw(mac); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); } eth_duplex_t EthernetComponent::get_duplex_mode() { diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index bffed4dc4a..490a9d026e 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #ifdef USE_ESP32 @@ -93,6 +94,7 @@ class EthernetComponent : public Component { void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); + const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); bool powerdown(); From 45e61f100c0e45c957a081ad9e3fdade7a7f4041 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:59:55 -0500 Subject: [PATCH 43/43] [core] Replace USE_ESP_IDF with USE_ESP32 across components (#12673) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- .clang-tidy.hash | 2 +- esphome/components/audio/audio_reader.cpp | 2 +- esphome/components/audio/audio_reader.h | 2 +- esphome/components/captive_portal/__init__.py | 9 ++++----- .../captive_portal/captive_portal.cpp | 5 ++--- .../captive_portal/captive_portal.h | 20 +++++++++---------- .../captive_portal/dns_server_esp32_idf.cpp | 4 ++-- .../captive_portal/dns_server_esp32_idf.h | 4 ++-- esphome/components/climate/climate.cpp | 2 +- esphome/components/debug/debug_esp32.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 18 +++-------------- esphome/components/esp_ldo/__init__.py | 2 +- .../components/micro_wake_word/__init__.py | 2 +- .../components/micro_wake_word/automation.h | 2 +- .../micro_wake_word/micro_wake_word.cpp | 4 ++-- .../micro_wake_word/micro_wake_word.h | 4 ++-- .../micro_wake_word/preprocessor_settings.h | 2 +- .../micro_wake_word/streaming_model.cpp | 2 +- .../micro_wake_word/streaming_model.h | 2 +- esphome/components/mipi_dsi/display.py | 2 +- esphome/components/mipi_rgb/display.py | 2 +- esphome/components/mipi_spi/display.py | 2 +- esphome/components/mixer/speaker/__init__.py | 4 +--- .../components/mqtt/mqtt_backend_esp32.cpp | 10 ---------- esphome/components/network/ip_address.h | 2 +- esphome/components/openthread/__init__.py | 1 - .../components/openthread/openthread_esp.cpp | 2 +- esphome/components/qspi_dbi/display.py | 2 +- esphome/components/qspi_dbi/qspi_dbi.cpp | 2 +- esphome/components/qspi_dbi/qspi_dbi.h | 2 +- esphome/components/rpi_dpi_rgb/display.py | 1 - .../speaker/media_player/__init__.py | 2 +- .../speaker/media_player/audio_pipeline.cpp | 2 +- .../speaker/media_player/audio_pipeline.h | 2 +- .../speaker/media_player/automation.h | 2 +- .../media_player/speaker_media_player.cpp | 2 +- .../media_player/speaker_media_player.h | 2 +- esphome/components/spi/__init__.py | 4 ++-- esphome/components/st7701s/display.py | 2 +- esphome/components/usb_host/__init__.py | 1 - .../voice_assistant/voice_assistant.cpp | 2 +- .../web_server/ota/ota_web_server.cpp | 4 ++-- esphome/config_validation.py | 11 +++++++++- esphome/core/defines.h | 8 ++------ esphome/core/log.cpp | 2 +- esphome/core/log.h | 7 ++----- platformio.ini | 1 + tests/component_tests/mipi_spi/test_init.py | 17 ---------------- 48 files changed, 74 insertions(+), 119 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 240b205158..e1f5e096c0 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9 +5ac05ac603766d76b86a05cdf6a43febcaae807fe9e2406d812c47d4b5fed91d diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 6966c95db7..7794187a69 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -1,6 +1,6 @@ #include "audio_reader.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include "esphome/core/hal.h" diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 3fdc3c3ff2..0b73923e84 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "audio.h" #include "audio_transfer_buffer.h" diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 763e2e4ec5..232b868e82 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -97,10 +97,6 @@ async def to_code(config): cg.add_define("USE_CAPTIVE_PORTAL") if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("ESP32 Async UDP", None) - cg.add_library("DNSServer", None) - cg.add_library("WiFi", None) if CORE.is_esp8266: cg.add_library("DNSServer", None) if CORE.is_libretiny: @@ -110,6 +106,9 @@ async def to_code(config): # Only compile the ESP-IDF DNS server when using ESP-IDF framework FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF}, + "dns_server_esp32_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index e1f92d2d2b..749aa705df 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -69,12 +69,11 @@ void CaptivePortal::start() { network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) // Create DNS server instance for ESP-IDF this->dns_server_ = make_unique(); this->dns_server_->start(ip); -#endif -#ifdef USE_ARDUINO +#elif defined(USE_ARDUINO) this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->start(53, ESPHOME_F("*"), ip); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..0c63a3670a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -2,11 +2,10 @@ #include "esphome/core/defines.h" #ifdef USE_CAPTIVE_PORTAL #include -#ifdef USE_ARDUINO -#include -#endif -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) #include "dns_server_esp32_idf.h" +#elif defined(USE_ARDUINO) +#include #endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -23,15 +22,14 @@ class CaptivePortal : public AsyncWebHandler, public Component { void setup() override; void dump_config() override; void loop() override { -#ifdef USE_ARDUINO - if (this->dns_server_ != nullptr) { - this->dns_server_->processNextRequest(); - } -#endif -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) if (this->dns_server_ != nullptr) { this->dns_server_->process_next_request(); } +#elif defined(USE_ARDUINO) + if (this->dns_server_ != nullptr) { + this->dns_server_->processNextRequest(); + } #endif } float get_setup_priority() const override; @@ -64,7 +62,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; -#if defined(USE_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ARDUINO) || defined(USE_ESP32) std::unique_ptr dns_server_{nullptr}; #endif }; diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 740107400a..5188b2047f 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -1,5 +1,5 @@ #include "dns_server_esp32_idf.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -202,4 +202,4 @@ void DNSServer::process_next_request() { } // namespace esphome::captive_portal -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 13d9def8e3..3e0ac07373 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include "esphome/core/helpers.h" @@ -24,4 +24,4 @@ class DNSServer { } // namespace esphome::captive_portal -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 229862ce01..2d35509493 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -369,7 +369,7 @@ optional Climate::restore_state_() { } void Climate::save_state_() { -#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ +#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" #define TEMP_IGNORE_MEMACCESS diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 1c3dc3699b..25852b32a7 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -200,7 +200,7 @@ void DebugComponent::get_device_info_(std::string &device_info) { #ifdef USE_ARDUINO ESP_LOGD(TAG, "Framework: Arduino"); device_info += "Arduino"; -#elif defined(USE_ESP_IDF) +#elif defined(USE_ESP32) ESP_LOGD(TAG, "Framework: ESP-IDF"); device_info += "ESP-IDF"; #else diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 42f8ab8fd4..87b5e2b738 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -24,7 +24,9 @@ extern "C" { #include #ifdef USE_ARDUINO -#include +// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c). +// Without this, esp_bt_controller_init() fails with ESP_ERR_INVALID_STATE. +extern "C" bool btInUse() { return true; } // NOLINT(readability-identifier-naming) #endif namespace esphome::esp32_ble { @@ -165,12 +167,6 @@ void ESP32BLE::advertising_init_() { bool ESP32BLE::ble_setup_() { esp_err_t err; #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return false; - } -#else if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { // start bt controller if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { @@ -195,7 +191,6 @@ bool ESP32BLE::ble_setup_() { return false; } } -#endif esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); #else @@ -334,12 +329,6 @@ bool ESP32BLE::ble_dismantle_() { } #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#ifdef USE_ARDUINO - if (!btStop()) { - ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status()); - return false; - } -#else if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) { // stop bt controller if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) { @@ -363,7 +352,6 @@ bool ESP32BLE::ble_dismantle_() { return false; } } -#endif #else if (esp_hosted_bt_controller_disable() != ESP_OK) { ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index 38e684c537..f136dd149b 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( } ) ), - cv.only_with_esp_idf, + cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32P4]), ) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 0d478f749b..74696584da 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -368,7 +368,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index e1795a7e64..218ce9e4bc 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -3,7 +3,7 @@ #include "micro_wake_word.h" #include "streaming_model.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 namespace esphome { namespace micro_wake_word { diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index b8377ead38..d7e80efc84 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -1,6 +1,6 @@ #include "micro_wake_word.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -473,4 +473,4 @@ bool MicroWakeWord::update_model_probabilities_(const int8_t audio_features[PREP } // namespace micro_wake_word } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index 84261eaa5b..b427e4dfcb 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "preprocessor_settings.h" #include "streaming_model.h" @@ -140,4 +140,4 @@ class MicroWakeWord : public Component } // namespace micro_wake_word } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/preprocessor_settings.h b/esphome/components/micro_wake_word/preprocessor_settings.h index 3de21de92e..c9d195b49b 100644 --- a/esphome/components/micro_wake_word/preprocessor_settings.h +++ b/esphome/components/micro_wake_word/preprocessor_settings.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 2b073cce56..47d2c70e13 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -1,6 +1,6 @@ #include "streaming_model.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index b7b22b9700..0811bfb19b 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "preprocessor_settings.h" diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 90c4cc082e..c288b33cd2 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -165,8 +165,8 @@ def model_schema(config): ) return cv.All( schema, + cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32P4]), - cv.only_with_esp_idf, ) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 61dbeb8ed4..96e167b2e6 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -224,8 +224,8 @@ def _config_schema(config): schema = model_schema(config) return cv.All( schema, + cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32S3]), - cv.only_with_esp_idf, )(config) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 50ea826eab..69bf133c68 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -224,7 +224,7 @@ def model_schema(config): } ) if bus_mode != TYPE_SINGLE: - return cv.All(schema, cv.only_with_esp_idf) + return cv.All(schema, cv.only_on_esp32) return schema diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 46729f8eda..c4069851af 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -93,9 +93,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_NUM_CHANNELS): cv.int_range(min=1, max=2), cv.Optional(CONF_QUEUE_MODE, default=False): cv.boolean, - cv.SplitDefault(CONF_TASK_STACK_IN_PSRAM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf - ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, } ), cv.only_on([PLATFORM_ESP32]), diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index dcc51ed60e..e3105f4860 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -232,16 +232,6 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) { this_mqtt->mqtt_event_pool_.release(elem); } } - - // Clean up any remaining items in the queue - struct QueueElement *elem; - while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { - this_mqtt->mqtt_event_pool_.release(elem); - } - - // Note: EventPool destructor will clean up the pool itself - // Task will delete itself - vTaskDelete(nullptr); } bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 27cc212a47..b719d1a70e 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -8,7 +8,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/macros.h" -#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) #include #endif diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 050e45cdc9..26c05a0a86 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -152,7 +152,6 @@ CONFIG_SCHEMA = cv.All( } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), - cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2]), _validate, _require_vfs_select, diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index b47e4b884a..1f18e51496 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -1,5 +1,5 @@ #include "esphome/core/defines.h" -#if defined(USE_OPENTHREAD) && defined(USE_ESP_IDF) +#if defined(USE_OPENTHREAD) && defined(USE_ESP32) #include #include "openthread.h" diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 74d837a794..e4440c9b81 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -154,7 +154,7 @@ CONFIG_SCHEMA = cv.All( upper=True, key=CONF_MODEL, ), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index 6c95bb7cf2..24b9a0ce0a 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP_IDF) && defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3) #include "qspi_dbi.h" #include "esphome/core/log.h" diff --git a/esphome/components/qspi_dbi/qspi_dbi.h b/esphome/components/qspi_dbi/qspi_dbi.h index f35f0e519c..3eee9bec47 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.h +++ b/esphome/components/qspi_dbi/qspi_dbi.h @@ -3,7 +3,7 @@ // #pragma once -#if defined(USE_ESP_IDF) && defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_buffer.h" diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index 8e9da43a74..e92eee7c0c 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -122,7 +122,6 @@ CONFIG_SCHEMA = cv.All( ) ), only_on_variant(supported=[VARIANT_ESP32S3]), - cv.only_with_esp_idf, ) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 4ca57f2c4a..370b4576a7 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -315,7 +315,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), } ), - cv.only_with_esp_idf, + cv.only_on_esp32, _validate_repeated_speaker, _request_high_performance_networking, ) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index dc8572ae43..8be37d740a 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -1,6 +1,6 @@ #include "audio_pipeline.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include "esphome/core/hal.h" diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 98f43fda6e..6fffde6c20 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_reader.h" diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index fdf3db07f9..6270da7bd4 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -2,7 +2,7 @@ #include "speaker_media_player.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/audio/audio.h" #include "esphome/core/automation.h" diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 5722aab195..9a3a47bac8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -1,6 +1,6 @@ #include "speaker_media_player.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/log.h" diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index f1c564b63d..065926d0cf 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "audio_pipeline.h" diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index ad279dcf1a..045cdd09d3 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -311,7 +311,7 @@ def spi_mode_schema(mode): if mode == TYPE_SINGLE: return SPI_SINGLE_SCHEMA pin_count = 4 if mode == TYPE_QUAD else 8 - onlys = [cv.only_on([PLATFORM_ESP32]), cv.only_with_esp_idf] + onlys = [cv.only_on([PLATFORM_ESP32])] if pin_count == 8: onlys.append( only_on_variant( @@ -399,7 +399,7 @@ def spi_device_schema( cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True ), - cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), } if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index 6e4bff6431..3078158d25 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -161,8 +161,8 @@ CONFIG_SCHEMA = cv.All( } ).extend(spi.spi_device_schema(cs_pin_required=False, default_data_rate=1e6)) ), + cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32S3]), - cv.only_with_esp_idf, ) FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index cccabcf646..9e058ee20b 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -53,7 +53,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), - cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]), ) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 551f0370f2..b946e3b38a 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -947,7 +947,7 @@ void VoiceAssistant::on_set_configuration(const std::vector &active } // Enable only active wake words - for (auto ww_id : active_wake_words) { + for (const auto &ww_id : active_wake_words) { for (auto &model : this->micro_wake_word_->get_wake_words()) { if (model->get_id() == ww_id) { model->enable(); diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index b8bea40b84..3793f01eb5 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -10,7 +10,7 @@ #endif #ifdef USE_ARDUINO -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_LIBRETINY) #include #endif #endif // USE_ARDUINO @@ -118,7 +118,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Platform-specific pre-initialization #ifdef USE_ARDUINO -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_LIBRETINY) if (Update.isRunning()) { Update.abort(); } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 08fffa6cec..d085206ee8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -697,7 +697,16 @@ only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) -only_with_esp_idf = only_with_framework(Framework.ESP_IDF) + + +def only_with_esp_idf(obj): + """Deprecated: use only_on_esp32 instead.""" + _LOGGER.warning( + "cv.only_with_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + "Use cv.only_on_esp32 and/or cv.only_with_arduino instead." + ) + return only_with_framework(Framework.ESP_IDF)(obj) # Adapted from: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 11c5062140..a269f40479 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -164,13 +164,9 @@ #define USE_I2S_LEGACY #endif -// IDF-specific feature flags -#ifdef USE_ESP_IDF -#define USE_MQTT_IDF_ENQUEUE -#endif - // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK @@ -231,7 +227,7 @@ #define USE_ETHERNET_MANUAL_IP #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #define USE_MICRO_WAKE_WORD #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 909319dd28..8338efbb33 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -46,7 +46,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; diff --git a/esphome/core/log.h b/esphome/core/log.h index cade6a74c1..a2c4b35c6e 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -14,13 +14,10 @@ #endif // Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) #include #include #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include -#endif #ifdef USE_LIBRETINY #include #endif @@ -66,7 +63,7 @@ void esp_log_vprintf_(int level, const char *tag, int line, const char *format, #ifdef USE_STORE_LOG_STR_IN_FLASH void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif diff --git a/platformio.ini b/platformio.ini index a27fb1f537..e38e1a5f3c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -156,6 +156,7 @@ lib_deps = esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@2.0.1 ; audio + kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:arduino.build_flags} diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index 0c7dea2286..f752c41d8c 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -227,23 +227,6 @@ def test_esp32s3_specific_errors( run_schema_validation(config) -def test_framework_specific_errors( - set_core_config: SetCoreConfigCallable, -) -> None: - """Test framework-specific configuration errors""" - - set_core_config( - PlatformFramework.ESP32_ARDUINO, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, - ) - - with pytest.raises( - cv.Invalid, - match=r"This feature is only available with framework\(s\) esp-idf", - ): - run_schema_validation({"model": "wt32-sc01-plus"}) - - def test_custom_model_with_all_options( set_core_config: SetCoreConfigCallable, ) -> None: